自定义View

758 阅读5分钟

判断自己有没有掌握这个知识点,就模拟面试,看看你能不能给对方讲清楚

1. 坐标系

在Android坐标系中,以屏幕左上角作为原点,这个原点向右是X轴的正轴,向下是Y轴正轴。如下所示:

image.png

除了Android坐标系,还存在View坐标系,View坐标系内部关系如图所示。

image.png

2. 自定义属性

Android系统的控件以android开头的都是系统自带的属性。为了方便配置自定义View的属性,我们也可以自定义属性值。
Android自定义属性可分为以下几步:

  1. 自定义一个View
  2. 编写values/attrs.xml,在其中编写styleable和item等标签元素
  3. 在布局文件中View使用自定义的属性(注意namespace)
  4. 在View的构造方法中通过TypedArray获取

自定义View属性很重要,但是并不复杂,需要的话再查一下就好了

3. View绘制流程

View的绘制基本由measure()、layout()、draw()这个三个函数完成

函数作用相关方法
measure()测量View的宽高measure(),setMeasuredDimension(),onMeasure()
layout()计算当前View以及子View的位置layout(),onLayout(),setFrame()
draw()视图的绘制工作draw(),onDraw()

3.1 MeasureSpec

MeasureSpec是View的内部类,它封装了一个View的尺寸,在onMeasure()当中会根据这个MeasureSpec的值来确定View的宽高。

MeasureSpec的值保存在一个int值当中。一个int值有32位,前两位表示模式mode后30位表示大小size。即MeasureSpecmodesize

MeasureSpec当中一共存在三种modeUNSPECIFIEDEXACTLY
AT_MOST

对于View来说,MeasureSpec的mode和Size有如下意义

模式意义对应
EXACTLY精准模式,View需要一个精确值,这个值即为MeasureSpec当中的Sizematch_parent
AT_MOST最大模式,View的尺寸有一个最大值,View不可以超过MeasureSpec当中的Size值wrap_content
UNSPECIFIED无限制,View对尺寸没有任何限制,View设置为多大就应当为多大一般系统内部使用

3.2 Layout()

layout()过程,对于View来说用来计算View的位置参数,对于ViewGroup来说,除了要测量自身位置,还需要测量子View的位置。

3.3 Draw()

draw流程也就是的View绘制到屏幕上的过程,整个流程的入口在Viewdraw()方法之中,而源码注释也写的很明白,整个过程可以分为6个步骤。

  1. 如果需要,绘制背景。
  2. 有过有必要,保存当前canvas。
  3. 绘制View的内容。
  4. 绘制子View。
  5. 如果有必要,绘制边缘、阴影等效果。
  6. 绘制装饰,如滚动条等等。

View的流程图表示:

image.png

ViewGroup的流程图表示:

image.png

4. 布局过程的自定义:

方式: 重写布局过程的相关方法\

1. 测量过程: onMeasure()
2. 布局过程: onLayout()

具体:

1. 重写onMeasure()来修改已有的View的尺寸
2. 重写onMeasure()来全新计算自定义View的尺寸
3. 重写onMeasure()和onLayout()来全新计算自定义 ViewGroup 的内部布局

4.1 重写onMeasure()来修改已有的View的尺寸

public class SquareImageView extends AppCompatImageView {
    private static final String TAG = "SquareImageView";

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

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



    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 先执行原测量算法
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        // 获取原先的测量结果
        int measureWidth = getMeasuredWidth();
        int measureHeight = getMeasuredHeight();
        Log.d(TAG, "onMeasure11" +
                ", measureWidth = " + measureWidth +
                ", measureHeight = " + measureHeight +
                "");

        // 利用原先的测量结果计算出新尺寸
        if (measureWidth > measureHeight) {
            measureWidth = measureHeight;
        } else {
            measureHeight = measureWidth;
        }
        Log.d(TAG, "onMeasure22" +
                ", measureWidth = " + measureWidth +
                ", measureHeight = " + measureHeight +
                "");
        // 保存计算后的结果
        setMeasuredDimension(measureWidth, measureHeight);
    }
}

重写onMeasure() 修改尺寸

1. 重写 onMeasure() 修改尺寸,并调用super.onMeasure触发原先的测量
2. 用getMeasuredWidth() 和 getMeasuredHeight() 取到之前测得的尺寸,利用这两个尺寸来计算出最终尺寸。
3. 使用 setMeasuredDimension() 保存尺寸

4.2 重写onMeasure()来全新计算自定义View的尺寸

public class SquareImageView extends AppCompatImageView {
    private static final String TAG = "SquareImageView";

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

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 不用写super.onMeasure() 需要自己计算尺寸
//        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        // 设置尺寸
        int measureWidth = 100;
        int measureHeight = 100;

        // 矫正尺寸
        measureWidth = resolveSize(measureWidth, widthMeasureSpec);
        measureHeight = resolveSize(measureHeight, heightMeasureSpec);

        // 保存计算后的结果
        setMeasuredDimension(measureWidth, measureHeight);
    }

    // 注:下面这段代码已经过简化,不是resolveSize()的源码
    public static int resolveSize(int size, int measureSpec) {
        final int specMode = MeasureSpec.getMode(measureSpec);
        final int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
            case MeasureSpec.UNSPECIFIED:
                return size;
            case MeasureSpec.AT_MOST:
                if (size <= specSize) {
                    return size;
                } else {
                    return specSize;
                }
            case MeasureSpec.EXACTLY:
                    return specSize;
            default:
                return size;
        }
    }
}

4.3 重写onMeasure()和onLayout()来全新计算自定义 ViewGroup 的内部布局

4.3.1 可用空间 的判断方法\

根据自己的 MeasureSpec 中 mode 的不同:

  1. EXACTLY/AT_MOST:
    可用空间:MeasureSpec 中的 size
  2. UNSPECIFIED:
    可用空间:无限大

4.3.2 onMeasure() 的重写

  1. 调用每个子 View 的 measure(), 让子View 自我测量
  2. 根据子 View 给出的尺寸,得出子View的位置,并保存他们的位置和尺寸
  3. 根据子 View 的位置和尺寸计算出自己的尺寸,并用 setMeasureDimension() 保存

4.3.3 关于保存子 View 位置的两点说明

  1. 不是所有的Layout都需要保存子View的位置(因为有的Layout可以在布局阶段实时推导出子View的位置,例如LinearLayout)

  2. 有时候对某些子 View 需要重复测量两次或多次才能得到正确的尺寸和位置

4.3.4 Layout 内部布局的自定义

  1. 重写onMeasure() 来计算内部布局
  2. 重写onLayout() 来摆放子 View
public class SomeLayout extends ViewGroup {
    private static final String TAG = "SomeLayout";

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

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        for (int i = 0; i < getChildCount(); i++) {
            View childView = getChildAt(i);
            int childWidthSpec;
            int usedWidth = 0;

            LayoutParams lp = childView.getLayoutParams();
            // lp.width: 对应 layout_width
            // lp.height: 对应 layout_height
            /**
             * wrap_content => WRAP_CONTENT
             * match_parent => MATCH_PARENT
             * xxdp / xxsp => 具体的像素值
             *
             */
            int selfWidthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
            int selfWidthSpecSize = MeasureSpec.getSize(widthMeasureSpec);

            switch (lp.width) {
                case MATCH_PARENT:
                    if (selfWidthSpecMode == EXACTLY || selfWidthSpecMode == AT_MOST) {
                        childWidthSpec = MeasureSpec.makeMeasureSpec(selfWidthSpecSize - usedWidth, EXACTLY);
                    } else {
                        childWidthSpec = MeasureSpec.makeMeasureSpec(0, UNSPECIFIED);
                    }
                    break;
                case WRAP_CONTENT:
                    if (selfWidthSpecMode == EXACTLY || selfWidthSpecMode == AT_MOST) {
                        childWidthSpec = MeasureSpec.makeMeasureSpec(selfWidthSpecSize - usedWidth, AT_MOST);
                    } else {
                        childWidthSpec = MeasureSpec.makeMeasureSpec(0, UNSPECIFIED);
                    }
                    break;
                default:
                    childWidthSpec = MeasureSpec.makeMeasureSpec(lp.width, EXACTLY);
                    break;
            }

        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        for (int i = 0; i < getChildCount(); i++) {
            View childView = getChildAt(i);
            childView.layout(childLeft[i], childTop[i], childRight[i], childBottom[i]);
        }
    }
}

4.3.5 onMeasure onSizeChanged onLayout 和 onDraw 执行顺序

打印结果如下:可以看出onMeasure执行了2次,onSizeChanged在onMeasure之后,onDraw最后执行

2021-09-02 17:21:19.842 3685-3685/com.example.javademo D/SquareImageView: onMeasure
2021-09-02 17:21:19.865 3685-3685/com.example.javademo D/SquareImageView: onMeasure
2021-09-02 17:21:19.873 3685-3685/com.example.javademo D/SquareImageView: onSizeChanged
2021-09-02 17:21:19.874 3685-3685/com.example.javademo D/SquareImageView: onLayout
2021-09-02 17:21:19.916 3685-3685/com.example.javademo D/SquareImageView: onDraw

5. get到的新知识点

像View中layout_width layout_height以layout_开头的,都是给父view看的,其他的属性才是给自己看的

像RleativeLayout LinearLayout这种以layout结尾的是ViewGroup 像ImageView和TextView以View结尾的是View

未命名文件.png

参考:Android自定义View全解