阅读 2306

[Android 自定义 View] —— 深入总结 onMeasure、 onLayout

在这里插入图片描述
我的 Android 知识体系,欢迎 Star https://github.com/daishengda2018/AndroidKnowledgeSystem

onMeasure、onLayout 可以说是自定 View 的核心,但是很多开发者都没能理解其含义与作用,也不理解 onMeasure 、 xml 指定大小这二者的关系与差异,也不能区分 getMeasureWidth 与 getWidth 的本质区别又是什么。本文将通过理论加实践的方法带领大家深入理解 onMeasure 、onLayout 的定义、流程、具体使用方法与需要注意的细节。

自定义View —— onMeasure、 onLayout

布局过程的作用

  • 确定每个View的尺寸和位置
  • 作用:为绘制和触摸范围做支持
    • 绘制:知道往哪里了画
    • 触摸返回:知道用户点的是哪里

布局的流程

从整体看

  • 测量流程:从根 View 递归调用每一级子 View 的 measure 方法,对它们进行测量。
  • 布局流程:从根 View 递归调用每一级子 View 的 layout 方法,把测量过程得出的子 View 的位置和尺寸传给子 View,子 View 保存。

从个体看

对于每一个 View:

  1. 运行前,开发者会根据自己的需求在 xml 文件中写下对于 View 大小的期望值

  2. 在运行的时候,父 View 会在 onMeaure()中,根据开发者在 xml 中写的对子 View 的要求, 和自身的实际可用空间,得出对于子 View 的具体尺寸要求

  3. 子 View 在自己的 onMeasure中,根据 xml 中指定的期望值和自身特点(指 View 的定义者在onMeasrue中的声明)算出自己的**期望*

    如果是 ViewGroup 还会在 onMeasure 中,调用每个子 View 的 measure () 进行测量.

  4. 父 View 在子 View 计算出期望尺寸后,得出⼦ View 的实际尺⼨和位置

  5. ⼦ View 在自己的 layout() ⽅法中将父 View 传进来的实际尺寸和位置保存

    如果是 ViewGroup,还会在 onLayout() ⾥调用每个字 View 的 layout() 把它们的尺寸 置传给它们

为啥需要两个过程呢?

原因一

measure 的测量过程可能不止一次,比如有三个子 View 在一个 ViewGroup 里面,ViewGroup 的宽度是 warp_content,A 的宽度是 match_parent, B 和 C 是 warp_content, 此时 ViewGroup 的宽度是不固定的,怎么确定 A 的 match_parent 到底有多大呢?此时是如何测量的呢?

以 LinearLayout 为例:第一次测量 LinearLayout 的大小也是没有确定的,所以无法确定 A 的 match_parent 到底有多大,这时候的 LinearLayout 会对 A 直接测量为 0 ,然后测量 B、C 的宽度,因为 B、C 的大小是包裹内容的,在测量后就可以确定 LinearLayout 的宽度了:即为最长的 B 的宽度。

\[外链图片转存失败(img-io4cWzMo-1566741015606)(assets/image-20190816011514042.png)\]

这时候再对 A 进行第二次测量,直接设置为与 LinearLayout 相同的宽度,至此达到了 match_parent 的效果。

\[外链图片转(img-1YxB5phjh66741015609)(assets/image-20190816011559286.png)(assets/image-20190816011559286.png)\]

如果将 measure 和 layout 的过程糅合在一起,会导致两次测量的时候进行无用的 layout,消耗了更多的资源,所以为了性能,将其二者分开。

原因二

也是二者的职责相互独立,分为两个过程,可以使流程、代码更加清晰。

拓展

上面例子中的情况仅仅存在于 LinearLayout中,每种布局的测量机制是不同的。那么如果 A B C 三个 View 都是 match_parent LinearLayout 是如何做的呢?

  • 第一轮测量:LinearLayout 无法确定自己的大小,所以遇到子 View match_parent 都会测量为 0

\[外链图片转存失败(img-yJZK4eWe-1566741015610)(assets/image-20190816013740231.png)\]

  • 第二轮测量:都没有大小,LinearLayout 会让所有子 View 自由测量(父 View 不限制宽度)。每个测量之后都会变为和最宽的一样的宽度。

    \[外链图片转存失败(img-OAccRlTA-1566741015611)(assets/image-20190816013805676.png)\]

注意:

  • onMeasure 与 measure() 、onDraw 与 draw 的区别

    onXX 方法是调度过程,而 measure、draw 才是真正做事情的。可以从源码中看到 measure 中调用了 onMeasure 方法。

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
           // ……………
                if (cacheIndex < 0 || sIgnoreMeasureCache) {
                    // measure ourselves, this should set the measured dimension flag back
                    onMeasure(widthMeasureSpec, heightMeasureSpec);
                 // ………………
                }
    }
        
    复制代码
  • 为什么不把对于尺寸的要求直接交个子 View 而是要交给父 View 呢?

    因为有些场景子 View 的大小需要父 View 进行规划,例如上面的例子中 LinearLayout 的子 View 设置了 weight。

  • layout() 很少被使用到,因为他的改变没有通知父 View,这可能会导致布局重叠等问题 。在下面的「综合演练 —— 简单改写已有 View 的尺寸」中会有一个证明。

##onMeasure 方法

要明确的一个问题是: 什么时候需要我们自己实现 onMeasure 方法呢?

答:具体开发的时候有以下三种场景:

  • 当我们继承一个已有 View 的时候,简单改写他们的尺寸,比如自定义一个正方形的 ImageView,取宽高中较大的值为边长。
  • 完全进行自定义尺寸的计算。比如实现一个绘制圆形的 View 我们需要在尺寸为 warp_content 时指定一个大小例如下文中的「综合演练 —— 完全自定义 View 的尺寸」。
  • 自定义 Layout,这时候内部所有的子 View 的尺寸和位置都需要我们自己控制,需要重写 onMeasure()onLayout()方法。例如下文中的「综合演练 —— 自定义 Layout」

onLayout 方法

onLayout 方法是 ViewGroup 中用于控制子 View 位置的方法。放置子 View 位置的过程很简单,只需重写 onLayout 方法,然后获取子 View 的实例,调用子 View 的 layout 方法实现布局。在实际开发中,一般要配合 onMeasure 测量方法一起使用。在下文「综合演练 —— 自定义 Layout」中会详细演示。

综合演练

简单改写已有 View 的尺寸实现方形 ImageView

  • 首先来证明一下改写 layout 方法会存在的问题
/**
 * 自定义正方形 ImageView
 *
 * Created by im_dsd on 2019-08-24
 */
public class SquareImageView extends android.support.v7.widget.AppCompatImageView {

    public SquareImageView(Context context) {
        super(context);
    }

    public SquareImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public SquareImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

   @Override
    public void layout(int l, int t, int r, int b) {
        // 使用宽高的最大值设置边长
        int width = r - l;
        int height = b - t;
        int size = Math.max(width, height);
        super.layout(l, t, l + size, t + size);
    }
}
复制代码

代码很简单,获取宽与高的最大值用于设置正方形 View 的边长。再看一下布局文件的设置

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    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"
    android:orientation="horizontal"
    tools:context=".MainActivity">


    <com.example.dsd.demo.ui.custom.measure.SquareImageView
        android:background="@color/colorAccent"
        android:layout_width="200dp"
        android:layout_height="300dp"/>

    <View
        android:background="@android:color/holo_blue_bright"
        android:layout_width="200dp"
        android:layout_height="200dp"/>
</LinearLayout>
复制代码

通过布局文件的描述如果是普通的 View 显示的状态应该是这样的

\[外链图片转存失败(img-aPRLST3i-1566741015611)(assets/image-20190824182806710.png)\]

而我们期待的状态应该是这样的:SquareImageView 的宽高均为 300dp。

\[外链图片转存失败(img-kRU6VS9V-1566741015612)(assets/image-20190824184207686.png)\]

但是最终的结果却是下图,虽然我们使用了 LinearLayout 但是我们通过layout() 方法改变了 SquareImageView 的大小,对于这个变化LinearLayout 并不知道,所以会发生布局重叠的问题。可见一般情况下不要使用 layout()方法

\[外链图片转存失败(img-2AwQ6d3I-1566741015612)(assets/image-20190824183744689.png)\]

  • 通过 onMeasure 方法更改尺寸。
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // super.onMeasure 中已经完成了 View 的测量
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 获取测量的结果比较后得出最大值
        int height = getMeasuredHeight();
        int width = getMeasuredWidth();
        int size = Math.max(width, height);
        // 将结果设置回去
        setMeasuredDimension(size, size);
    }

复制代码

总结

简单来说,更改已有 View 的尺寸主要分为以下步骤

  1. 重写 onMeasure()
  2. getMeasureWidthgetMeasureHeight()获取测量尺寸
  3. 计算最终要的尺寸
  4. setMeasuredDimension(width, height)把结果保存

完全自定义 View 的尺寸

此处用绘制圆形的 CircleView 做一个例子。对于这个 View 的期望是:View 的大小有内部的圆决定。

\[外链图片转存失败(img-ANAQF9RH-1566741015613)(assets/image-20190824191402384.png)\]

首先画一个圆形看看

/**
 * 自定义 View 简单测量
 * Created by im_dsd on 2019-08-15
 */
public class CircleView extends View {
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    /**
     * 为了方便简单,固定尺寸
     */
    private static final float PADDING = DisplayUtils.dp2px(20);
    private static final float RADIUS = DisplayUtils.dp2px(80);

    public CircleView(Context context) {
        super(context);
    }

    public CircleView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public CircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPaint.setColor(Color.RED);
        canvas.drawCircle(PADDING + RADIUS, PADDING + RADIUS, RADIUS, mPaint);
    }
}
复制代码
    <com.example.dsd.demo.ui.custom.layout.CircleView
        android:background="@android:color/background_dark"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
复制代码

此时将大小设置为 wrap_content 包裹布局,结果会是怎么样的呢?

\[外链图片转存失败(img-KFaFriMr-1566741015613)(assets/image-20190824192535840.png)\]
竟然填充了屏幕!根本就没有包裹内容,此时就需要我们大展身手了

  @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 没有必要再让 view 自己测量一遍了,浪费资源
        // super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        
        // 计算期望的 size
        int size = (int) ((PADDING + RADIUS) * 2);
        // 获取父 View 传递来的可用大小
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);

        // 开始计算
        int result = 0;
        switch (widthMode) {
            // 不超过
            case MeasureSpec.AT_MOST:
                // 在 AT_MOST 模式下,取二者的最小值
                if (widthSize < size) {
                    result = widthSize;
                } else {
                    result = size;
                }
                break;
            // 精准的
            case MeasureSpec.EXACTLY:
                // 父 View 给多少用多少
                result = widthSize;
                break;
            // 无限大,没有指定大小
            case MeasureSpec.UNSPECIFIED:
                // 使用计算出的大小
                result = size;
                break;
            default:
                result = 0;
                break;
        }
        // 设置大小
        setMeasuredDimension(result, result);
    }
复制代码

\[外链图片转存失败(img-0xlvjzrm-1566741015614)(assets/image-20190824193549345.png)\]

上面的代码就是 onMeasure(int,int) 的模板代码了,要注意一点的是需要注释 super.onMeasure 方法,此处面试的时候普遍会问。

 // 没有必要再让 view 自己测量一遍了,浪费资源
 // super.onMeasure(widthMeasureSpec, heightMeasureSpec);
复制代码

这段模版代码其实 Android SDK 里面早就有了很好的封装 : resolveSize(int size, int measureSpec)resolveSizeAndState(int size, int measureSpec, int childMeasuredState) ,两行代码直接搞定。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 没有必要再让 view 自己测量一遍了,浪费资源
        // super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        
        // 计算期望的 size
        int size = (int) ((PADDING + RADIUS) * 2);
        // 指定期望的 size
        int width = resolveSize(size, widthMeasureSpec);
        int height = resolveSize(size, heightMeasureSpec);
        // 设置大小
        setMeasuredDimension(width, height);
    }

复制代码

使用的时候完全可以这样做,但是非常建议大家都自己手写几遍理解其中的含义,因为面试会问到其中的细节。

还有一点很遗憾,就是 resolveSizeAndState(int, int, int) 不好用。不好用的原因不是方法有问题,而是很多自定义 View 包括原生的 View 都没有使用 resolveSizeAndState(int, int, int) 方法,或者没用指定 sate (state 传递父 View 对于子 View 的期望,相比resolveSize(int, in) 方法对于子 View 的控制更好)所以就算设置了,也不会起作用。

总结

完全自定义 View 的尺寸主要分为以下步骤:

  1. 重写 onMeasure()
  2. 计算自己期望的尺寸
  3. resolveSize() 或者 resolveSizeAndState()修正结果
  4. setMeasuredDimension(width, height)保存结果

自定义 Layout

源码地址

以 TagLayout 为例一步一步实现一个自定义 Layout。具体期望的效果如下图:

\[外链图片转存失败(img-Iz6Sy7Gd-1566741015614)(assets/image-20190824202927270.png)\]

重写 onLayout()

在继承 ViewGroup 的时候 onLayout() 是必须要实现的,这意味着子 View 的位置摆放的规则,全部交由开发者定义。

/**
 * 自定义 Layout Demo
 *
 * Created by im_dsd on 2019-08-11
 */
public class TagLayout extends ViewGroup {

    public TagLayout(Context context) {
        super(context);
    }

    public TagLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public TagLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            // 此时所有的子 View 都和 TagLayout 一样大
            child.layout(l, t, r, b);
        }
    }
}

复制代码

实验一下是否和期望的效果一样呢

<?xml version="1.0" encoding="utf-8"?>
<com.example.dsd.demo.ui.custom.layout.TagLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="5dp"
        android:padding="5dp"
        android:background="#ffee00"
        android:textSize="16sp"
        android:textStyle="bold"
        android:text="音乐" />

</com.example.dsd.demo.ui.custom.layout.TagLayout>
复制代码

\[外链图片转存失败(img-9grUdnPa-1566741015615)(assets/image-20190824203849801.png)\]

的确和期望一致。如果想要 TextView 显示为 TagLayout 的四分之一呢?

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            // 子 View 显示为 TagLayout1/4
            child.layout(l, t, r / 4, b / 4);
        }
    }
复制代码

效果达成!!!很明显onLayout可以非常灵活的控制 View 的位置

\[外链图片转存失败(img-i9K9gBGp-1566741015616)(assets/image-20190824204034040.png)\]

再尝试让两个 View 呈对角线布局呢?

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            if (i == 0 ){
                child.layout(0, 0, (r - l) / 2, (b - t)  / 2);
            } else {
                child.layout((r - l) / 2, (b - t)  / 2, (r - l), (b - t));
            }
        }
    }
复制代码

\[外链图片转存失败(img-L7XEOX1q-1566741015616)(assets/image-20190824204701652.png)\]

onLayout的方法还是很简单的,但是在真正布局中怎么获取 View 的位置才是难点!如何获取呢,这时候就需要 onMeasure 的帮助了!

计算

在写具体的代码之前,先来搭建大体的框架。主要的思路就是在 onMeasure()方法中计算好子 View 的尺寸和位置信息包括 TagLayout 的具体尺寸,然后在onLayout()方法中摆放子 View。

在计算过程中涉及到三个难点,具体请看注释

private List<Rect> mChildRectList = new ArrayList<>();

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 没有必要让 View 自己算了,浪费资源。 
        // super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            // 难点1: 计算出对于每个子 View 的尺寸
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            // 难点2:计算出每一个子 View 的位置并保存。
            Rect rect = new Rect(?, ?, ?, ?);
            mChildRectList.add(rect);
        }
        // 难点3:根据所有子 View 的尺寸计算出 TagLayout 的尺寸
        int measureWidth = ?;
        int measureHeight = ?;
        setMeasuredDimension(measureWidth, measureHeight);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (mChildRectList.size() == 0) {
            return;
        }
        for (int i = 0; i < getChildCount(); i++) {
            if (mChildRectList.size() <= i) {
                return;
            }
            View child = getChildAt(i);
            // 通过保存好的位置,设置子 View
            Rect rect = mChildRectList.get(i);
            child.layout(rect.left, rect.top, rect.right, rect.bottom);
        }
    }
复制代码
难点1 :如何计算子 View 的尺寸。

主要涉及两点:开发者对于子 View 的尺寸设置和父 View 的具体可用空间。获取开发者对于子 View 尺寸的设置就比较简单了:

// 获取开发者对于子 View 尺寸的设置
LayoutParams layoutParams = child.getLayoutParams();
int width = layoutParams.width;
int height = layoutParams.height;
复制代码

获取父 View (TagLayout) 的可用空间要结合两点:

  1. TagLayout 的父 View 对于他的尺寸限制
  2. TagLayout 的剩余空间。我们用 width 为例用伪代码简单分析一下如何计算子 View 的尺寸
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
// TagLayout 已经使用过的空间,此处的计算是个难点,此处不是本例子重点,一会儿讨论
int widthUseSize = 0;
for (int i = 0; i < getChildCount(); i++) {
	View child = getChildAt(i);
  // 获取开发者对于子 View 尺寸的设置
  LayoutParams layoutParams = child.getLayoutParams();
  int childWidthMode;
  int childWidthSize;
  // 获取父 View 具体的可用空间
  switch (layoutParams.width) {
  // 如果说子 View 被开发者设置为 match_parent
  	case LayoutParams.MATCH_PARENT:
    	switch (widthMode) {
      	case MeasureSpec.EXACTLY:
        // TagLayout 为 EXACTLY 模式下,子 View 可以填充的部位就是 TagLayout 的可用空间
        case MeasureSpec.AT_MOST:
        // TagLayout 为 AT_MOST 模式下有一个最大可用空间,子 View 要是想 match_parent 其实是和 
        // EXACTLY 模式一样的
        childWidthMode = MeasureSpec.EXACTLY;
        childWidthSize = widthSize - widthUseSize;
        break;
        case MeasureSpec.UNSPECIFIED:
        // 当 TagLayout 为 UNSPECIFIED 不限制尺寸的时候,意味着可用空间无限大!空间无限大还想
        // match_parent 二者完全是悖论,所以我们也要将子 View 的 mode 指定为 UNSPECIFIED
        childWidthMode = MeasureSpec.UNSPECIFIED;
        // 此时 size 已经没有作用了,写 0 就可以了
        childWidthSize = 0;
        break;
        }
      case LayoutParams.WRAP_CONTENT:
       break;
      default:
      // 具体设置的尺寸
      break;
}
// 获取 measureSpec
int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, childWidthMode);
复制代码

补充一下什么时候会是 UNSPECIFIED 模式呢?比如说横向或纵向滑动的 ScrollView,他的宽度或者高度的模式就是 UNSPECIFIED

伪代码仅仅模拟了开发者将子 View 的 size 设置为 match_parent 的情况,其他的情况读者要是感兴趣可以自己分析一下。笔者就不做过多的分析了!因为 Android SDK 早就为我们提供好了可用的 API: measureChildWithMargins(int, int, int, int)一句话就完成了对于子 View 的测量。

难点2:计算出每一个子 View 的位置并保存。
难点3:根据所有子 View 的尺寸计算出 TagLayout 的尺寸

有了 measureChildWithMargins 方法,对于子 View 的测量就很简单啦。 一口气解决难点 2 3。

  @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int lineHeightUsed = 0;
        int lineWidthUsed = 0;
        int widthUsed = 0;
        int heightUsed = 0;
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            // 测量子 View 尺寸。TagLayout 的子 view 是可以换行的,所以设置 widthUsed 参数为 0
            // 让子 View 的尺寸不会受到挤压。
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, heightUsed);
            if (widthMode != MeasureSpec.UNSPECIFIED && lineWidthUsed + child.getMeasuredWidth() > widthSize) {
                // 需要换行了
                lineWidthUsed = 0;
                heightUsed += lineHeightUsed;
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, heightUsed);
            }
            Rect childBound;
            if (mChildRectList.size() >= i) {
                // 不存在则创建
                childBound = new Rect();
                mChildRectList.add(childBound);
            } else {
                childBound = mChildRectList.get(i);
            }
            // 存储 child 位置信息
            childBound.set(lineWidthUsed, heightUsed, lineWidthUsed + child.getMeasuredWidth(),
                            heightUsed + child.getMeasuredHeight());
            // 更新位置信息
            lineWidthUsed += child.getMeasuredWidth();
            // 获取一行中最大的尺寸
            lineHeightUsed = Math.max(lineHeightUsed, child.getMeasuredHeight());
            widthUsed = Math.max(lineWidthUsed, widthUsed);
        }

        // 使用的宽度和高度就是 TagLayout 的宽高啦
        heightUsed += lineHeightUsed;
        setMeasuredDimension(widthUsed, heightUsed);
    }
复制代码

终于写完代码啦,运行起来瞧瞧看。

\[外链图片转存失败(img-4b1o6kXK-1566741015617)(assets/image-20190825204518467.png)\]

竟然奔溃了!通过日志可以定位到是

  // 对于子 View 的测量
  measureChildWithMargins(child, widthMeasureSpec, widthUsed, 
                                                          heightMeasureSpec, heightUsed);
复制代码

这一句出了问题,通过源码得知measureChildWithMargins方法会有一个类型转换导致了崩溃

protected void measureChildWithMargins(int, int ,int, int) {
	final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
	………………
}
复制代码

解决办法就是在 TagLayout 中重写方法 generateLayoutParams(AttributeSet) 返回一个 MarginLayoutParams 就可以解决问题了。

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }
复制代码

再次运行达到最终目标!

\[外链图片转存失败(img-Z60DY2Gw-1566741015617)(assets/image-20190825214300636.png)\]

总结

自定义 Layout 的主要步骤分为以下几点:

  1. 重写 onMeasure()
    • 遍历每一个子 View,用 measureChildWidthMargins() 测量 View
      • MarginLayoutParams 和 generateLayoutParams()
      • 有些子 View 可能需要多次测量
      • 测量完成后,得出子 View 的实际尺寸和位置,并暂时保存
    • 测量出所有子 View 的位置和尺寸后,计算出自己的尺寸,并用setMeasuredDimension(width, height)保存
  2. 重写 onLayout()
    • 遍历每个子 View,调用它们的 layout() 方法来将位置和尺寸传递给它们。

getMeasureWidth 与 getWidth 的区别

getMeasureXX 代表的是 onMeasure 方法结束后(准确的说应该是测量结束后)测量的值,而 getXX 代表的是 layout 阶段 right - left、bottom - top 的真实显示值,所以第一个不同点就是赋值的阶段不同,可见 getXXX 在 layout() 之前一直为 0, 而 getMeasureXX 可能不是最终值( onMeasure 可能会被调用多次),但是最终的时候二者的数值都会是相同的。使用那个还需要看具体的场景。

总结: getMeasureXX 获取的是临时的值,而 getXX 获取的时候最终定稿的值,一般在绘制阶段、触摸反馈阶段使用 getXXX,在 onMeasure 阶段被迫使用 getMeasureXX 。

本文所有源码地址

我的 Android 知识体系,欢迎 Star https://github.com/daishengda2018/AndroidKnowledgeSystem

文章分类
Android
文章标签