绘制流程

参考:
Android应用层View绘制流程与源码分析 - 工匠若水
https://blog.csdn.net/yanbober/article/details/46128379

前言

View在Activity的onCreate()方法中通过setContentView()方法添加到Activity的DecorView上。此时ViewRootImpl和DecorView没有关联上,不会绘制View。在 Activity的onResume()方法执行后,通过最终执行ViewRootImpl#setView,将DecorView会被添加带ViewRootImpl中。然后执行requestlayout(),requestLayout()和invalidate()都会触发ViewRootImpl绘制View => performTraversals()。


Dialog和PopupWindow中View的绘制过程也是一样的,只是触发的方式不同。例如Dialog中,是调用dialog.show()时,触发了WindowManagerImpl的addView()(上图步骤2),后面的流程就一样了。

可以通过Activity类的requestWindowFeature()方法来定制Activity关联PhoneWindow的外观,这个方法实际上做的是把我们所请求的窗口外观特性存储到了PhoneWindow的mFeatures成员中,在窗口绘制生成外观模板时,根据mFeatures的值绘制特定外观。

PhoneWindow中有DecorView对象;
DecorView中有PhoneWindow对象;
ViewRootImpl中有DecorView对象;

ViewRootImpl#performTraversals()

View的绘制从ViewRootImp#performTraversals()开始,该方法在另一个线程中被执行。该函数做的执行过程主要是根据之前设置的状态,判断是否重新计算视图大小(measure)、是否重新放置视图的位置(layout)、以及是否重绘 (draw)。

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal(); => performTraversals();
    }
}
private void performTraversals() { 
...... 
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); 
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); 
...... 
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec); 
......
mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
...... 
mView.draw(canvas); 
......
}
//mView对于Activty来说就是PhoneWindow.DecorView.

其中,getRootMeasureSpec的两个参数(mWidth, lp.width)mWith和mHeight 是屏幕的宽度和高度, lp是WindowManager.LayoutParams,它的lp.width和lp.height的默认值是MATCH_PARENT,所以通过getRootMeasureSpec 生成的测量规格MeasureSpec 的mode是MATCH_PARENT ,size是屏幕的高宽,即DecorView的MeasureSpec。

MeasureSpec(View的内部类)测量规格为int型,值由高2位规格模式specMode和低30位具体尺寸specSize组成。其中specMode只有三种值:

MeasureSpec.EXACTLY     //确定模式,父View希望子View的大小是确定的,由specSize决定;
MeasureSpec.AT_MOST     //最多模式,父View希望子View的大小最多是specSize指定的值;
MeasureSpec.UNSPECIFIED //未指定模式,父View完全依据子View的设计值来决定; 

对于DecorView而言,它的MeasureSpec是由窗口尺寸和其自身的LayoutParams共同决定;对于普通的View,它的MeasureSpec由父View的MeasureSpec和其自身的LayoutParams共同决定。

1. measure

父View的measure的过程会先测量子View,等子View测量结果出来后,再来测量自己。

View#onMeasure

onMeasure默认的实现仅仅调用了setMeasuredDimension,setMeasuredDimension函数是一个很关键的函数,它对View的成员变量mMeasuredWidth和mMeasuredHeight变量赋值,measure的主要目的就是对View树中的每个View的mMeasuredWidth和mMeasuredHeight进行赋值,所以一旦这两个变量被赋值意味着该View的测量工作结束。

setMeasuredDimension=>getDefaultSize
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)

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; //注意UNSPECIFIED返回的值是getSuggestedMinimumWidth
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize; //系统默认的规格
        break;
    }
    return result;
}

protected int getSuggestedMinimumWidth() {
    // 跟背景有关
    return (mBackground == null) ? mMinWidth :
                   max(mMinWidth, mBackground.getMinimumWidth());
    }

ViewGroup#measureChildren
ViewGroup#measureChild
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);
}
//静态方法
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);

    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:
        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:
        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:
        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
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

注意:int size = Math.max(0, specSize - padding);

MeasureSpec是由父View的MeasureSpec和子View的LayoutParams通过计算得出一个针对子View的测量结果。其中,子View的LayoutParams其实就是我们在xml写的时候设置的layout_width和layout_height 转化而来的。

实际的测量是在onMeasure方法进行,所以在View的子类需要重写onMeasure方法,这是因为measure方法是final的,不允许重载,所以View子类只能通过重载onMeasure来实现自己的测量逻辑。

measure流程图
image.png
measure注意事项
  1. 直接继承View的控件需要重写onMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent。
  2. ViewGroup的子类就必须要求LayoutParams继承子MarginLayoutParams,否则无法使用layout_margin参数。
  3. 使用View的getMeasuredWidth()和getMeasuredHeight()方法来获取View测量的宽高,必须保证这两个方法在onMeasure流程之后被调用才能返回有效值。而getWidth()与getHeight()方法必须在layout(int l, int t, int r, int b)执行之后才有效。

2. layout

重载onLayout的目的就是安排其children在父View的具体位置,重载onLayout通常做法就是写一个for循环调用每一个子视图的layout(l, t, r, b)函数,传入不同的参数l, t, r, b来确定每个子视图在父视图中的显示位置。View#layout=>setFrame(int left, int top, int right, int bottom)最终确定view的位置。一般情况下layout过程会参考measure过程中计算得到的mMeasuredWidth和mMeasuredHeight来安排子View在父View中显示的位置。

整个layout过程比较容易理解,layout也是从顶层父View向子View的递归调用view.layout方法的过程,即父View根据上一步measure子View所得到的布局大小和布局参数,将子View放在合适的位置上。

layout注意事项
  1. View.layout方法可被重载,ViewGroup.layout为final的不可重载,ViewGroup.onLayout为abstract的,子类必须重载实现自己的位置逻辑。

  2. 凡是layout_XXX的布局属性基本都针对的是包含子View的ViewGroup的,当对一个没有父容器的View设置相关layout_XXX属性是没有任何意义。

  3. 使用View的getWidth()和getHeight()方法来获取View测量的宽高,必须保证这两个方法在onLayout流程之后被调用才能返回有效值。

3. draw

draw的流程
  1. 背景绘制
  2. 对View的内容进行绘制
  3. 对当前View的所有子View进行绘制,ViewGroup #dispatchDraw(canvas)
  4. 对View的滚动条进行绘制

ViewGroup类已经为我们实现绘制子View的默认过程,这个实现基本能满足大部分需求,所以ViewGroup类的子类(LinearLayout,FrameLayout)也基本没有去重写dispatchDraw方法,我们在实现自定义控件,除非比较特别,不然一般也不需要去重写它, drawChild()的核心过程就是为子视图分配合适的cavas剪切区,剪切区的大小正是由layout过程决定的,而剪切区的位置取决于滚动值以及子视图当前的动画。设置完剪切区后就会调用子视图的draw()函数进行具体的绘制了。

ViewRootImpl中的代码会创建一个Canvas对象。

final Rect dirty = mDirty;
......
canvas = mSurface.lockCanvas(dirty);
......
mView.draw(canvas);
......
draw注意事项
  1. 如果该View是一个ViewGroup,则需要递归绘制其所包含的所有子View。View默认不会绘制任何内容,真正的绘制都需要自己在子类中实现。View的绘制是借助onDraw方法传入的Canvas类来进行的。

  2. 区分View动画和ViewGroup布局动画,前者指的是View自身的动画,可以通过setAnimation添加,后者是专门针对ViewGroup显示内部子视图时设置的动画,可以在xml布局文件中对ViewGroup设置layoutAnimation属性(譬如对LinearLayout设置子View在显示时出现逐行、随机、下等显示等不同动画效果)。

  3. 默认情况下子View的ViewGroup.drawChild绘制顺序和子View被添加的顺序一致,但是你也可以重载ViewGroup.getChildDrawingOrder()方法提供不同顺序。

4. invalidate

public void invalidate(boolean invalidateCache) {
    invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}

void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache, boolean fullInvalidate) {
......
  // Propagate the damage rectangle to the parent view.
  final AttachInfo ai = mAttachInfo;
  final ViewParent p = mParent;
  if (p != null && ai != null && l < r && t < b) {
    final Rect damage = ai.mTmpInvalRect;
    //设置刷新区域
    damage.set(l, t, r, b);
    //传递调运Parent ViewGroup的invalidateChild方法
    p.invalidateChild(this, damage);
  }
  ......
}
public final void invalidateChild(View child, final Rect dirty) {
    ViewParent parent = this;
    final AttachInfo attachInfo = mAttachInfo;
    ......
    do {
        ......
        //循环层层上级调运,直到ViewRootImpl会返回null
        parent = parent.invalidateChildInParent(location, dirty);
        ......
    } while (parent != null);
}

View的invalidate(invalidateInternal)方法实质是将要刷新区域直接传递给了父ViewGroup的invalidateChild方法,在invalidate中,调用父View的invalidateChild,这是一个从当前View向上级父View回溯的过程,每一层的父View都将自己的显示区域与传入的刷新Rect做交集。最后传递到ViewRootImpl的invalidateChildInParent,方法结束。并且在ViewRootImpl#invalidateChildInParent中调用scheduleTraversals(),scheduleTraversals会通过Handler的Runnable发送一个异步消息,调用doTraversal方法,然后最终调用performTraversals()执行重绘。

注意:invalidate方法向上遍历是在主线程。

invalidate注意事项
  1. 直接调用invalidate方法。请求重新draw,但只会绘制调用者本身。
  2. 触发setSelection方法。请求重新draw,但只会绘制调用者本身。
  3. 触发setVisibility方法。 当View可视状态在INVISIBLE转换VISIBLE时会间接调用invalidate方法,继而绘制该View。当View的可视状态在INVISIBLE\VISIBLE 转换为GONE状态时会间接调用requestLayout和invalidate方法,同时由于View树大小发生了变化,所以会请求measure、layout过程以及draw过程,同样只绘制需要【重新绘制】的视图。
  4. 触发setEnabled方法。请求重新draw,但不会重新绘制任何View包括该调用者本身。
  5. 触发requestFocus方法。请求View树的draw过程,只绘制【需要重绘】的View。

ViewRootImpl类的performTraversals()何时被调用?
Activity#setContentView=>mContentParent.addView(view, params);
ViewGroup#addView=>requestLayout()&invalidate(true);

5. requestLayout

public void requestLayout() {
  ......
  if (mParent != null && !mParent.isLayoutRequested()) {
    //由此向ViewParent请求布局
    //从这个View开始向上一直requestLayout,最终到达ViewRootImpl的requestLayout
    mParent.requestLayout();
 }
}

View的requestLayout时其实质就是层层向上传递,直到ViewRootImpl为止,然后触发ViewRootImpl的requestLayout方法,其中调用 scheduleTraversals();

requestLayout()方法会调用measure过程和layout过程,不会调用draw过程,也不会重新绘制任何View包括该调用者本身。

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

推荐阅读更多精彩内容