View的绘制流程

1.前&ensp言

对View的绘制流程设计思想,运用场景来次梳理。以前学只是套公式用,比较浅。

2.ViewRoot与DecorView

ViewRoot是链接WindowManager和DecorView的纽带,View绘制的三大流程从ViewRoot来开始到完成.它为抽象类,具体实现算ViewRootImpl.
Activity创建完毕会将DecorView添加到WindowManager,同时创建ViewRootImpl与DecorView(顶级View)关联.

ViewRoot的performTraversals方法按顺序调用三个方法,三个方法分别调用顶级View的三大绘制流程,顶级View调用子View的绘制流程.层层下发,完成一个Activity中Window下所有的View的绘制流程

2017-03-16 10-21-11屏幕截图.png

(1)measure得出View的测量宽高。
This is called to find out how big a view should be.
(2)layout得出view的位置与实际宽高,即四个定点坐标与View的最终宽高。
(3)draw完成具体图像绘制。
绘制流程就是这三步的顺序执行:先(1)测量完毕才好确定(2)具体位置,才能做(3)具体图像绘制.

3.MeasureSpec

理解View的测量原理就要先明白MesaureSpec测量参数,或者叫测量规格。

MesaureSpec一个32位的int值,前2位表示MeasureMode,后30位表示MeasureSize。MeasureSize是宽高数值。这样设计的一个好处是节约内存空间,但需要封包与解包.

MeasureMode有3种标识:
UNSPECIFIED标识
父View对自身无限制.一般用于系统内部,实际应用中我们可以忽略此标识。
AT_MOST
表示MeasureSize不确定。
当自身的LayoutParamters指定为wrap_content的时候,就为此标识。MeasureSize值需要View自己处理(可以是固定值也可以是变量),不能超过父view的MeasureSize。不设置为父View的MeasureSize。如TextView的测量值会根据TextSize来计算得出。具有变化性.
EXACTLY
表示MeasureSize是确定的。当自身的LayoutParamters指定为match_parent或者准确值的时候,就为是此标记。
match_parent时MeasureSize为父View的MeasureSize.准确值时MeasureSize就是准确值。

MesaureSpec受自身的LayoutParamters的宽高,margin与父View限制(父的MesaureSpec,padding,已用空间)影响,可以从源码 ViewGroup#measureChildWithMargins看出。

    protected void measureChildWithMargins(View child, int parentWidthMeasureSpec,
                         int widthUsed,int parentHeightMeasureSpec, int heightUsed) 
   {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

4.View与ViewGroup的measure过程

View的measure方法带final修饰,子类无法修改。measure中调用onMeasure方法,从源码看是否调用onMesure要经过一些判断条件,常规的View绘制都会满足调用onMeasure方法。
源码的注释表示,真实的测量过程在onMeasure方法。onMeasure可以被子类覆写。

 The actual measurement work of a view is performed in
 {@link #onMeasure(int, int)}, called by this method. Therefore, only
 {@link #onMeasure(int, int)} can and must be overridden by subclasses.

onMeasure()的源码描述覆写此方法必须要调用setMeasureDimension().

When overriding this method, you
     * <em>must</em> call {@link #setMeasuredDimension(int, int)} to store the
     * measured width and height of this view. Failure to do so will trigger an
     * <code>IllegalStateException</code>, thrown by

setMeasuredDimension()设置测量值。父View的measureChildlWithMargin调用child的measure方法传入允许的测量值(DecorView由ViewRootImpl给出,为屏幕长宽).子View调用getDefaultSize得出测量值作为setMeasuredDimension的参数

       setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
       getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
   }```
getDefaultSize()也没干啥事,将MeasureSpec解包,如果是UNSPECIFIED,返回getSuggestedMinimumWidth().有背景返回背景宽高与android:miniWith较大的值,否返回在布局文件中设置的那个从不起作用的android:miniWith。
如果是 AT_MOST与EXACTLY,返回解包的size值。

ViewGroup继承自View,继承了measure与onMeasure方法,未对onMeasure覆写。而是由它的继承者根据自己的布局特性去覆写。比如LinearLayout.

ViewGroup中增加了measureChildren,measureChild,,measureChildWithMargins三个方法,继承者在onMeasure中调用这些方法达到层层测量的效果。measureChildWithMargins与measureChild的区别在于子View的margin值对子View测量是否影响。

看LinearLayout的源码知道,onMesure中测量完子View,再来设置自己的测量值。因为在AT_MOST模式(wrap_content)的时候测量值会受到子View的测量值影响。我们在自定义View直接继承View或者ViewGroup覆写onMeasure方法时也要学LinearLayout,在模式是AT_MOST(wrap_content)的时候给出获取测量值的方式。不做处理测量值将会为父的测量值,也就是wrap_content等同于match_parent.

总结下测量流程:
![2017-04-14 09-57-13屏幕截图.png](http://upload-images.jianshu.io/upload_images/2492300-226786ab2a94078a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

综上有我们在自定义View覆写onMeasure的时候有二个注意点:
(1)自定义View覆写onMeasure的时候不要忘记调用setMeasureDimension().
(2)AT_MOST(wrap_content)的时候给出获取测量值的方式。否则wrap_content效果同match_parent.
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int  widthMeasureMode = MeasureSpec.getMode(widthMeasureSpec);
    int  widthMeasureSize = MeasureSpec.getSize(widthMeasureSpec);
    int  heightMeasureMode = MeasureSpec.getMode(heightMeasureSpec);
    int  heightMeasureSize = MeasureSpec.getSize(heightMeasureSpec);
   if (widthMeasureMode == MeasureSpec.AT_MOST && heightMeasureMode == MeasureSpec.AT_MOST) {
       //设置成默认的大小
       setMeasuredDimension(mWrapWidth, mWrapHeight);
   } else if (widthMeasureMode == MeasureSpec.AT_MOST) {
       setMeasuredDimension(mWrapWidth, heightMeasureSize);
   } else if(heightMeasureMode == MeasureSpec.AT_MOST) {
       setMeasuredDimension(widthMeasureSize, mWrapHeight);
   }
}

##5.layout过程
ViewRootImpl#performMeasure完成后,执行performLayout过程,确定View树下View们的位置参数,从而View的实际宽高也会在这过程中得出.

**layout方法:**
它的四个参数l,t,r,b.方法描述是相对父View四个顶点的偏移坐标。
public修饰,子类可以覆盖,类的描述中不建议这么做。ViewGroup覆写layout方法,并且加上了final修饰。
layout方法调用setFrame.四个顶点与全局变量保存的顶点比较,判断是否改变.改变则保存成全局变量顶点,将View的宽高size更新,返回true.
setFrame()如果返回true则调用onLayout。

**onlayout方法**
onLayout方法在View中是空实现。ViewGroup中也是空实现,由继承者根据自己的布局特性去覆写。
LinearLayout的onLayout根据布局方向的不同调用layoutVertical与layoutHorizontal,将子View的位置确定(排列好子View)。

看完layout与onLayout的源码设计,知道,要改变View的layout过程,可以直接重写layout,也可以对setFrame重写.
onLayout的用途是父View确定子View的位置.如果自定义ViewGroup就要注意对OnLayout重写实现.在ViewGroup中layout方法是final的.

以layoutVertical()为例。layoutVertical传入4个参数(int left, int top, int right, int bottom)。
1.layoutVertical先处理padding和gravity对子View顶点坐标的影响。
2.遍历子View,处理子View的measure宽高,layoutParams对子View顶点坐标的影响。记录下childTop,childLeft给下个子View使用
3.执行setChildFrame,调用子View的layout()
private void setChildFrame(View child, int left, int top, int width, int height) {        
    child.layout(left, top, left + width, top + height);
}
```

一层层下来就完成一个Activity中所有View的layout过程。

在setChildFrame()中传入的width与height是测量宽高。测量宽高在layout之前的measure过程完成赋值。child.layout调用setFrame给mLeft,mTop,mRight,mBottom赋值。实际宽等于mLeft-mRight。正常情况,测量宽高与实际宽高是相等的。但是如果覆写View的layout(l.,t,,r + 100, b + 100)就会导致测量宽高不等于实际宽高。或者某些极端的情况下,需要多次测量,也会导致不一致。
所以,在layout过程中才会确定View的实际宽高.

6.draw过程

分如下几步:
(1)绘制背景background.draw(canvas)
(2)If necessary, save the canvas' layers to prepare for fading
(3)绘制自己(onDraw)
(4)绘制children(dispatchDraw)
(5) If necessary, draw the fading edges and restore layers
(6)绘制装饰(onDrawScrollBars)
源码的英文注释也是按照这个步骤来的,一般只要看1,3,4,6
此方法不建议被覆盖,如果覆盖记得调用super.draw.建议覆盖的是onDraw.onDraw会在第三步调用.第四步如果是ViewGroup,则调用dispatchDraw调用drawChild。而View的dispatchDraw的实现是空的,ViewGroup对dispatchDraw进行了实现。符合是ViewGroup才需要画子View的思想.

 protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }

setWillNotDraw这个标记为true系统会认为View不需要绘制即执行onDraw,那么就会进行相应的优化.View除ViewGroup默认开启的此标记位为true,其他都默认关闭.
如果ViewGroup要绘制要显示关闭这个优化标记位.

7.自定义View的分类

(1)继承View
多用于实现不规则图形,无法通过组合现有的View.需要覆写onDraw()画出想要的效果.因为直接继承View,需要覆写onMeasure,处理wrap_content失效问题.padding也需要在onDraw中处理
(2)继承ViewGroup
现有的布局方式不满足需求才会这样,或者几个View组成一个特定的布局,很少会这样做,
需要覆写onMeasure,处理wrap_content失效问题.,测量完子View再给出自己的测量值.布局过程中根据布局特性对子View进行布局.
(3)继承特定的View
对已有的实现View进行功能扩充.因为被继承的View已经有对绘制流程做处理.如果没有涉及到对绘制流程的修改,则不需要关注绘制流程.有修改则考虑修改对绘制流程的影响,按需灵活处理.
(4)继承特定的ViewGroup
几种View组合会使用这种方式.和(3)一样,绘制过程已有处理,按需灵活处理.

注意事项汇总

(1)wrap_content失效处理
直接继承View与ViewGroup,需要在onMeasure中对wrap_content做处理.
(2)padding与margin处理
直接继承View的控件,需要在onDraw中处理padding,否则padding属性不起作用.
直接继承ViewGroup的需要在onMeasure与onLayout中对padding与子元素的margin做处理,否则属性失效.
(3)尽量不要在View中使用Handler
View内部本身提供了post系列方法,可以替代Handler.
(4)View中有线程或者动画,需要及时停止.
当包含View的Activity退出或者当前View被remove的时,View的onDetachedFromWindow会被调用.在此方法中停止线程或者动画.当View不可见的时候也需要停止线程或者动画.
如果不及时处理,可能会造成内存泄露.
(5)有滑动嵌套,注意滑动冲突

实战自定义View

(1)直接继承View的CricleView
注意处理wrap_content与padding
有时候希望在xml布局文件中给View设置自定义属性.自定义xml属性步骤:
一:在res->values下新建xml.名字随意.我的是:attr.xml
二:在attr.xml中声明属性集合与自定义属性.下面我声明了一个叫"CircleView"的属性集合,定义了circle_color与circle_radius属性

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CircleView">
        <attr name="circle_color" format="color"/>
        <attr name="circle_radius" format="dimension"/>
    </declare-styleable>
</resources>

三:在xml布局文件中使用自定义属性
布局xml根节点需要加上

 xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools"
tools:ignore="missingPrefix"

为什么要加tools:ignore="missingPrefix":
Lint, Android's code analysis tool, doesn't seem to know about support 自定义的属性 , yet. You can safely ignore the error by addingtools:ignore="MissingPrefix"to theViewtag.
使用自定义属性:

    <android.xwpeng.tviewdesign.view.CircleView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="50dp"
            app:circle_color="@color/blue"
            app:circle_radius="50dp"
            android:background="#ff0000"
            android:padding="10dp"
            />

四:在View的构造方法中解析获得自定义属性,在绘制流程中使用属性.

       TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView, defStyleAttr, 0);
        mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED);
        mRadius = a.getDimension(R.styleable.CircleView_circle_radius, 0);
        mWrapWidth = mWrapHeight = 2 * (int)mRadius;
        a.recycle();

也可以这么写

    TypedArray ta = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SwipeMenuLayout, defStyleAttr, 0);
        int count = ta.getIndexCount();
        for (int i = 0; i < count; i++) {
            int attr = ta.getIndex(i);
            //如果引用成AndroidLib 资源都不是常量,无法使用switch case
            if (attr == R.styleable.SwipeMenuLayout_swipeEnable) {
                isSwipeEnable = ta.getBoolean(attr, true);
            } else if (attr == R.styleable.SwipeMenuLayout_ios) {
                isIos = ta.getBoolean(attr, true);
            } else if (attr == R.styleable.SwipeMenuLayout_leftOrRight) {
                isLeftSwipe = ta.getBoolean(attr, true);
            }
        }
        ta.recycle();
    }

源码参考
https://github.com/xwpeng/TViewDesign/blob/master/app/src/main/java/android/xwpeng/tviewdesign/view/CircleView.java

(2)直接继承ViewGroup
直接继承ViewGroup处理的地方较多教麻烦.像子View的measureSpe的处理,子View的layout,gravity属性等,都要一一处理好.一般继承现成的ViewGroup就能满足需求.github练习源码中HorizontalScrollViewEx,MyScollView是差不多的,只要看一个熟悉一下继承ViewGroup的一些Layout都要做写什么处理就行.
(3)(4)继承特定的View与ViewGroup,比较简单,绘制流程已经处理过,按需求修改扩充.

后记

github练习源码:https://github.com/xwpeng/TViewDesign
接下来对SwipeMenuLayout与StickLayout进行探索学习,实际运用巩固知识.

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

推荐阅读更多精彩内容