一般来说一个自定义View包含以下三个部分:
布局、显示、交互
布局:onMeasure、onLayout (ViewGroup)
显示:onDraw–>canvas paint matrix clip rect animation path(贝塞尔) line
交互:事件分发 解决事件冲突的能力
我们可以查看android给我们提供的布局里面涉及onDraw的地方。
FrameLayout的源码看不到onDraw
LinearLayout有onDraw:判断一下是水平方向还是竖直方向,绘制一下行而已,非常简单。
总结:ViewGroup一般涉及onDraw的地方很少,或者需要的实现是比较少。
自定义View主要实现onMeasure + onDraw
自定义ViewGroup主要实现:onMeasure+onLayout
自定义布局继承自ViewGroup有四个构造函数,参数分别有:
Context、AttributeSet、int、int
其中第二个是我们在xml里面正常设置的android自动的宽高等熟悉的属性集合对,第三个是主题的id,第四个是自定义属性的id
如果用过一些第三方控件的同学应该知道。
实际上最终调用的inflate去实例化的时候也有对应的四个方法,但是最终都会调用到有四个参数的实例化方法,如果你没有传递这些参数进来则给默认值。
onMeasure和onLayout方法,其实主要是onMeasure,因为onLayout方法里面的参数都是来自onMeasure。
我们知道布局里面大小有三种类型设置,MATCH_PARENT、WRAP_CONTENT和确切的尺寸大小,当父布局设置WRAP_CONTENT的时候你知道给多少吗?显然不知道,所以我们的度量一般都是从子View开始度量。
我们的View都是树形结构,从顶层的ViewGroup开始,调用度量下层的子View进行度量,下层的布局又再次调用它的子View进行度量,度量是从上而下,但是真正开始计算是从底部的View开始度量结束一层层计算往上提交。
这里面有一个关键的辅助类:MeasureSpec
我们通过或者一个View的layoutParmas,里面记录了这个view设置的宽高属性,它的宽和高分别是多少。那么我们能知道把它转成一个具体的多少dp dip等尺寸吗?layoutParams可以看做是子View自身的布局大小期望值,但是我们具体最终会分配多少呢?这个时候MeasureSpec就上场了,通过将父View自身能拥有的大小和子View自身的期望值通过一系列的度量计算之后返回的MeasureSpec就是子View所能拥有的大小。
还是面对对象的逻辑:子View如果设置WRAP_CONTENT我们能知道给多少吗?如果我们自己也是WRAP_CONTENT呢?
所以需要MeasureSpec来作为一个辅助的计算工具类。
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
定义三种mode,分别对应MATCH_PARENT、WRAP_CONTENT和确切的尺寸大小。
MeasureSpec 是通过 int 值的位运算来实现的,一个int有32位。
高两位分别代表这三种mode:10 00 01,剩下的来标识具体的尺寸绰绰有余
通过MeasureSpec 的 makeMeasureSpec来转换成具体的大小
度量的判断在网上有一张非常经典的图这里我就不盗图了,免得又被判定为高度相似,我有几篇文章盗了几张图被判断高度相似了。
那张图其实没什么卵用,因为你根本记不住也没必要去记,java的代码都是很面对对象的,是可以理解的方式来学习的。
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {}
int spec:这个度量的子View的方法,第一个参数是你自己当前能拥有多大的尺寸,也就是你的父布局传递下来的,一层一层传递。
int padding:子View所有占据的宽高得先减去我的内间距
int childDimension:子View的layoutParams,它所有要求的宽或者高
测量宽高:
var childwithMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
right + left, params.width)
var childheightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
top + bottom, params.height)
度量方法源码:
//这三个参数的含义上面解释了
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//子View的期望mode
int specMode = MeasureSpec.getMode(spec);
//子View的期望尺寸
int specSize = MeasureSpec.getSize(spec);
//减去我的内间距
int size = Math.max(0, specSize - padding);
//跟父布局计算之后的子View想要的期望mode和尺寸
//不是子View想要多少最终就有多少,还是需要一层层传递上去等待最终的分配
int resultSize = 0;
int resultMode = 0;
//具体的逻辑
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
//确切的子View想要的尺寸大小
if (childDimension >= 0) {
//你想要多少我就给你多少,当然先记着,等最终分配,包括mode
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
//子View是match,那么就把我自己所能分配给子View全分配给你
//记住mode
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
//WRAP_CONTENT,自适应,那么我也全给你,当然要看最终的分配
//也记住mode,因为你是可以压缩给的
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
//子View有设置最大的尺寸的时候就记录最大的尺寸和mode
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
//子View如果是MATCH就把我自己所有的都给你,记住mode
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
//自适应,跟上面一样
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
//如果你有确切的尺寸,那么就记录一下尺寸和mode
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
//子View想要和父View一样大,那就把自己全给你,记住mode
//View.sUseZeroUnspecifiedMeasureSpec是一个静态变量 false
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
//和上面的一样,子View子使用全给你,并记住mode
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
经过上面的将layoutParams转为MeasureSpec之后,我们才能真正的度量子View的确切大小
//测量子View
childView.measure(childwithMeasureSpec,childheightMeasureSpec)
//获取测量之后的子View的宽高
var childMeasuredWith = childView.measuredWidth
var childheight = childView.measuredHeight
还有一点onMeasure可能会被调用多次,例如FrameLayout,所以你如果在onMeasure里面定义的变量最好小心一点,什么时候会被度量多次是由你的父布局决定的,具体看需求。
这是一个练习onMeasure和onLayout的例子:流式布局
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:text="搜索历史"
android:textColor="@android:color/black"
android:textSize="18sp"/>
<com.example.flowlayout.MyFlowLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="10dp"
android:layout_margin="8dp">
<TextView
android:id="@+id/sgw"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="水果味孕妇奶粉" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="儿童洗衣机" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="洗衣机全自动" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="小度" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="儿童汽车可坐人" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="抽真空收纳袋" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="儿童滑板车" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="稳压器 电容" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="羊奶粉" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="奶粉1段" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="图书勋章日" />
</com.example.flowlayout.MyFlowLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:text="搜索发现"
android:textColor="@android:color/black"
android:textSize="18sp" />
<com.example.flowlayout.MyFlowLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="惠氏3段" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="奶粉2段" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="图书勋章日" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="伯爵茶" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="阿迪5折秒杀" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="蓝胖子" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="婴儿洗衣机" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="小度在家" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="遥控车可坐" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="搬家袋" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="剪刀车" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="滑板车儿童" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="空调风扇" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="空鼓锤" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="笔记本电脑" />
</com.example.flowlayout.MyFlowLayout>
</LinearLayout>
</ScrollView>
java代码的实现:
public class FlowLayout extends ViewGroup {
private static final String TAG = "FlowLayout";
private int mHorizontalSpacing = dp2px(16); //每个item横向间距
private int mVerticalSpacing = dp2px(8); //每个item横向间距
private List<List<View>> allLines = new ArrayList<>(); // 记录所有的行,一行一行的存储,用于layout
List<Integer> lineHeights = new ArrayList<>(); // 记录每一行的行高,用于layout
public FlowLayout(Context context) {
super(context);
// initMeasureParams();
}
//反射
public FlowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
// initMeasureParams();
}
//主题style
public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// initMeasureParams();
}
//四个参数 自定义属性
private void clearMeasureParams() {
allLines.clear();
lineHeights.clear();
}
//度量
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
clearMeasureParams();//内存 抖动
//先度量孩子
int childCount = getChildCount();
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
int selfWidth = MeasureSpec.getSize(widthMeasureSpec); //ViewGroup解析的父亲给我的宽度
int selfHeight = MeasureSpec.getSize(heightMeasureSpec); // ViewGroup解析的父亲给我的高度
List<View> listViews = new ArrayList<>(); //保存一行中的所有的view
int lineWidthUsed = 0; //记录这行已经使用了多宽的size
int lineHeight = 0; // 一行的行高
int parentNeededWidth = 0; // measure过程中,子View要求的父ViewGroup的宽
int parentNeededHeight = 0; // measure过程中,子View要求的父ViewGroup的高
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
LayoutParams childLP = childView.getLayoutParams();
if (childView.getVisibility() != View.GONE) {
//将layoutParams转变成为 measureSpec
int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, paddingLeft + paddingRight,
childLP.width);
int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, paddingTop + paddingBottom,
childLP.height);
childView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
//获取子view的度量宽高
int childMesauredWidth = childView.getMeasuredWidth();
int childMeasuredHeight = childView.getMeasuredHeight();
//如果需要换行
if (childMesauredWidth + lineWidthUsed + mHorizontalSpacing > selfWidth) {
//一旦换行,我们就可以判断当前行需要的宽和高了,所以此时要记录下来
allLines.add(listViews);
lineHeights.add(lineHeight);
parentNeededHeight = parentNeededHeight + lineHeight + mVerticalSpacing;
parentNeededWidth = Math.max(parentNeededWidth, lineWidthUsed + mHorizontalSpacing);
listViews = new ArrayList<>();
lineWidthUsed = 0;
lineHeight = 0;
}
// view 是分行layout的,所以要记录每一行有哪些view,这样可以方便layout布局
listViews.add(childView);
//每行都会有自己的宽和高
lineWidthUsed = lineWidthUsed + childMesauredWidth + mHorizontalSpacing;
lineHeight = Math.max(lineHeight, childMeasuredHeight);
//处理最后一行数据
if (i == childCount - 1) {
allLines.add(listViews);
lineHeights.add(lineHeight);
parentNeededHeight = parentNeededHeight + lineHeight + mVerticalSpacing;
parentNeededWidth = Math.max(parentNeededWidth, lineWidthUsed + mHorizontalSpacing);
}
}
}
//再度量自己,保存
//根据子View的度量结果,来重新度量自己ViewGroup
// 作为一个ViewGroup,它自己也是一个View,它的大小也需要根据它的父亲给它提供的宽高来度量
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int realWidth = (widthMode == MeasureSpec.EXACTLY) ? selfWidth: parentNeededWidth;
int realHeight = (heightMode == MeasureSpec.EXACTLY) ?selfHeight: parentNeededHeight;
setMeasuredDimension(realWidth, realHeight);
}
//布局
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int lineCount = allLines.size();
int curL = getPaddingLeft();
int curT = getPaddingTop();
for (int i = 0; i < lineCount; i++){
List<View> lineViews = allLines.get(i);
int lineHeight = lineHeights.get(i);
for (int j = 0; j < lineViews.size(); j++){
View view = lineViews.get(j);
int left = curL;
int top = curT;
int right = left + view.getMeasuredWidth();
int bottom = top + view.getMeasuredHeight();
view.layout(left,top,right,bottom);
curL = right + mHorizontalSpacing;
}
curT = curT + lineHeight + mVerticalSpacing;
curL = getPaddingLeft();
}
}
// @Override
// protected void onDraw(Canvas canvas) {
// super.onDraw(canvas);
// }
public static int dp2px(int dp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, Resources.getSystem().getDisplayMetrics());
}
}
kotlin代码的实现:
class MyFlowLayout : ViewGroup {
constructor(context: Context):super(context){}
constructor(context: Context,attrs: AttributeSet):super(context,attrs){}
//记录所有的行,一行一行地存储,用于layout
var allLines:MutableList<MutableList<View>> = mutableListOf()
//记录每一行的行高,用于layout
var allHeight : MutableList<Int> = mutableListOf()
val mHorizontalSpacing = dp2px(16) //每个item横向间距
val mVerticalSpacing = dp2px(8) //每个item横向间距
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
clearMeasureParams()//内存 抖动
//记录当前行已经使用了多宽
var lineWithSize = 0;
//一行的行高
var heightMaxSzie = 0;
//获取父布局给我的宽高
var selfwith = MeasureSpec.getSize(widthMeasureSpec)
var selfheight = MeasureSpec.getSize(heightMeasureSpec)
//子View所能占据的最大宽高必须先减去我的内间距
val top = paddingTop
val right = paddingRight
val left = paddingLeft
val bottom = paddingBottom
//测量过程中子view要求的父布局宽高
var parnetNeedWith = 0;
var parentNeedHeight = 0;
//保存一行中所有的子View
var listView : MutableList<View> = mutableListOf()
//先度量子View
var childCout = childCount
//for循环递归调用
for (index in 0 until childCout) {
var childView = getChildAt(index)
var params = childView.layoutParams
if (childView.visibility != View.GONE) {
//将layoutParams转变成为 measureSpec
// 一个int 高两位来作为MeasureSpec 的类型判断,10 00 01 分别作为:确认的多少尺寸,match 和wrap 三种类型
//低30位表示具体的尺寸
var childwithMeasureSpec = getChildMeasureSpec(widthMeasureSpec, right + left, params.width)
var childheightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, top + bottom, params.height)
//测量子View
childView.measure(childwithMeasureSpec,childheightMeasureSpec)
//获取测量之后的子View的宽高
var childMeasuredWith = childView.measuredWidth
var childheight = childView.measuredHeight
//判断是否需要换行
if (childMeasuredWith + lineWithSize + mHorizontalSpacing > selfwith) {
//换行,记录宽高
allLines.add(listView)
allHeight.add(heightMaxSzie)
//测量过程中子view要求的父布局宽高,计算
parentNeedHeight = parentNeedHeight + childheight + mVerticalSpacing //加一行高度,计算的是总行高
parnetNeedWith = Math.max(parnetNeedWith,lineWithSize + mHorizontalSpacing) //?:已经使用的宽度 + 横向间距 :子View要求的宽度,取所有行的最大宽度
//listView 创建一个新再记录新的一行
listView = mutableListOf<View>()
lineWithSize = 0 //记录这行已经使用了多宽的size 重置为0
heightMaxSzie = 0 // 一行的行高 重置为0
}
lineWithSize = childMeasuredWith + lineWithSize + mHorizontalSpacing
heightMaxSzie = Math.max(heightMaxSzie,childheight) //取一行中子View最高的那个最为行高
listView.add(childView)
//处理最后一行,当最后一行不满足换行,但是也需要添加对应的子View到数据里去
if (index == childCout-1) {
//记录宽高
allLines.add(listView)
allHeight.add(heightMaxSzie)
parentNeedHeight = parentNeedHeight + childheight + mVerticalSpacing //加一行高度,计算的是总行高
parnetNeedWith = Math.max(parnetNeedWith,lineWithSize + mHorizontalSpacing) //?:已经使用的宽度 + 横向间距 :子View要求的宽度
}
}
}
//子View度量完了之后再度量自己
//根据子View的度量结果,来重新度量自己ViewGroup
// 作为一个ViewGroup,它自己也是一个View,它的大小也需要根据它的父亲给它提供的宽高来度量
var widthMode = MeasureSpec.getMode(widthMeasureSpec)
var heightMode = MeasureSpec.getMode(heightMeasureSpec)
//真实的宽高
//如果父View给自己分配的是确切的宽高,那么直接使用,如果不是,把子View希望的宽高设置成自己希望的宽高
//测量子View的时候我们已经把自己的padding传进去了,所以子View希望能给予的最大宽高是包括自己的需求的
var realwidth = if (widthMode == MeasureSpec.EXACTLY) selfwith else parnetNeedWith
var realheight = if (heightMode == MeasureSpec.EXACTLY) selfheight else parentNeedHeight
//设置自己度量的宽高
setMeasuredDimension(realwidth,realheight)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
//开始布局参数,有多少行,每一行有多少个子View我们已经清楚了
var outCount= allLines.size
//我们每一行的listView记录的是每一个子View,不包含我们自己padding,所有要加上
var top = paddingTop
var left =paddingLeft
var lineView: MutableList<View>
for (index in 0 until outCount) {
lineView = allLines.get(index)
var size = lineView.size
for (child in 0 until size) {
var view = lineView.get(child)
//每一个view都需要设置左上右下四个点
//每一次都拿外面定义的,内部不需要改变
//父View最左边开始第一个
var right = view.measuredWidth + left
var bottom = view.measuredHeight + top
view.layout(left,top,right,bottom)
//第二个同一行top是一样,left需要加上第一个的距离和同一行之间view的间距
left = right + mHorizontalSpacing
}
//外层每循环一次 topOut 需要重新设置,left则要复原
var height = allHeight.get(index)
// 顶部间距 + 当前行的行高 + 行间距
top += height + mVerticalSpacing
left = paddingLeft
}
}
private fun clearMeasureParams() {
allLines.clear()
allHeight.clear()
}
fun dp2px(dp: Int): Int {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), Resources.getSystem().displayMetrics).toInt()
}
}