Android 自定义View--从源码理解View的绘制流程

前言

在Android的世界里,View扮演着很重要的角色,它是Android世界在视觉上的具体呈现。Android系统本身也提供了很多种原生控件供我们使用,然而在日常的开发中我们很多时候需要去实现一些原生控件无法实现的效果。这个时候,我们就不得不采取自定义View的方式来实现我们所需要的效果。其实要想使用自定义View,首先我们应该对View的绘制流程有一个基本的了解,只有掌握了View的绘制原理,才能更好的掌握自定义View这一技能。

走进源码

1.起点--performTraversals():

任何一件事都有一个事件的起点,View的绘制流程同样如此,它的起点就在ViewRootImpl的performTraversals方法。在这个方法中,会依次调用到三个重要的方法,它们分别是performMeasure、performLayout、performDraw方法;而这三个方法的内部又分别会调用到View的measure、layout、draw方法。由于performTraversals方法源码比较长,这里只贴出其内部最终调用到View的measure、layout、draw方法的关键代码行:

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    if (mView == null) {
        return;
    }
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
    try {
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec); // View的measure方法
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
 }

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) {
    ........
    final View host = mView;
    if (host == null) {
        return;
    }
    ........
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
    try {
        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); // View的layout方法
        ........
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
    ........
}

private void performDraw() {
    if (mAttachInfo.mDisplayState == Display.STATE_OFF && !mReportNextDraw) {
        return;
    } else if (mView == null) {
        return;
    }
    ........
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");
    ........
    try {
        boolean canUseAsync = draw(fullRedrawNeeded); // draw方法内部调用drawSoftware方法
        ........                                     
    } 
    ........
}

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
    // Draw with software renderer.
    final Canvas canvas;
    ........
    try {
        canvas = mSurface.lockCanvas(dirty);
        ........
        mView.draw(canvas); // View的draw方法
        ........
    } 
    ........
    return true;
}

现在,我们对View绘制的流程有了一个简单的了解,它要经历measure、layout、draw这三个流程才能最终被绘制到屏幕上。其中,measure完成对View的测量,layout完成对View的布局,draw完成对View的绘制。

2.MeasureSpec:

在开始阅读View绘制的三个流程之前,我们还需要知道MeasureSpec这个类的作用。它是View内部的一个类,这个类封装了从父布局传递到子View的布局要求,即对View宽度和高度的要求,它由模式和尺寸两部分组成,有三种可能的模式:

2.1.EXACTLY:

精确值模式,父布局已经确定了子View所需的大小。对应于我们在布局中给View的宽高指定了具体的数值或设置成match_parent。

2.2.AT_MOST:

最大值模式,在指定的大小范围内(由父布局决定),子元素可以任意增大。对应于我们在布局中给View的宽高设置成wrap_content。

2.3.UNSPECIFIED:

未指定模式,父布局没有对子View施加任何约束,它可以是任意大小。这种我们日常开发基本上没用到过。

之所以说到这个类,是因为在View的measure方法中,接收两个32位的int类型的参数。而这两个参数均是通过MeasureSpec获取到的,它的高两位代表测量模式,低30位代表测量尺寸。而关于这两个参数值的确定,还要根据View的布局参数(即LayoutParams)和父布局的约束而共同决定,这一点在MeasureSpec类的源码解释中也能体现。

3.测量--onMeasure():

现在开始阅读View绘制的第一个流程--测量。前面讲到的performTraversals方法中,最终调用到View的measure方法来开始View的测量。其实在measure方法中并没有进行真正的测量操作,其内部只是根据传入的两个参数以及View内部的一些属性进行一些逻辑判断,来决定是否需要调用onMeasure方法来进行真正的测量操作。关于measure方法的源码这里就不详细讲解,但需要知道的是它是一个被final修饰的方法,这也就意味着子类不能重写它。这里贴出源码中关于measure方法的解释:

这个方法用于获取一个View的大小,父布局在宽度和高度的参数中提供了约束信息;View的实际测量工作是在onMeasure方法中进行的,只是在这个方法内被调用。

既然实际的测量工作在onMeasure方法中,我们就来看一下其方法的源码:

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

方法内只是调用了setMeasuredDimension方法,这个方法的作用就是存储测量得到的宽度和高度,源码的注释中还提到此方法一定要在onMeasure中被调用,否则在测量时会触发异常。现在我们再回过头来看一下setMeasuredDimension方法中的两个参数的由来,首先我们来看一下getSuggestedMinimumWidth和getSuggestedMinimumHeight方法:

protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
protected int getSuggestedMinimumHeight() {
    return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}

方法的内部就是判断当前View是否设置了Drawable背景,如果没有设置,就返回当前View的最小宽度(高度)。如果设置了背景,那就返回背景的最小宽度(高度)和View的最小宽度(高度)二者间的最大值。其中,mMinWidth和mMinHeight我们可以在xml中指定或者通过代码动态设置。接下来再看看getDefaultSize方法:

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;
}

方法中的第二个参数是从measure方法中一路透传过来的,关于它的值的由来前面已经阐述过。方法的内部调用MeasureSpec类的getMode和getSize方法获取到测量模式和测量尺寸,然后根据测量模式返回对应的尺寸结果。
到这里,一个单独View的measure过程可以说就讲完了,但日常开发中一个View往往是放在一个ViewGroup容器中的,那么ViewGroup的measure过程又是怎么实现的呢?带着这个疑问我们走进ViewGroup的源码,我们会发现,在ViewGroup中根本没有重写onMeasure方法,也就是说没有定义具体的测量逻辑,其实这也不难理解,因为ViewGroup本身是一个抽象类,而且它的子类的布局特性也都不尽相同,这也就导致它们测量的方式会有所不同,因此在ViewGroup中并没有定义onMeasure方法的实现(在它的子类中可以看到onMeasure的实现)。不过在ViewGroup中,定义了一个measureChildren方法,其源码如下:

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < size; ++i) {
        final View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
    final LayoutParams lp = child.getLayoutParams();
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

在measureChildren方法中就是遍历自身所有的子View去调用measureChild方法,在measureChild方法的内部根据父布局对其宽度和高度的限制以及自身的LayoutParams来计算出自身的宽度和高度的MeasureSpec值,然后就是执行单个View的measure流程了。关于View的measure流程到这就全部讲完了,在这里要记住几个关于measure过程的注意点:

1.如果自定义View重写了onMeasure方法,那么记得一定要在其内部调用setMeasuredDimension方法,否则会抛出IllegalStateException。
2.一个View的宽度和高度,不仅由自身的LayoutParams决定,还取决于父布局的约束。
3.View的measure方法是被final修饰的,不能被子类重写。

4.布局--onLayout():

现在来看一下View绘制的第二个流程--布局,这个流程的作用就是父布局用来确定它的每一个子View的位置。在前面说到的performTraversals方法在执行完View的measure过程后,会继续向下执行调用到View的layout方法,首先看一下layout方法的源码:

public void layout(int l, int t, int r, int b) {
    if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) { // step1
        onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
        mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    }
    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;
    boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b)
                          : setFrame(l, t, r, b); // step2
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {  // step3
        onLayout(changed, l, t, r, b);
        ........
    }
    ........
}

这里只贴出了layout方法中部分关键的代码,方法中的四个参数分别代表View相对于父容器的左、上、右、下的位置。接着走进方法内部,在step1处判断是否需要在布局前进行测量操作,如果需要就重新调用onMeasure方法进行测量操作。然后在step2处判断当前View的父布局是否有特殊的边界(类似于阴影),如果有就执行setOpticalFrame方法,如果没有就执行setFrame方法。而其实setOpticalFrame方法的内部最终还是调用了setFrame方法,至于这个setFrame方法,我们先来看一下它的源码:

protected boolean setFrame(int left, int top, int right, int bottom) {
    boolean changed = false;
    ........
    if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
        changed = true;
        ........
        mLeft = left;
        mTop = top;
        mRight = right;
        mBottom = bottom;
        ........
    }
    return changed;
}

这里也只是贴出了一些关键的代码,方法的主要作用就是两个,第一将View相对于父布局的左、上、右、下位置信息保存(分别赋值给mLeft、mTop、mRight、mBottom),也就是说在这个方法中确定了View在父布局中的位置信息;第二就是根据对比之前的左、上、右、下位置信息来确定布局位置是否发生了改变。
现在,回到刚刚的layout方法中,在step3处,根据step2处返回的结果来决定是否需要调用onLayout方法。当我们在View的源码中找到onLayout方法时,我们会发现它是一个空方法,这个方法的源码注释为:

当一个视图需要为其每个子视图分配大小和位置时,它会在layout方法中被调用;一个带有子视图的派生类需要重写这个方法并让其每个子视图调用它的layout方法。

这也就能说明了在View的源码中onLayout方法为什么是一个空方法了,因为它只是一个单独的 View,不包含任何一个子View,所以也就无需去布局任何一个View。那谁才会包含子View呢?答案当然是ViewGroup了,我们走进ViewGroup的源码寻找onLayout方法,结果如下:

@Override
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);

居然是一个抽象方法,其实这个也不难理解,因为ViewGroup是一个抽象类,它没有具体的布局特性,而它的子类像LinearLayout、RelativeLayout才有具体的布局特性,因此在ViewGroup的子类中我们才可以看到onLayout方法的具体实现。为了验证这一点,这里我贴出FrameLayout中的onLayout方法的源码,如下:

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}

void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
    final int count = getChildCount();
    ........
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        if (child.getVisibility() != GONE) {
            ........
            child.layout(childLeft, childTop, childLeft + width, childTop + height);
        }
    }
}

在FrameLayout类的onLayout方法中,会调用其内部的layoutChildren方法,layoutChildren方法中略去的部分就是每个子View计算其相对于父布局的左、上、右、下位置信息的过程,因为本文意在讲解View的整个绘制流程,所以这里就不过多的描述位置信息计算的细节了(感兴趣的同学可以自己去看一下不同的布局控件的onLayout方法计算子View的位置信息的逻辑);在确定了位置信息后,最终再调用每个View的layout方法。到这里,View的layout流程就讲完了,关于View的layout流程,也有几个需要注意的点:

1.自定义View的时候尽量不要去重写layout方法,否则可能会导致一些意外的绘制结果。
2.一个继承了ViewGroup的子类,需要重写onLayout方法并在内部调用每一个子View的layout方法。

5.绘制--onDraw():

到了View绘制的最后一个流程--绘制,在经历了测量和布局两个流程后,View的尺寸和位置都已经确定,就只差将View绘制到屏幕上了。首先,我们需要知道,我们最终将View绘制到了什么上面去。在开头说到的performTraversals方法执行完layout流程后,会继续向下执行调用到drawSoftware方法,在drawSoftware方法的内部会创建一个Canvas对象,最终这个Canvas对象会被传入到View的draw方法中,而这个Canvas对象就是呈现视图的画布,也就是说最终View都会被绘制到这个画布对象上去。接下来,我们先来看一下源码中关于View的draw方法的解释:

手动将视图(及其所有子视图)渲染到给定的画布上。在调用此函数之前,视图必须已经完成了完整的布局过程。当实现一个View的时候,去重写onDraw方法而不是重写这个draw方法;如果要重写此方法,要调用超类的版本。

通过源码中的注释我们可以看出在我们使用自定义View的时候,系统是不建议我们重写draw方法的,而是让我们去重写onDraw方法。由此我们可以先推测出一个结论,在draw方法中系统已经默认为我们提供了一套完整有序的绘制View到画布上的流程,而onDraw方法中会根据不同的View的特性来采取不同的方式绘制View的具体内容。带着这个推测我们来看一下draw方法的源码,如下:

public void draw(Canvas canvas) {
    ........
    /*
     * Draw traversal performs several drawing steps which must be executed in the appropriate order:
     *1. Draw the background
     *2. If necessary, save the canvas' layers to prepare for fading
     *3. Draw view's content
     *4. Draw children
     *5. If necessary, draw the fading edges and restore layers
     *6. Draw decorations (scrollbars for instance)
     */
    // Step 1, draw the background, if needed
    ........
    drawBackground(canvas);
    // skip step 2 & 5 if possible (common case)
    final int viewFlags = mViewFlags;
    boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
    boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
    if (!verticalEdges && !horizontalEdges) {
        // Step 3, draw the content
        onDraw(canvas);
        // Step 4, draw the children
        dispatchDraw(canvas);
        ........
        // Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground(canvas);
        // Step 7, draw the default focus highlight
        drawDefaultFocusHighlight(canvas);
        ........
        return;
    }
    ........
}

还是只贴出了关键部分的代码,在方法中我们可以看到一段很长的注释,这就是View在通常情况下的draw流程的一套完整的步骤(步骤2和步骤5通常情况下会跳过,这里就不做说明):

  • step1:调用drawBackground方法绘制View的背景(内部调用Drawable的draw方法)
  • step3:调用onDraw方法绘制View的具体内容
  • step4:调用dispatchDraw方法绘制子View
  • step6:调用onDrawForeground方法绘制装饰(例如,滚动条)
  • step7:调用drawDefaultFocusHighlight方法绘制默认的焦点突出显示

这里我们重点看一下步骤3和步骤4,在步骤3中调用的是View的onDraw方法,而View的onDraw方法是一个空方法,这就说明具体该怎么绘制View的内容要取决于View的子类的特性,因此要根据子类的特点来重写onDraw方法实现绘制View内容的具体逻辑(TextView和ImageView的源码中可以看到其根据自身的特点重写了onDraw方法的逻辑)。现在,也可以证明刚刚我的推测是正确的了。
再来看看步骤4中的dispatchDraw方法,这个方法在View中也是一个空方法,因为一个单个的View是不存在子View的;而ViewGroup中重写了这个方法,在方法的内部遍历所有的子View执行drawChild方法,在drawChild方法的内部又调用了View的draw方法。到此,View的draw流程也讲完了,关于View的draw流程,这里依然有几个需要注意的点:

1.在View的源码中系统已默认为我们提供了一套完整有序的draw流程,我们在自定义View的时候最好不要自己去重写draw方法中的逻辑。
2.View的子类需要重写onDraw方法,并根据自身的特点来编写绘制View内容的逻辑。

结语

到这里,View的整个绘制流程就已经分析完了,其实View的源码还是非常多的,文中提到的也只是View源码中的冰山一角。如果想要对View有更深刻的理解,还是需要更深层次的阅读它的源码的。最后,如果文章对您有帮助,还望点赞支持下,有写的不好的地方也望指出。

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

推荐阅读更多精彩内容