Android界面生成流程:View的绘制(Draw)

来源:互联网 发布:软件开发和软件研发 编辑:程序博客网 时间:2024/05/17 16:55

回顾

在《 Android界面生成流程:View的布局(Layout)》中说到View的布局流程,之前通过测量得到一个View的矩阵图,那么布局是根据父容器s属性和子view的属性以及宽高进行计算,从而得到4个顶点的坐标:

首先获取父容器的padding值,然后遍历每一个子view,子view根据自己的Gravity值,测量宽高,父容器的padding值,来确定子view的的布局参数,然后调用child.layout方法,把布局流程从父容器传递到子容器。

注意:

  • 如果要得到View的位置信息,那么就应该在layout方法完成后调用getLeft()、getTop()等方法来取得最终宽高,如果是在此之前调用相应的方法,只能得到0的结果,所以一般我们是在onLayout方法中获取View的宽高信息。

  • getMeasureWidth()方法在measure()过程结束后就可以获取到了,而getWidth()方法要在layout()过程结束后才能获取到。如果编码习惯比较好,一般情况下两者结果是相同的。但是如果刻意去在child.layout的时候去改变值,两者结果是会不同的,但是这种做法充分不尊重measure()过程计算出的结果,通常情况下是不推荐这么写的。

绘制流程一

从performDraw说起

前面几篇文章提到,三大工作流程始于ViewRootImpl#performTraversals,在这个方法内部会分别调用performMeasure,performLayout,performDraw三个方法来分别完成测量,布局,绘制流程。那么我们现在先从performDraw方法看起,ViewRootImpl#performDraw:

private void performDraw() {    //...    final boolean fullRedrawNeeded = mFullRedrawNeeded;    try {        draw(fullRedrawNeeded);    } finally {        mIsDrawing = false;        Trace.traceEnd(Trace.TRACE_TAG_VIEW);    }    //省略...}

里面又调用了ViewRootImpl#draw方法,并传递了fullRedrawNeeded参数,而该参数由mFullRedrawNeeded成员变量获取,它的作用是判断是否需要重新绘制全部视图,如果是第一次绘制视图,那么显然应该绘制所以的视图,如果由于某些原因,导致了视图重绘,那么就没有必要绘制所有视图。我们来看看ViewRootImpl#draw:

private void draw(boolean fullRedrawNeeded) {    ...    //获取mDirty,该值表示需要重绘的区域    final Rect dirty = mDirty;    if (mSurfaceHolder != null) {        // The app owns the surface, we won't draw.        dirty.setEmpty();        if (animating) {            if (mScroller != null) {                mScroller.abortAnimation();            }            disposeResizeBuffer();        }        return;    }    //如果fullRedrawNeeded为真,则把dirty区域置为整个屏幕,表示整个视图都需要绘制    //第一次绘制流程,需要绘制所有视图    if (fullRedrawNeeded) {        mAttachInfo.mIgnoreDirtyState = true;        dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));    }    //省略...    if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {                return;        }}

这里省略了一部分代码,我们只看关键代码,首先是先获取了mDirty值,该值保存了需要重绘的区域的信息,关于视图重绘,后面会有文章专门叙述,这里先熟悉一下。接着根据fullRedrawNeeded来判断是否需要重置dirty区域,最后调用了ViewRootImpl#drawSoftware方法,并把相关参数传递进去,包括dirty区域,我们接着看该方法的源码:

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,            boolean scalingRequired, Rect dirty) {    // Draw with software renderer.    final Canvas canvas;    try {        final int left = dirty.left;        final int top = dirty.top;        final int right = dirty.right;        final int bottom = dirty.bottom;        //锁定canvas区域,由dirty区域决定        canvas = mSurface.lockCanvas(dirty);        // The dirty rectangle can be modified by Surface.lockCanvas()        //noinspection ConstantConditions        if (left != dirty.left || top != dirty.top || right != dirty.right                || bottom != dirty.bottom) {            attachInfo.mIgnoreDirtyState = true;        }        canvas.setDensity(mDensity);    }     try {        if (!canvas.isOpaque() || yoff != 0 || xoff != 0) {            canvas.drawColor(0, PorterDuff.Mode.CLEAR);        }        dirty.setEmpty();        mIsAnimating = false;        attachInfo.mDrawingTime = SystemClock.uptimeMillis();        mView.mPrivateFlags |= View.PFLAG_DRAWN;        try {            canvas.translate(-xoff, -yoff);            if (mTranslator != null) {                mTranslator.translateCanvas(canvas);            }            canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);            attachInfo.mSetIgnoreDirtyState = false;            //正式开始绘制            mView.draw(canvas);        }    }     return true;}

可以看出,首先是实例化了Canvas对象,然后锁定该canvas的区域,由dirty区域决定,接着对canvas进行一系列的属性赋值,最后调用了mView.draw(canvas)方法,前面分析过,mView就是DecorView,也就是说从DecorView开始绘制,前面所做的一切工作都是准备工作,而现在则是正式开始绘制流程。

View的绘制

由于ViewGroup没有重写draw方法,因此所有的View都是调用View#draw方法,因此,我们直接看它的源码:

public void draw(Canvas canvas) {    final int privateFlags = mPrivateFlags;    final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&            (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);    mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;    /*     * 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    int saveCount;    if (!dirtyOpaque) {        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        if (!dirtyOpaque) onDraw(canvas);        // Step 4, draw the children        dispatchDraw(canvas);        // Overlay is part of the content and draws beneath Foreground        if (mOverlay != null && !mOverlay.isEmpty()) {            mOverlay.getOverlayView().dispatchDraw(canvas);        }        // Step 6, draw decorations (foreground, scrollbars)        onDrawForeground(canvas);        // we're done...        return;    }    ...}

可以看到,draw过程比较复杂,但是逻辑十分清晰,而官方注释也清楚地说明了每一步的做法。我们首先来看一开始的标记位dirtyOpaque,该标记位的作用是判断当前View是否是透明的,如果View是透明的,那么根据下面的逻辑可以看出,将不会执行一些步骤,比如绘制背景、绘制内容等。这样很容易理解,因为一个View既然是透明的,那就没必要绘制它了。接着是绘制流程的六个步骤,这里先小结这六个步骤分别是什么,然后再展开来讲。

绘制流程的六个步骤:
1、对View的背景进行绘制
2、保存当前的图层信息(可跳过)
3、绘制View的内容
4、对View的子View进行绘制(如果有子View)
5、绘制View的褪色的边缘,类似于阴影效果(可跳过)
6、绘制View的装饰(例如:滚动条)
其中第2步和第5步是可以跳过的,我们这里不做分析,我们重点来分析其它步骤。

Step 1 :绘制背景

这里调用了View#drawBackground方法,我们看它的源码:

private void drawBackground(Canvas canvas) {    //mBackground是该View的背景参数,比如背景颜色    final Drawable background = mBackground;    if (background == null) {        return;    }    //根据View四个布局参数来确定背景的边界    setBackgroundBounds();    ...    //获取当前View的mScrollX和mScrollY值    final int scrollX = mScrollX;    final int scrollY = mScrollY;    if ((scrollX | scrollY) == 0) {        background.draw(canvas);    } else {        //如果scrollX和scrollY有值,则对canvas的坐标进行偏移,再绘制背景        canvas.translate(scrollX, scrollY);        background.draw(canvas);        canvas.translate(-scrollX, -scrollY);    }}

可以看出,这里考虑到了view的偏移参数,scrollX和scrollY,绘制背景在偏移后的view中绘制。

Step 2:绘制内容

这里调用了View#onDraw方法,View中该方法是一个空实现,因为不同的View有着不同的内容,这需要我们自己去实现,即在自定义View中重写该方法来实现。

Step 3 :绘制背景

如果当前的View是一个ViewGroup类型,那么就需要绘制它的子View,这里调用了dispatchDraw,而View中该方法是空实现,实际是ViewGroup重写了这个方法,那么我们来看看,ViewGroup#dispatchDraw:

protected void dispatchDraw(Canvas canvas) {    boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);    final int childrenCount = mChildrenCount;    final View[] children = mChildren;    int flags = mGroupFlags;    for (int i = 0; i < childrenCount; i++) {        while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {            final View transientChild = mTransientViews.get(transientIndex);            if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||                    transientChild.getAnimation() != null) {                more |= drawChild(canvas, transientChild, drawingTime);            }            transientIndex++;            if (transientIndex >= transientCount) {                transientIndex = -1;            }        }        int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;        final View child = (preorderedList == null)                ? children[childIndex] : preorderedList.get(childIndex);        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {            more |= drawChild(canvas, child, drawingTime);        }    }    //省略...}

源码很长,这里简单说明一下,里面主要遍历了所以子View,每个子View都调用了drawChild这个方法,我们找到这个方法,ViewGroup#drawChild:

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

可以看出,这里调用了View的draw方法,但这个方法并不是上面所说的,因为参数不同,我们来看看这个方法,View#draw:

boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {    //省略...    if (!drawingWithDrawingCache) {        if (drawingWithRenderNode) {            mPrivateFlags &= ~PFLAG_DIRTY_MASK;            ((DisplayListCanvas) canvas).drawRenderNode(renderNode);        } else {            // Fast path for layouts with no backgrounds            if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {                mPrivateFlags &= ~PFLAG_DIRTY_MASK;                dispatchDraw(canvas);            } else {                draw(canvas);            }        }    } else if (cache != null) {        mPrivateFlags &= ~PFLAG_DIRTY_MASK;        if (layerType == LAYER_TYPE_NONE) {            // no layer paint, use temporary paint to draw bitmap            Paint cachePaint = parent.mCachePaint;            if (cachePaint == null) {                cachePaint = new Paint();                cachePaint.setDither(false);                parent.mCachePaint = cachePaint;            }            cachePaint.setAlpha((int) (alpha * 255));            canvas.drawBitmap(cache, 0.0f, 0.0f, cachePaint);        } else {            // use layer paint to draw the bitmap, merging the two alphas, but also restore            int layerPaintAlpha = mLayerPaint.getAlpha();            mLayerPaint.setAlpha((int) (alpha * layerPaintAlpha));            canvas.drawBitmap(cache, 0.0f, 0.0f, mLayerPaint);            mLayerPaint.setAlpha(layerPaintAlpha);        }    }}

我们主要来看核心部分,首先判断是否已经有缓存,即之前是否已经绘制过一次了,如果没有,则会调用draw(canvas)方法,开始正常的绘制,即上面所说的六个步骤,否则利用缓存来显示。
这一步也可以归纳为ViewGroup绘制过程,它对子View进行了绘制,而子View又会调用自身的draw方法来绘制自身,这样不断遍历子View及子View的不断对自身的绘制,从而使得View树完成绘制。

Step 5 :绘制背景

所谓的绘制装饰,就是指View除了背景、内容、子View的其余部分,例如滚动条等,我们看View#onDrawForeground:

public void onDrawForeground(Canvas canvas) {    onDrawScrollIndicators(canvas);    onDrawScrollBars(canvas);    final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;    if (foreground != null) {        if (mForegroundInfo.mBoundsChanged) {            mForegroundInfo.mBoundsChanged = false;            final Rect selfBounds = mForegroundInfo.mSelfBounds;            final Rect overlayBounds = mForegroundInfo.mOverlayBounds;            if (mForegroundInfo.mInsidePadding) {                selfBounds.set(0, 0, getWidth(), getHeight());            } else {                selfBounds.set(getPaddingLeft(), getPaddingTop(),                        getWidth() - getPaddingRight(), getHeight() - getPaddingBottom());            }            final int ld = getLayoutDirection();            Gravity.apply(mForegroundInfo.mGravity, foreground.getIntrinsicWidth(),                    foreground.getIntrinsicHeight(), selfBounds, overlayBounds, ld);            foreground.setBounds(overlayBounds);        }        foreground.draw(canvas);    }}

可以看出,逻辑很清晰,和一般的绘制流程非常相似,都是先设定绘制区域,然后利用canvas进行绘制,这里就不展开详细地说了,有兴趣的可以继续了解下去。

那么,到目前为止,View的绘制流程也讲述完毕了,希望这篇文章对你们起到帮助作用,谢谢你们的阅读。

绘制流程二

performTraversals 方法的下一步就是mView.draw(canvas); 因为View的draw 方法一般不去重写,官网文档也建议不要去重写draw 方法,所以下一步执行就是View.java的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    ...        background.draw(canvas);    ...        // skip step 2 & 5 if possible (common case)    ...        // Step 2, save the canvas' layers    ...        if (solidColor == 0) {            final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;            if (drawTop) {                canvas.saveLayer(left, top, right, top + length, null, flags);            }    ...        // Step 3, draw the content        if (!dirtyOpaque) onDraw(canvas);        // Step 4, draw the children        dispatchDraw(canvas);        // Step 5, draw the fade effect and restore layers        if (drawTop) {            matrix.setScale(1, fadeHeight * topFadeStrength);            matrix.postTranslate(left, top);            fade.setLocalMatrix(matrix);            canvas.drawRect(left, top, right, top + length, p);        }    ...        // Step 6, draw decorations (scrollbars)        onDrawScrollBars(canvas);    }

注释写得比较清楚,一共分成6步,看到注释没有( // skip step 2 & 5 if possible (common case))除了2 和 5之外 我们一步一步来看:

1、第一步:背景绘制

看注释即可,不是重点

private void drawBackground(Canvas canvas) {      Drawable final Drawable background = mBackground;       ......      //mRight - mLeft, mBottom - mTop layout确定的四个点来设置背景的绘制区域      if (mBackgroundSizeChanged) {         background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);           mBackgroundSizeChanged = false; rebuildOutline();      }      ......      //调用Drawable的draw() 把背景图片画到画布上     background.draw(canvas);      ...... }

2、第三步,对View的内容进行绘制。

onDraw(canvas) 方法是view用来draw 自己的,具体如何绘制,颜色线条什么样式就需要子View自己去实现,View.java 的onDraw(canvas) 是空实现,ViewGroup 也没有实现,每个View的内容是各不相同的,所以需要由子类去实现具体逻辑。

3、第4步 对当前View的所有子View进行绘制

dispatchDraw(canvas) 方法是用来绘制子View的,View.java 的dispatchDraw()方法是一个空方法,因为View没有子View,不需要实现dispatchDraw ()方法,ViewGroup就不一样了,它实现了dispatchDraw ()方法:

@Override protected void dispatchDraw(Canvas canvas) {       ...        if ((flags & FLAG_USE_CHILD_DRAWING_ORDER) == 0) {            for (int i = 0; i < count; i++) {                final View child = children[i];                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {                    more |= drawChild(canvas, child, drawingTime);                }            }        } else {            for (int i = 0; i < count; i++) {                final View child = children[getChildDrawingOrder(count, i)];                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {                    more |= drawChild(canvas, child, drawingTime);                }            }        }      ......    }

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

4、第6步 对View的滚动条进行绘制

不是重点,知道有这东西就行,onDrawScrollBars 的一句注释 :Request the drawing of the horizontal and the vertical scrollbar. The scrollbars are painted only if they have been awakened first.

一张图看下整个draw的递归流程。

这里写图片描述

到此整个绘制过程基本讲述完毕了。

绘制流程三

measure和layout的过程都结束后,接下来就进入到draw的过程了。同样,根据名字你就能够判断出,在这里才真正地开始对视图进行绘制。ViewRoot中的代码会继续执行并创建出一个Canvas对象,然后调用View的draw()方法来执行具体的绘制工作。draw()方法内部的绘制过程总共可以分为六步,其中第二步和第五步在一般情况下很少用到,因此这里我们只分析简化后的绘制过程。代码如下所示:

public void draw(Canvas canvas) {    if (ViewDebug.TRACE_HIERARCHY) {        ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);    }    final int privateFlags = mPrivateFlags;    final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE &&            (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);    mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN;    // Step 1, draw the background, if needed    int saveCount;    if (!dirtyOpaque) {        final Drawable background = mBGDrawable;        if (background != null) {            final int scrollX = mScrollX;            final int scrollY = mScrollY;            if (mBackgroundSizeChanged) {                background.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);                mBackgroundSizeChanged = false;            }            if ((scrollX | scrollY) == 0) {                background.draw(canvas);            } else {                canvas.translate(scrollX, scrollY);                background.draw(canvas);                canvas.translate(-scrollX, -scrollY);            }        }    }    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        if (!dirtyOpaque) onDraw(canvas);        // Step 4, draw the children        dispatchDraw(canvas);        // Step 6, draw decorations (scrollbars)        onDrawScrollBars(canvas);        // we're done...        return;    }}

可以看到,第一步是从第9行代码开始的,这一步的作用是对视图的背景进行绘制。这里会先得到一个mBGDrawable对象,然后根据layout过程确定的视图位置来设置背景的绘制区域,之后再调用Drawable的draw()方法来完成背景的绘制工作。那么这个mBGDrawable对象是从哪里来的呢?其实就是在XML中通过android:background属性设置的图片或颜色。当然你也可以在代码中通过setBackgroundColor()、setBackgroundResource()等方法进行赋值。

接下来的第三步是在第34行执行的,这一步的作用是对视图的内容进行绘制。可以看到,这里去调用了一下onDraw()方法,那么onDraw()方法里又写了什么代码呢?进去一看你会发现,原来又是个空方法啊。其实也可以理解,因为每个视图的内容部分肯定都是各不相同的,这部分的功能交给子类来去实现也是理所当然的。

第三步完成之后紧接着会执行第四步,这一步的作用是对当前视图的所有子视图进行绘制。但如果当前的视图没有子视图,那么也就不需要进行绘制了。因此你会发现View中的dispatchDraw()方法又是一个空方法,而ViewGroup的dispatchDraw()方法中就会有具体的绘制代码。

以上都执行完后就会进入到第六步,也是最后一步,这一步的作用是对视图的滚动条进行绘制。那么你可能会奇怪,当前的视图又不一定是ListView或者ScrollView,为什么要绘制滚动条呢?其实不管是Button也好,TextView也好,任何一个视图都是有滚动条的,只是一般情况下我们都没有让它显示出来而已。绘制滚动条的代码逻辑也比较复杂,这里就不再贴出来了,因为我们的重点是第三步过程。

通过以上流程分析,相信大家已经知道,View是不会帮我们绘制内容部分的,因此需要每个视图根据想要展示的内容来自行绘制。如果你去观察TextView、ImageView等类的源码,你会发现它们都有重写onDraw()这个方法,并且在里面执行了相当不少的绘制逻辑。绘制的方式主要是借助Canvas这个类,它会作为参数传入到onDraw()方法中,供给每个视图使用。Canvas这个类的用法非常丰富,基本可以把它当成一块画布,在上面绘制任意的东西,那么我们就来尝试一下吧。

这里简单起见,我只是创建一个非常简单的视图,并且用Canvas随便绘制了一点东西,代码如下所示:

public class MyView extends View {      private Paint mPaint;      public MyView(Context context, AttributeSet attrs) {          super(context, attrs);          mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);      }      @Override      protected void onDraw(Canvas canvas) {          mPaint.setColor(Color.YELLOW);          canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);          mPaint.setColor(Color.BLUE);          mPaint.setTextSize(20);          String text = "Hello View";          canvas.drawText(text, 0, getHeight() / 2, mPaint);      }  }  

可以看到,我们创建了一个自定义的MyView继承自View,并在MyView的构造函数中创建了一个Paint对象。Paint就像是一个画笔一样,配合着Canvas就可以进行绘制了。这里我们的绘制逻辑比较简单,在onDraw()方法中先是把画笔设置成黄色,然后调用Canvas的drawRect()方法绘制一个矩形。然后在把画笔设置成蓝色,并调整了一下文字的大小,然后调用drawText()方法绘制了一段文字。

就这么简单,一个自定义的视图就已经写好了,现在可以在XML中加入这个视图,如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"      android:layout_width="match_parent"      android:layout_height="match_parent" >      <com.example.viewtest.MyView           android:layout_width="200dp"          android:layout_height="100dp"          />  </LinearLayout>  

将MyView的宽度设置成200dp,高度设置成100dp,然后运行一下程序,结果如下图所示:

这里写图片描述

图中显示的内容也正是MyView这个视图的内容部分了。由于我们没给MyView设置背景,因此这里看不出来View自动绘制的背景效果。

当然了Canvas的用法还有很多很多,这里我不可能把Canvas的所有用法都列举出来,剩下的就要靠大家自行去研究和学习了。

到此为止,我们把视图绘制流程的第三阶段也分析完了。整个视图的绘制过程就全部结束了,你现在是不是对View的理解更加深刻了呢?感兴趣的朋友可以继续阅读 Android视图状态及重绘流程分析,带你一步步深入了解View(三) 。

参考

  • Android View 绘制流程(Draw) 完全解析

  • Android View的绘制流程

  • Android视图绘制流程完全解析,带你一步步深入了解View(二)

  • Android中View绘制流程以及invalidate()等相关方法分析

0 0
原创粉丝点击