解码自定义View-布局绘制与测量

一.引言

  最近在作重庆掌厅的项目,发现使用ViewGroup包裹的子view,常常子view的高度测量不许,为了了解这个问题的本质缘由,简单写了几个demo做为演示,自定义View是比较重要的,尤为是布局和测量流程
  测量和布局的方法实际上是比较少的,一个是 onMeasure,一个是 onLayout,这个是布局的核心所在。
下面咱们就开始布局过程:java

二.布局过程:

  • 肯定每一个view的位置和尺寸

  通俗来讲就是:计算每一个 view 的尺寸以及相对于他们的父view的相对位置,目的是帮助咱们写布局文件的难。ide

  • 做用:
    View绘制和触摸反馈作支持

三.布局的本质

A.从总体来看:

1. 测量过程:
  从根 View 递归调用每个子Viewmeasure()方法。让每一个子view进行自我测量,每次测量以后,它会算出每一个子view,都会算出本身的指望尺寸局,根据本身指望的尺寸得出子view,实际尺寸。布局

2. 布局过程:
  从根 View 递归调用到每一级的 layout() 方法,把测量过程得出的子view位置和尺寸传给子 view,子view保存。ui

3. 为何要有两个流程呢?
  主要是测量过程比较复杂,可能测量某个子view,直接获得它的位置和尺寸,可是有的时候,你可能须要测量屡次,你一次不行可能须要测量屡次。spa

B.从个体来看:
  1. 运行前,开发者在 xml 文件写入对View的布局要求layout_xxxcode

  2. view 在本身的onMeasure中,根据开发者在xml中写对子view的要求,和本身的可用空间,得出子view的具体尺寸要求。xml

  3. view在本身的onMeasure()中根据本身的父view的特性算出本身的指望值。递归

    • 若是是viewGroup,还会在这里调用每一个子viewmeasure()进行测量。
  4. view在子view计算出指望尺寸后,得出子view的实际尺寸和位置。
      开发

  5. view在本身的layout()方法中,将view传进本身的实际尺寸和位置保存。get

    • 若是是viewGroup,还会在onLayout里面每一个字viewonLayut()把它们尺寸位置传给它们。

二.具体开发:

a.extend已有的view,简单修改他们的尺寸
public class SquareImageView extends AppCompatImageView {
    public SquareImageView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

   /** 1. 重写`onMeasure()` */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 2. ⽤ getMeasuredWidth() 和 getMeasuredSize() 获取到测量出的尺寸
        int measuredWidth = getMeasuredWidth();
        int measuredHeight = getMeasuredHeight();
        // 3. 计算最终的尺寸
        int size = Math.max(measuredWidth, measuredHeight);
        // 4.用 setMeasuredDimension 把最终的结果保存起来


        setMeasuredDimension(size, size);
    }
}
复制代码

  须要注意的是在自定义view里面是不能用 onLayout 去获取 view 宽高的,由于父view可能会被测量屡次,拿到的宽高并不必定是真实的宽高。

  咱们须要获取的是测量阶段算出来的view的宽高,getWidthgetHeight其实是在layout里面获取的宽高。即右 - 左边距;那个实际的宽度和高度

  通常状况下,测量的结果都同样,你的宽度和高度在测量过程是拿不到的,还没布局,因此保险起见只能用getMeasureWidthgetMeasureHeight。就至关于你论文的初稿。真正定稿是getWidthgetHeight

  因此只要遵循一个原则,在绘制阶段,只能用getMeasureWidthgetMeasureHeight,在触摸反馈和布局阶段只能用getWidthgetHeight

b.彻底自定义View的尺寸
  • 重写 onMeasure
  • 计算本身的尺寸
  • resolveSize()resolveSizeAndState() 修正结果
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
    // 1.MeasureSpec.getMode(measureSpec) 和 MeasureSpec.getSize(measureSpec) 取出对本身的尺寸类型和具体尺寸
        final int specMode = MeasureSpec.getMode(measureSpec);
        final int specSize = MeasureSpec.getSize(measureSpec);
        final int result;
        switch (specMode) {
            case MeasureSpec.AT_MOST:
            // 若是measure spec 的model是 AT_MOST,表示view对子view的尺寸只限制了上限
                if (specSize < size) {
                // 若是计算出的size 不大于 spec 的size,而且在 resolveSizeAndState 会添加标志 MEASURED_STATE_TOO_SMALL 这个能够辅助父view的测量
                    result = specSize | MEASURED_STATE_TOO_SMALL;
                } else {
                    result = size;
                }
                break;
            case MeasureSpec.EXACTLY:
            // 若是模式是`EXACTLY`,表示父view对子view作出了精确的限制,因此就放弃计算的size直接选择measureSpec的size
                result = specSize;
                break;
            case MeasureSpec.UNSPECIFIED:
            // 若是measure spec 的 model 是 UNSPECIFIED , 表示父 view 对 子 view 没有任何尺寸限制,因此直接选用计算出来的size,忽略 spec 的 size
            default:
                result = size;
        }
        return result | (childMeasuredState & MEASURED_STATE_MASK);
    }
复制代码
  • 使用 setMeasureDimension(width,height)保存结果
@Override
   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
       int size = (int) ((PADDING + radius) * 2);

       int measuredWidth = resolveSize(size, widthMeasureSpec);
       int measuredHeight = resolveSize(size, heightMeasureSpec);

       setMeasuredDimension(measuredWidth, measuredHeight);
   }
复制代码
c.自定义Layout: 重写onMeasure()onLayout(): TagLayout

时间关系,明天总结~

四. 参考资料

相关文章
相关标签/搜索