你须要知道的 Android View 的绘制

通过上一篇AndroidView的布局分析以后,咱们继续View的绘制分析讲解。咱们依旧从ViewRootImpl#performTraversals提及。javascript

private void performTraversals() {
            ...
        if (!cancelDraw && !newSurface) {
            if (!skipDraw || mReportNextDraw) {
                if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
                    for (int i = 0; i < mPendingTransitions.size(); ++i) {
                        mPendingTransitions.get(i).startChangingAnimations();
                    }
                    mPendingTransitions.clear();
                }

                performDraw();
            }
        } 
        ...
}复制代码

咱们对performDraw()执行绘制方法进行分析:java

private void performDraw() {
    ···
    final boolean fullRedrawNeeded = mFullRedrawNeeded;
    try {
        draw(fullRedrawNeeded);
    } finally {
        mIsDrawing = false;
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
    ···
}复制代码

在ViewRootImpl#performDraw里调用了ViewRootImpl#draw方法,并将fullRedrawNeeded形参传进方法体中,咱们经过变量的命名方式大概推断出该成员变量的做用是判断是否须要从新绘制所有视图,由于咱们从DecorView根布局分析至今,咱们显然是绘制全部视图的。那么咱们继续分析ViewRootImpl#drawcanvas

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方法,并把相关参数传递进去。缓存

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进行一系列的赋值,最后调用mView.draw(canvas)方法。从以前的分析能够知道mView指向的就是DecorView。app

View#draw。咱们清楚知道DecorView、FrameLayout、ViewGroup、View之间的继承关系。咱们在FrameLayout里面用方法搜索的时候,搜到的draw(Canvas canvas)是View里面的方法。那就是说,View的子类都是是调用View#draw()方法的。(源码注释中不建议重写draw方法)ide

public class View implements···{
    ···
    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;
        // 绘制背景,只有dirtyOpaque为false时才进行绘制
        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
            // 绘制子View
            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;
            ···
        }
}复制代码

咱们能够看到官方给出的注释是很是清晰地说明每一步的作法。咱们首先来看一开始的标记位dirtyOpaque,这是标记的做用是判断当前View是不是透明的,若是View是透明的,那么根据下面的逻辑能够看出,有一些步骤就不进行。咱们仍是跟着注释来分析一下有哪六个步骤:函数

  1. 对View背景的绘制
  2. 保存当前的图层信息(可跳过)
  3. 绘制View的内容信息
  4. 绘制子View
  5. 绘制阴影的边缘和恢复层(有必要的话)
  6. 绘制装饰(前景、滚动条)
    第二和第五步能够跳过,我这里不做分析。

绘制背景

调用了View#drawBackground方法,咱们看一下源码:布局

private void drawBackground(Canvas canvas) {
        //mBackground是该View的背景参数,好比背景颜色
        final Drawable background = mBackground;
        if (background == null) {
            return;
        }
        //根据View四个布局参数来肯定背景的边界
        setBackgroundBounds();

        // Attempt to use a display list if requested.
        if (canvas.isHardwareAccelerated() && mAttachInfo != null
                && mAttachInfo.mHardwareRenderer != null) {
            mBackgroundRenderNode = getDrawableRenderNode(background, mBackgroundRenderNode);

            final RenderNode renderNode = mBackgroundRenderNode;
            if (renderNode != null && renderNode.isValid()) {
                setBackgroundRenderNodeProperties(renderNode);
                ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
                return;
            }
        }
        //获取当前View的mScrollX和mScrollY值
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        if ((scrollX | scrollY) == 0) {
            background.draw(canvas);
        } else {
            canvas.translate(scrollX, scrollY);
            background.draw(canvas);
            canvas.translate(-scrollX, -scrollY);
        }
    }复制代码

不是重点,咱们记住这个步骤,往下看。动画

绘制View的内容信息

/** * Implement this to do your drawing. * * @param canvas the canvas on which the background will be drawn */
    protected void onDraw(Canvas canvas) {
    }复制代码

View#onDraw是一个空方法,就如咱们上一个篇分析onLayout()同样,因为不一样的View有着不一样的布局,因此在视图的绘制过程也一样有所不一样,这须要咱们View的子类按照本身的需求去重写onDraw()方法来实现。ui

绘制子View

由于是绘制子View,那么咱们能够在View找不到这个方法,在FrameLayout中方法查找是指向ViewGroup#dispatchDraw的方法,因为这个方法实现主要是遍历因此子View,每一个子View调用drawChild。其实ViewGroup#dispatchDraw方法实现知足了咱们不少ViewGroup的子类,如LinearLayout、FrameLayout都是没有重写dispatchDraw方法的,若是咱们自定义View需求比较特殊,能够重写该方法。 另外ViewGroup#dispatchDraw方法代码过多,咱们直接圈出重点ViewGroup#drawChild:

/** * Draw one child of this View Group. This method is responsible for getting * the canvas in the right state. This includes clipping, translating so * that the child's scrolled origin is at 0, 0, and applying any animation * transformations. * * @param canvas The canvas on which to draw the child * @param child Who to draw * @param drawingTime The time at which draw is occurring * @return True if an invalidate() was issued */
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }复制代码

子类View的draw()方法,主要方法的重载,跟咱们上面分析的draw(Canvas canvas)是不同的。

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)方法。而咱们上面分析过,drawChild()的核心过程就是为子视图分配的cavas画布绘制区,在设置了一些位置、动画等参数和一个Flag标记后就会调用子视图的draw()函数进行具体的绘制了。

咱们整体能够这样理解ViewGroup的绘制过程,遍历子View,重载draw方法对子View进行绘制,而子View又会调用自身的draw方法绘制本身,经过不断遍历子View及子View的不断对自身的绘制,从而使得View树完成绘制。(该方法实现过程较为复杂抽象,能力有限在参考下截取这部分重点来讲说,有须要你们能够结合其余分析进行自我总结概括。)

绘制装饰

绘制装饰,其实对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的绘制流程咱们所有说完了,从一开始的View的建立、View的测量、View的布局到这篇View的绘制,里面包含着不少有价值的知识,因为自身水平有限不能面面俱到,主要配合本身对View的绘制流程梳理一遍,但愿对你们也有帮助。谢谢大家的阅读!

相关文章
相关标签/搜索