1.onMeasure
1.1ViewGroup#onMeasure
有必要为你的child测量时支持margin。即调用 measureChildWithMargins()
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed)
measureChildWithMargins()和measureChild()区别
measureChildWithMargins实现如下
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
measureChild实现如下
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
从源码实现上可知
- measureChildWithMargins()在测量子View时会减去ViewGroup的padding和子View的margin以及ViewGroup已使用的宽高。剩余的就是子View最大可用空间
- measureChild()在测量子View时只减去了ViewGroup的padding。剩余的就是子View最大可用空间
考虑一个场景,如下代码所示
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<View
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginLeft="100dp"
android:layout_marginRight="100dp"
android:background="#FF0000" />
</LinearLayout>
如果LinearLayout使用measureChild()测量子View时。由于计算的子View可用空间和LinearLayout一样宽(LinearLayout的padding为0)。那么子View显示就和LinearLayout一样宽,子View设置的margin失效。
所以在自定义ViewGroup重写onMeasure()测量子View时需要考虑到子View的margin。即使用measureChildWithMargins()。
而measureChildWithMargins()中widthUsed和heightUsed参数又是什么意思呢?
- widthUsed 代表父控件已使用的宽度
- heightUsed 代表父控件已使用的高度
比如横向的LinearLayout传递widthUsed,由于widthUsed不断变大,导致后续的子View宽度可用空间越来越小。实际在子View测量时,子View宽度会被压缩。
比如纵向的LinearLayout传递heightUsed,由于heightUsed不断变大,导致后续的子View高度可用空间越来越小。实际在子View测量时,子View高度会被压缩。
那如要要定义ViewGroup时,在测量子子View时该使用哪种方式呢?
如果需要支持子View margin属性使用measureChildWithMargins()。反之使用measureChild()。话说一般情况下都需要支持子View margin属性。
那么在使用measureChildWithMargins()测试子View时,widthUsed和heightUsed需不需要传0?
这就要看你想要的结果是什么样的了。如果是自定义TagFlowLayout,在测量子View时 widthUsed和heightUsed传0就好。即使边界子View(最右边和最下边)显示一半也要拒绝子View实际尺寸变小。可以通过嵌套一层ScrollView来解决显示不全问题。但如果是子View实际尺寸变小了,再嵌套ScrllView也没用,因为它就那么大。
1.2View#onMeasure
为你的View支持AS_MOST和UNSPECIFIED测量模式,先说下AS_MOST情况。
- AS_MOST情况
如果在自定义View对AS_MOST不做处理。那么当View设置wrap_content时效果和match_parent一样。原因如下:
ViewGroup在测量子View时最终调用child.measure(childWidthMeasureSpec, childHeightMeasureSpec)。而childWidthMeasureSpec和childHeightMeasureSpec生成规则如下:
//父控件测量模式
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
//父控件的可用空间
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch(specModel){
case MeasureSpec.EXACTLY:
if (childDimension == LayoutParams.MATCH_PARENT) {
//当子View设置match_parent时其大小就是父控件的可用空间,模式为EXACTLY
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//当子View设置wrap_content时其大小就是父控件的可用空间,模式为AT_MOST
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
...
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
那么一般情况下如何处理AS_MOST测量规格?
根据实际需求来处理,比如说UI上展示的这个View宽度为300dp。那可以在AS_MOST情况下做如下处理。
val widthSpecModel = MeasureSpec.getMode(widthMeasureSpec)
val widthSpecSize = MeasureSpec.getSize(widthMeasureSpec)
var result =0
if(MeasureSpec.AS_MOST==withSpecModel){
result =Math.min(300,widthSpecSize)
}else{
result = widthSpecSize
}
当然像这种硬编码的判断不需要自己写,Android已经提供了封装的方法
//View#resolveSize()
//size为你期望的大小。measureSpec直接传递onMeasure中的宽或者高measureSpec参数
public static int resolveSize(int size, int measureSpec) {
return resolveSizeAndState(size, measureSpec, 0) & MEASURED_SIZE_MASK;
}
为什么是最小300和widthSpecSize最小的那个呢?
因为AS_MOST模式下是父控件要求子控件最大不能超过我的剩余空间。
- UNSPECIFIED情况
什么情况下子View测量规格中才会出现UNSPECIFIED模式。一般来说如果子View外部存在ScrollView时或者可滑动的View时。在生成childHeightMeasureSpec时会将测量模式会改为UNSPECIFIED。而为什么UNSPECIFIED模式呢?这是因为滑动特性,因为存在滑动,所以无论里面内容有多大都可以通过滑动来显示。所以父容器就不限制子View的大小,你多大都可以。
那么在写自定义View时应该在onMeasure中对UNSPECIFIED支持。应该返回实际大小,或者你期望的大小。当然View#resolveSize()已经帮你判断好了,直接使用即可。
2.onLayout
对于ViewGroup在布局子View位置时有必要将自身padding和子View margin代入计算。不然ViewGroup的padding或者子View的margin会失效
3.LayoutParmas
对于自定义ViewGroup时如有必要提供默认的LayoutParams。为子View支持margin和其他属性。即重新一下三个方法(这里只是为子View支持margin属性,如想支持自定义属性继承MarginLayoutParams并返回即可)。
//如果xml文件没有声明子View的话并且在代码中addView时如没有设置LayoutParams则会调用此方法
override fun generateDefaultLayoutParams(): MarginLayoutParams {
return MarginLayoutParams(WRAP_CONTENT, WRAP_CONTENT)
}
//如果一个ViewGroup只是在xml文件声明子View的话,会调用此方法。为xml文件中声明的子View支持margin。如果需要支持其他属性可扩展MarginLayoutParams
override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
return MarginLayoutParams(context, attrs)
}
//当ViewGroup addView时。当前两个都为空时会调用此方法。
override fun generateLayoutParams(p: LayoutParams?): LayoutParams {
return MarginLayoutParams(p)
}
4.onDraw
-
在继承ViewGroup时不需要在onDraw()中绘制时,应标记自己不可绘制 setWillNotDraw(true) 反之则传递false。
-
在自定义View时需要在onDraw()中支持自身padding。
5.保存和恢复状态
在View或者ViewGroup中如有必要需重写onSaveInstanceState()和onRestoreInstanceState()方法来保存和恢复状态。即因资源和配置发生改变导致View重建,需要保存当前View状态和恢复当前View状态