Android进阶——自定义View之View的绘制流程及实现onMeasure完全攻略

引言

Android实际项目开发中,自定义View不可或缺,而作为自定义View的一种重要实现方式——继承View重绘尤其重要,前面很多文章基本总结了继承View的基本流程:自定义属性和继承View重写onDraw方法——>实现构造方法并完成相关初始化操作——>重写onMeasure方法——>onSizeChanged()拿到view的宽高等数据——>重写onLayout————>重写onTouch实现交互——>定义交互回调接口,但是由于当时的具体的业务需求并没有详解总结下关于onMeasure和onLayout方法,相信很多初学者都是处于知其然而不知其所以然,这篇文章就专门总结下。

一、View的系统架构

虽然前面已经总结过了,但是这在里还是重申下,加深印象。总所周知,在Android中每一个控件都会再界面中占据一块矩形的区域,这和大多数系统的控件机制都差不多。Android中控件是通过构造树的形式来管理的(所谓控件树如下图所示),主要分为View和ViewGroup两大类,其中ViewGroup直接继承自View,View作为系统所有可视组件的基类,而通过控件树,上层控件负责下层子控件的测量与绘制(即先执行onMeasure——>onLayout——>onDraw的),并负责分发交互事件的即事件是先传递到ViewGroup的,再由ViewGroup决定是否传递给下层子View。而这颗树的根节点ViewParent(其实质是一个接口定义了一系列管理View的方法)对于该控件树所有的交互事件惊喜统一管理和分发,从而实现对整个树进行整体控制。

二、View、ViewGroup的测量和绘制概述

Android中的GUI系统是客户端和服务端配合的窗口系统,即后台运行了一个绘制服务,每个应用程序都是该服务端的一个客户端,当客户端需要绘制时,首先请求服务端创建一个窗口,然后在窗口中进行具体的视图内容绘制;对于每个客户端而言,他们都感觉自己独占了屏幕,而对于服务端而言,它会给每一个客户端窗口分配不同的层值,并根据用户的交互情况动态改变窗口的层值,这就给用户造成了所谓的前台窗口和后台窗口的概念。当然这是屏幕绘制的原理简要描述,绘制离不开测量,无论是系统控件和自定义的View要想展示于界面之上都离不开测量工作。简而言之,当Activity获得焦点时,Activity将被通知要求绘制自己的布局,从而Android framework接到Activity的消息将会处理绘制过程,而Activity只需提供它的布局的根节点即可。绘制过程是从布局的根节点开始,从根节点开始测量和绘制整个View tree。每一个父级ViewGroup 负责要求它的每一个孩子被绘制,每一个子View负责绘制自己。因为整个View tree是按顺序遍历的,所以父节点会先被绘制,而兄弟节点会按照它们在树中出现的顺序被绘制。完整的绘制是包含两个过程:测量Measure 和布局Layout。测量过程(measuring pass)是在measure(int, int)中实现的,是从树的顶端由上到下进行的(top-down)。在这个递归过程中,每一个View会把自己的dimension specifications传递下去。在测量Measure 完成之后,每一个View都存储好了自己的测量结果。再者就是是布局Layout,它发生在 layout(int, int, int, int)中,仍然是从上到下进行,每一个父级都会负责用测量过程中得到的尺寸,把自己的所有孩子放在正确的地方。

1、View的测量

由父级ViewGroup负责要求子级View进行测量和绘制。我们都知道每一个控件都会占据一个矩形区域,但是Android系统在绘制前本身并不知道具体的大小和位置,所以它会先进行测量,主要是在View的onMeasure里去实现(这也是我们在自定义View里的构造方法里,无论是调用getMeasureWidth抑或getWidth获取宽度时得到的总是0的原因),而Android中还有一个功能类MeasureSpec(封装了从父节点传递到子节点下的布局信息包括View的测量模式和大小)用于辅助测量View,当我们重写了onMeasure方法之后,系统通过super.onMeasure方法去调用setMeasuredDimension(width, height)将测量的大小设置进去完成测量

2、View的绘制

完成测量工作之后,View的根据ViewGroup传人的测量值和模式,对自己宽高进行确定(onMeasure中完成),然后在onDraw中在Canvas上完成对自己的绘制。

3、ViewGroup的测量

ViewGroup需要管理子View,所有其中一项重要的职责就是负责子View的大小,当ViewGroup大小设置为wrap_content,ViewGroup会对子View进行层级遍历,来决定自己的大小,而其他模式下则会取设置的值来为自己的大小。ViewGroup在测量时遍历所有子View,从调用子View对应的onMeasure方法获得子View的大小,完成测量之后再通过调用onLayout方法来决定子View的位置,同样是通过遍历调用子View的onLayout方法,最后在自己的onLayout中完成子View的位置布局工作。

4、ViewGroup的绘制

ViewGroup通常不需要绘制,因为它本身没有需要绘制的东西,所以不会触发自己的onDraw方法,但如果指定了background属性则会触发自身的onDraw完成背景的绘制。但ViewGroup会通过dispatchDraw方法来绘制其子View,原理也是一样通过遍历子View调用其子View对应的onDraw方法来完成最终的绘制工作。

三、View.MeasureSpec和ViewGroup.LayoutParams

1、View.MeasureSpec

public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        /**
          * UNSPECIFIED 模式:
          * 父View不对子View有任何限制,子View需要多大就多大
          */ 
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        /**
          * EXACTYLY 模式:
          * 父View已经测量出子Viwe所需要的精确大小,这时候View的最终大小
          * 就是SpecSize所指定的值。对应于match_parent和精确数值这两种模式
          */ 
        public static final int EXACTLY     = 1 << MODE_SHIFT;

        /**
          * AT_MOST 模式:
          * 子View的最终大小是父View指定的SpecSize值,并且子View的大小不能大于这个值,
          * 即对应wrap_content这种模式
          */ 
        public static final int AT_MOST     = 2 << MODE_SHIFT;

        //将size和mode打包成一个32位的int型数值
        //高2位表示SpecMode,测量模式,低30位表示SpecSize,某种测量模式下的规格大小
        public static int makeMeasureSpec(int size, int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }

        //将32位的MeasureSpec解包,返回SpecMode,测量模式
        public static int getMode(int measureSpec) {
            return (measureSpec & MODE_MASK);
        }

        //将32位的MeasureSpec解包,返回SpecSize,某种测量模式下的规格大小
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }
        //...
    }

View.MeasureSpec是View中的一个静态内部类,封装了从父级节点传递下来给子级节点的布局需求信息,每一个MeasureSpec体现的是子类的布局的尺寸大小size(包括宽度或高度)和模式mode的需求,但是并不是子级的实际尺寸就必须是父级要求的,我们可以通过重写onMeasure方法实现自己的规则,然后在子级中,而这里的模式来源于父ViewGroup去解析子View对应的在布局文件中layout_width和layout_height值来决定采用什么模式(至于怎么解析,这是后话),其中主要有三种模式:UNSPECIFIEDEXACTLYAT_MOST

  • UNSPECIFIED:说明父级没有对子级强加任何限制,子级可以是它想要的任何尺寸。用得比较少,表示子布局被限制在一个最大值内,一般当childView设置其宽、高为wrap_content时,ViewGroup会将其设置为AT_MOST,换言之,表示子布局想要多大就多大,一般出现在AadapterView的item的heightMode中、ScrollView的childView的heightMode中

  • EXACTLY:父级为子级决定了一个确切的尺寸,子级将会被强制赋予这些边界限制,不管子级自己想要多大(View类onMeasure方法中只支持EXACTLY),换言之,表示设置了精确的值,一般当childView设置其宽、高为精确值、match_parent时,ViewGroup会将其设置为EXACTLY,即在布局文件中可以解析指定的具体尺寸和match_parent,不支持wrap_content

  • AT_MOST:子级可以是自己指定的任意大小,但是有个上限。比如说当MeasureSpec.EXACTLY的父容器为子级决定了一个大小,子级大小只能在这个父容器限制的范围之内。即在布局文件中可以解析wrap_content,换言之,表示子布局被限制在一个最大值内,一般当childView设置其宽、高为wrap_content时,ViewGroup会将其设置为AT_MOST。

方法 说明
static int getMode(int measureSpec) 获取模式
static int getSize(int measureSpec) 获取尺寸
static int makeMeasureSpec(int size, int mode) 根据指定的模式和尺寸创建对应的测量规则

2、ViewGroup.LayoutParams

ViewGroup.LayoutParams直接继承于Object作为位置参数信息的父类,是View用来告诉它的父容器它想要怎样被放置的(包含高度、宽度、对齐方式、外边距、内边距等等),Android中的布局信息ViewGroup.LayoutParams家族来决定的,常见包括AbsListView.LayoutParams, AbsoluteLayout.LayoutParams, Gallery.LayoutParams, ViewGroup.MarginLayoutParams, ViewPager.LayoutParams, WindowManager.LayoutParams、ActionBar.LayoutParams, ActionMenuView.LayoutParams, AppBarLayout.LayoutParams, BaseCardView.LayoutParams, BoxInsetLayout.LayoutParams,CollapsingToolbarLayout.LayoutParams,CoordinatorLayout.LayoutParams,DrawerLayout.LayoutParams,FrameLayout.LayoutParams,GridLayout.LayoutParams, GridLayoutManager.LayoutParams, LinearLayout.LayoutParams, LinearLayoutCompat.LayoutParams,PercentFrameLayout.LayoutParamsRelativeLayout.LayoutParams等。不同的Layout提供了不同LayoutParams,它们共同承担起整个Android 的布局任务。

四、View中几大重要的方法的意义和作用

继承View/ViewGroup实现自定义View后,一般还需要复写最基本的二、三个方法:onMeasure()onSizeChanged()拿到view的宽高等数据、onLayout()onDraw()

  • onMeasure:用于本View宽高的测量,布局复杂时可能触发多次。ViewGroup的onMeasure则负责处理它children的测量工作。由于View默认的onMeasure()仅仅支持EXACTLY模式,也就是说如果不重写onMeasure()方法的话则无法在正确解析布局文件里的wrap_content,因为onMeasure()是Android提供给我们告诉系统自己定义的View的实际大小(是否是仅仅依赖于父级要求的,也就是说自主定义View大小的)的机会,同时也是提供了我们自定义的解析规则的方法(如果你愿意,你可以完全实现match_parent和wrap_content和具体值一样的效果),最终调用setMeasuredDimension(int ,int)完成最终的测量(因为onMeasure方法没有返回值,所以测量的结果应该通过setMeasuredDimension方法告知系统)。

  • onSizeChanged():可拿到view的宽高等数据信息

  • onLayout:常复写于viewGroup的自定义子类。它有负责对它内部所有children进行处理,告知childrenView的位置,以正确摆放。ViewGroup中onLayout是抽象方法必须复写,这是children位置能正确摆放的保证。依靠mLeft,mTop,mRight,mBottom这四个值,以坐上为原点,这四个值分别为对应边到原点的距离。最后和onMeasure一样,记得调用child.layout()方法。

  • onDraw:UI最终呈现的过程,用户使用Paint(What to draw)、Canvas(How to
    draw)两个类完成自定义画面。绘制的时候需要考虑下padding,与margin不同,padding是属于本View的属性,不同于margin(不需要自定义时做处理系统就能很好的使用margin),所以要在测量绘图时考虑它:

  • 测量时:desireSize=实际所需size+相应方向的padding。

  • 绘图时:考虑padding,做相应的位移。

五、onMeasure方法详解及实现

onMeasure方法是测量View及其内容的,决定measured width和measured height的,这个方法由 measure(int, int)方法唤起,子类可以重写onMeasure来提供更加准确和有效的测量。(以前有一个约定:在重写onMeasure方法的时候,必须调用 setMeasuredDimension(int,int)来存储这个View经过测量得到的measured width and height。否则,将会由measure(int, int)方法抛出一个IllegalStateException。)View类onMeasure方法中只支持EXACTLY,如果不重写onMeasure的话就只支持EXACTLY模式。

1、onMeasure方法签名

/**
*这两个参数都是按赵View.MeasureSpec类来进行编码的
*@param :widthMeasureSpec 父级提出的水平宽度要求
*@param :heightMeasureSpec 父级提出的垂直高度要求
*/
protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec)

2、实现onMeasure方法的步骤

  • 从父级传递过来的View.MeasureSpec对象里获取测量模式和尺寸

  • 然后根据不同的模式,给出不同的测量值,(即实际值)宽高都采用一样的机制,一般mode为EXACTLY时,直接使用父类传递过来的测量值specValue;mode为UNSPECIFIED时,直接指定为默认的大小(这个值需要我们自己定义);当mode为AT_MOST时也指定为默认的大小,但还需要我们拿指定的默认大小和测量值specValue比较取最小值。

  • 调用父类测量方法setMeasuredDimension(测量宽度值,测量高度值)

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        measure(widthMeasureSpec);
        measure(heightMeasureSpec);
        Log.e("onMeasure", "realWidth: " + realWidth + "realHeiht: " + realHeiht + "widthMeasureSpec" + widthMeasureSpec + "heightMeasureSpec" + heightMeasureSpec);
        setMeasuredDimension(realWidth, realHeiht);
    }

    private void measure(int measureValue) {
        int defalueSize = 200;
        int mode = View.MeasureSpec.getMode(measureValue);
        int specValue = View.MeasureSpec.getSize(measureValue);
        Log.e("onMeasure", "mode: " + mode + "specValue: " + specValue);
        switch (mode) {
            //指定一个默认值
            case MeasureSpec.UNSPECIFIED:
                realWidth = defalueSize;
                realHeiht = defalueSize;
                break;
            //取测量值
            case MeasureSpec.EXACTLY:
                realHeiht = specValue;
                realWidth = specValue;
                break;
            //取测量值和默认值中的最小值
            case MeasureSpec.AT_MOST:
                realWidth = Math.min(defalueSize, specValue);
                realHeiht = Math.min(defalueSize, specValue);
                break;
            default:
                break;
        }
    }

3、模仿谷歌官方写法实现onMeasure

这里主要就是模仿View.resolveSizeAndState(int size, int measureSpec, int childMeasuredState),childMeasuredState其中 View.getMeasuredState()是由返回的,最终布局将结合childMeasuredState通过View.combineMeasuredStates()完成最终的测量结果,作用应该是自定义viewGroup时才使用用于记录children测量状态的,一般自定义View传0即可,特殊情况下可以传递1。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //third param. usually 0. http://stackoverflow.com/questions/13650903/whats-the-utility-of-the-third-argument-of-view-resolvesizeandstate
        int w = resolveSizeAndState2(getDesireW(), widthMeasureSpec, 0);
        int h = resolveSizeAndState2(300, heightMeasureSpec, 0);
        setMeasuredDimension(MeasureSpec.getSize(w), MeasureSpec.getSize(h));
    }

    private int getDesireW(){
        return 300;
    }

    /**
     *
     * @param size  How big the view wants to be.即传入你希望View的大小
     * @param measureSpec Constraints imposed by the parent. 父级约束大小
     * @param childMeasuredState 一般传递0即可,特殊情况还可以传入1
     * @return
     */
    private int resolveSizeAndState2(int size, int measureSpec, int childMeasuredState) {
        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:
                //当specMode为AT_MOST,并且父控件指定的尺寸specSize小于View自己想要的尺寸时,
                //我们就会用掩码MEASURED_STATE_TOO_SMALL向量算结果加入尺寸太小的标记
                //这样其父ViewGroup就可以通过该标记其给子View的尺寸太小了,
                //然后可能分配更大一点的尺寸给子View
                if (specSize < size) {
                    result = specSize | MEASURED_STATE_TOO_SMALL;//按味或
                } else {
                    result = size;
                }
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
        }
        return result | (childMeasuredState&MEASURED_STATE_MASK);
    }

五、一个简单的例子

/**
 * Auther: Crazy.Mo
 * DateTime: 2017/5/3 15:52
 * Summary:
 */
public class MeasuredView extends View {
    private Context context;
    private int realWidth, realHeiht;

    public MeasuredView(Context context) {
        this(context, null);
    }

    public MeasuredView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public MeasuredView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        measure(widthMeasureSpec);
        measure(heightMeasureSpec);
        Log.e("onMeasure", "realWidth: " + realWidth + "realHeiht: " + realHeiht + "widthMeasureSpec" + widthMeasureSpec + "heightMeasureSpec" + heightMeasureSpec);
        setMeasuredDimension(realWidth, realHeiht);
    }

    private void measure(int measureValue) {
        int defalueSize = 200;
        int mode = View.MeasureSpec.getMode(measureValue);
        int specValue = View.MeasureSpec.getSize(measureValue);
        Log.e("onMeasure", "mode: " + mode + "specValue: " + specValue);
        switch (mode) {
            //指定一个默认值
            case MeasureSpec.UNSPECIFIED:
                Log.e("onMeasure", "mode: " + mode + "UNSPECIFIED " );
                realWidth = defalueSize;
                realHeiht = defalueSize;
                break;
            //取测量值
            case MeasureSpec.EXACTLY:
                Log.e("onMeasure", "mode: " + mode + "EXACTLY " );
                realHeiht = specValue;
                realWidth = specValue;
                break;
            //取测量值和默认值中的最小值
            case MeasureSpec.AT_MOST:
                Log.e("onMeasure", "mode: " + mode + "AT_MOST " );
                realWidth = Math.min(defalueSize, specValue);
                realHeiht = Math.min(defalueSize, specValue);
                break;
            default:
                break;
        }
    }
}

此时在布局中使用的话,

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="16dp"
    android:orientation="vertical"
    android:background="#0f8">

<!--    <com.ce.sesamecredit.ClockView
        android:layout_width="match_parent"
        android:layout_height="match_parent" />-->
    <com.ce.sesamecredit.MeasuredView
        android:background="@color/colorAccent"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</LinearLayout>

2、运行结果分析:

  • 未重写onMeasure方法时,默认的onMeasure仅可以解析match_parent和指定的具体数值

    这里写图片描述

  • 重写onMeasure方法时,可以解析match_parent、指定的具体数值和wrap_content

    这里写图片描述

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容