自定义View的一些小Tip

2,142 阅读5分钟

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状态