Android-自定义View

1,062 阅读12分钟
做好自己的本职工作,学会独立的思考,在茫茫人海中寻找真实的自我,不要过多的关注外界的声音,不断的学习和充实自己,一切都会变好的。

Tip:最开始的时候,我对自定义View是茫然的,根本无从入手,特别是刚实习的时候,特别害怕分给我自定义View相关的活。我这里与其说是介绍,到不如说我学习的历程和总结,希望大家都可以学会。首先这里我介绍一些我看过的比较好的博客和内容,方便大家学习。

前言:这里我不会教大家怎么绘制,怎么draw,以及canvas的各种骚操作,我只会教大家measure,layout,draw的关系,如何测量,如何放置以及如何绘制View,以及自定义View我们需要需要关注的是什么?有人会问,你可以什么都自定义出来吗?我会很直接的回答你,我不会,但是我可以学会如何去抄,大家只需要知道原理,看懂别人的代码,自定义自己的东西就会变得很简单了。

自定义View分类

  • 直接继承View或者ViewGroup:
    • 这类的难度会大一些,这个View的measure,layout(如果是ViewGroup的话)以及draw全部都是由自己控制,但是好处也是很明显的,灵活性会很高,一般开发中用的真的不是很多,用的话,大都去GitHub找一个改改就行。
  • 继承已有的控件(比如TextView,FrameLayout)
    • 这种半成品比较常见,比如我们经常会继承LineaLayouut或者FrameLayout等,因为这个View的measure,layout(如果是ViewGroup的话),以及draw都不需要我们自己控制,它们都已经实现了,保持原有属性并扩展其能力。我们经常用父类是ViewGroup居多,我们常在里面放置一些View,提供给一些方法,方便使用以及其他同事复用。
  • 组合已有的View控件。
    • 把已有的控件按照布局,合成一个,供外界使用。

上面的第二种和第三种常常组合在一起使用,比如继承FrameLayout,里面放置一些原生控件,包装成一个View,供外界调用。

自定义View的主要方法介绍

  • onMeasure
    • 适用于View和ViewGroup,这个方法顾名思义就是对View进行测量,用来决定当前View的大小,当这个方法执行完毕之后,我们通过getMeasuredWidth或者getMeasuredHeight就可以获得其宽高。当然这个大小只是测量的理论建议大小,一般来说是准确的,最终的大小还需要通过onLayout来确定。(ViewGroup的onMeasure其实是对各个子View进行测量,只有子View都测量好了,那么这个ViewGroup也就测量好了,很好理解,ViewGroup的测量实际也就是View的测量)
  • onLayout
    • 这个是ViewGroup需要重写的方法,用来放置子View,当ViewGroup测量完毕之后,(实际也就是其子View的测量)我们也就知道了这个ViewGroup的大小,占据的空间,接下来我们就可以放置其子View的位置,这个方法就是来实现安放的。这个时候我们可以getWidth或者getHeight来获取宽高,一般来说是和getMeasuredWidth相等的,也是一般来说,为什么呢,后面也会介绍的。
  • onDraw
    • 使用与ViewGroup和View,大家可以重写这个方法,绘制想要绘制的东西,后面我会侧重介绍一些绘制顺序对View的影响,比如前景绘制,背景绘制等等。

理解MeasureSpec

理解MeasureSpec是由SpecMode和SpecSize组成,前者表示的是测量模式,模式有三种,后者指的是当前模式下的大小尺寸,从组成也可以看出来,两者是紧密关联起来的,(举个栗子:我们常常在xml文件写View的大小的时候,通常会有指定大小20dp,或者match_parent这些可以理解为模式,具体显示到界面的区域也可以理解为大小)下面接介绍一下三种模式。

  • UNSPECIFIED 父容器对子View没有限制,要多大给多大,用的应该不多,我用的很少。

  • EXACTLY 精准测量,能够得到子View的具体大小,这个时候子View的SpecSize就是具体的大小,当子View为match_parent或者为具体的值的时候,就对应这种模式,有人会问,为啥match_parent也是精准测量呢?

  • AT_MOST 父View有一个可用的大小,子View不超过父View指定的这个大小,对应的是wrap_content,这个时候有人问,真的不能超过吗?如果不超过,难道上面的两种模式就能超过吗,我平时写具体的大小,我也没有看到子View显示能超过父View之外啊?区别是什么呢,你说的好抽象,我看不懂。

在解决这个问题之前,我们需要先理解MeasureSpec和LayoutParams的关系。

MeasureSpec和LayoutParams的关系
  • MeasureSpec是根据父View的约束以及自己的布局参数生成的,有以下两种情况,对于顶级的View,也就是我们说的DecorView是个更具window的尺寸和自身的布局参数决定,普通的View就是根据父View的MeasureSpec和自身的不就参数决定。

首先是Decorview,我们可以跟踪到ViewRootImpl的measureHierarchy,通过getRootMeasureSpec来获取MeasureSpec的,核心代码如下:

private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {
        // 当前布局参数是填充父布局,父布局的大小,就是windowSize,那么大小是可以确定的,是
        // exactly很好理解
        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        // 如果当前是包裹内容,很显然,他的大小是不超过父View的,具体大小还要看看View自身的测量,但是
        // 给当前View当前Wind的大小作为最大尺寸,对应的就是at_most,从这个most大家可以理解一下
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            // 当前的View是外界指定好的,明显这个View的大小就是确定的,所以也是exactly
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

接下来,我们看看,通常我们获取MeasureSpec的通用方式,里面的代码是怎么走的,看看getChildMeasureSpec这个方法,我们通常在自定义ViewGroup这类组件的时候,需要测量子View,要用这个方法。

        // 获取测量的模式
        int specMode = MeasureSpec.getMode(spec);
        // 获取父View的size
        int specSize = MeasureSpec.getSize(spec);

        // size是当前View可用的剩余控件,具体解释如下,1
        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            // 2
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            // 3
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            // 4
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        // 最后更具测量模式和可用大小,确定MeasureSpec
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

首先官方文档的解析(用我蹩脚的英语): 目的是给子View找到一个MeasureSpec,用来确定宽高。通过结合父ViewMeasureSpec和子View的LayoutParams力求得到最好的结果。举一个栗子,如果当前子View知道自己的款到(因为它的测量模式是exactly),或者说这个字View想和父View有一样的大小,那么父View就应该给子View确切的尺寸。

挺难理解的,大概意思就是给子View一个好的MeasureSpec来确定子View的大小,同时,尽可能的满足子View的需求,别委屈它😝

详细解释:

  • 1-这个size就是子View可用的剩余空间。首先方法的参数Spec,是调用方传递来的父View的MeasureSpec,这个padding,就是占用的空间(比如父View的Padding,Margin,或者子View的Margin),通过Spec获取的size,是父View的size,这个size-padding,就是剩余控件,也就是子View可用的最大空间。

  • 2-如果父View的specMode是exactly

    • childDimension >= 0,子View的大小是通过手动指定写死的。既然如此,子View需要这个大小,OK,那我就给你这个childDimension大小,那么你的尺寸也就定死了,是exactly。
    • childDimension == LayoutParams.MATCH_PARENT。子View需要充满父View,那么子View的大小就填满剩余所有的空间,=size,大小也是确定的,同样是exactly。
    • childDimension == LayoutParams.WRAP_CONTENT,子View想要自适应大小,但是最大也不能超过可使用的剩余空间size,所以就给子View最大空间size(可以这么理解,尽量满足子View的大小),测量模式就是at_most。
  • 3-如果父View的specMode是at_most(父View的大小也是可变的不确定的,但是希望子View不要比自己还大)

    • childDimension >= 0,同样,子View需要自己制定尺寸,写死了,那么就给你这么childDimension多。(不要疑惑加入size<childDimension怎么办,超出就显示不出来了啊,也是合理的啊,不要纠结这个)
    • childDimension == LayoutParams.MATCH_PARENT,父View对我们有了最大限制,子View想要充满,那么就让子View也是at_most不超过,不要比父View大,大小是size。
    • childDimension == LayoutParams.WRAP_CONTENT,同样父View尺寸不确定,子View想自己决定大小,那么就让子View依旧是at_most,不要超过父View的大小,大小也是size。
  • 4-如果父View是MeasureSpec.UNSPECIFIED (父View对孩子不做任何限制,要多大给多大)

    • childDimension >= 0,子View的大小是通过手动指定写死的,满足你的要求,给子View的大小是childDimension,依旧是exactly。
    • childDimension == LayoutParams.MATCH_PARENT。子View需要充满父View,父View不限制子View的大小,那么你也可以无限大,你的测量模式就是UNSPECIFIED。
    • childDimension == LayoutParams.WRAP_CONTENT,子View想要自适应大小,父View是无限大,不限制你的大小,那么你可以无限大,测量模式也是UNSPECIFIED。

小结:通过上面的解析,我们就可以获取View的宽高的MeasureSpec(是通过自己的布局参数和父View的约束MeasureSpec决定的),就可以对子View进行重新测量,获取View的宽高。

理解核心的三个方法

onMeasure

首先我们看看默认View这个类的onMeasure方法(ViewGroup就不分析了,简单理解就是通过for循环测量每一个子View,然后把尺寸求和,核心还是需要关注View的Measure方法,后面我会有一个实战来分析,可以先忽略)

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

setMeasuredDimension:这个方法是把计算出来的目标尺寸设置给View。

public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

这个方法才是我们更加关注的,提供了View的默认测量方式,接下来分析子View的测量模式。

  • UNSPECIFIED 子View是不确定的,还是无限制的,就获取默认大小,size。(不要问我为什么,源码是这么写的,为什么不获取specSize,我怎么知道。)这个UNSPECIFIED一般我们用不到,更何况还是View级别的,可以不用特别关注。
  • AT_MOST,EXACTLY 子View都是获取MeasureSpec的size,这个是推荐的理论测量值,应为子View的MeasureSpec是根据父View的MeasureSpec和自己的布局参数决定的,一般都是用这个值,同时也是比较准确的。

小结(参照上面的方法分析):

当View是exactly的时候,有以下几种情况 父View任意模式+子View确定Dimension 父View是exactly+子View是match_parent 这个时候都是OK的,表现结果是符合预期的

如果当前View是at_most,同样有下面情况 父View是at_most,子View是match_parent,wrap_content 父View是exactly,子View是wrap_content 都会导致子View是at_most,会充满父View

如果想要让自定义的能够实现自己控制自己的尺寸,不充满父布局(前提你不希望充满,如果你定义match_parent当然是符合你的预期的),你就需要自己重写onMeasure来确定自己的尺寸大小。通过setDimension来最终设置自己的大小。

onLayout

这个是ViewGroup的方法,用来安放测量好的子View,通过View.layout方法来放置View,很简单不做过多说明,这里就说一下,为什么onMeasure之后获取的getMeasureWidth和onLayout之后获取的getWidth有什么区别?

正常情况下,两者是一样大小的,因为你测量好了大小measuredWidth,你就直接按照测量好的大小直接放置就行,放置OK之后,你的getWidth就是right-left,所以两者是相等的,你是按照测量宽度来摆放的,但是如果你有什么特殊需求,你的right或者left有改变,就会导致getWidth!=measuredWidth。

onMeasure

这个方法是View和ViewGroup共有的,目的就是绘制你需要的东西(这里我不会讲解canvas的具体使用方式),我给大家说一下,不同的绘制顺序会有不同的效果。

  • 在super.onDraw() 的下方插入绘制代码,让绘制内容盖住原主体内容
  • 把下面的绘制代码移到 super.onDraw() 的上面,就可以让原主体内容盖住你的绘制代码了
  • 在这里插入 setWillNotDraw(false) 以启用完整的绘制流程
  • 把onDraw()换成dispatchDraw(),让绘制内容可以盖住子 View,另外,在改完之后,上面的 setWillNotDraw(false) 也可以删了
  • 在super.onDrawForeground()的下方插入绘制代码,让绘制内容盖住前景
  • 在super.onDrawForeground() 的上方插入绘制代码,让绘制内容被前景盖住

总结

这里,基本就讲完了,我关注的是核心流程,怎么去绘制和测量,需要大家去理解去尝试,更多关注的就是测量,各种MeasureSpec怎么生成的,和LayoutParams关系是怎么样的,什么时候需要自己去定义宽高就行,了解这些也就足够了,下面我会有一个demo以及源码讲解。