自定义View最重要的部分是外观。绘制自定义视图可能很简单,也可能很复杂,具体取决于应用的需求。

在自定义view中,onDraw()扮演着非常重要的角色。通过对它的仔细探究就可以知道view是怎么画出来的。

UI绘制流程

绘制流程

在之前了解绘制流程中可以知道ViewRootImpl#performTraversals()方法中调用了performMeasure()performLayout()performDraw()

ViewRootImpl#performTraversals()

private void performTraversals() {
    // ...
    boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
    if (!cancelDraw) {
        // ...
        performDraw();
    } else {
        if (isViewVisible) {
            // Try again
            scheduleTraversals();
        } else if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
            for (int i = 0; i < mPendingTransitions.size(); ++i) {
                mPendingTransitions.get(i).endChangingAnimations();
            }
            mPendingTransitions.clear();
        }
    }
    // ...
}

performDraw()

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

performDraw()中调用了draw(),参数fullRedrawNeeded的作用是判断是否需要重新绘制全部视图,如果是第一次绘制视图,需要全部绘制,如果由于某些原因,导致视图重绘则不需要绘制所有视图。

draw()

private boolean draw(boolean fullRedrawNeeded) {
    // ...
    // mDirty 表示需要绘制视图的区域
    final Rect dirty = mDirty;
    if (mSurfaceHolder != null) {
        // The app owns the surface, we won't draw.
        dirty.setEmpty();
        if (animating && mScroller != null) {
            mScroller.abortAnimation();
        }
        return false;
    }
    // 如果需要全部重新绘制,则绘制区域为整个屏幕
    // 第一次绘制流程需要绘制整个屏幕
    if (fullRedrawNeeded) {
        dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
    }
 
    // ...
    boolean useAsyncReport = false;
    if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
        if (mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled()) {
            // If accessibility focus moved, always invalidate the root.
            boolean invalidateRoot = accessibilityFocusDirty || mInvalidateRootRequested;
            mInvalidateRootRequested = false;
 
            // Draw with hardware renderer.
            mIsAnimating = false;
 
            if (mHardwareYOffset != yOffset || mHardwareXOffset != xOffset) {
                mHardwareYOffset = yOffset;
                mHardwareXOffset = xOffset;
                invalidateRoot = true;
            }
 
            if (invalidateRoot) {
                mAttachInfo.mThreadedRenderer.invalidateRoot();
            }
 
            dirty.setEmpty();
 
            // Stage the content drawn size now. It will be transferred to the renderer
            // shortly before the draw commands get send to the renderer.
            final boolean updated = updateContentDrawBounds();
 
            if (mReportNextDraw) {
                // report next draw overrides setStopped()
                // This value is re-sync'd to the value of mStopped
                // in the handling of mReportNextDraw post-draw.
                mAttachInfo.mThreadedRenderer.setStopped(false);
            }
 
            if (updated) {
                requestDrawWindow();
            }
 
            useAsyncReport = true;
 
            mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
        } else {
            // ...
            if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
                              scalingRequired, dirty, surfaceInsets)) {
                return false;
            }
        }
    }
 
    if (animating) {
        mFullRedrawNeeded = true;
        scheduleTraversals();
    }
    return useAsyncReport;
}

draw()中首先获取了mDirty值,接着根据fullRedrawNeeded的值判断是否需要重置绘制区域,最后调用了drawSoftware()

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区域,区域范围由dirty决定
        canvas = mSurface.lockCanvas(dirty);
        // TODO: Do this in native
        // canvas设置像素密度
        canvas.setDensity(mDensity);
    }
    // ...
    try {
        //...
        // 清除位图格式的alpha通道,以便子控件重新组合其图纸上的透明背景
        // 避免在未出现偏移的情况下空白区域出现垃圾
        if (!canvas.isOpaque() || yoff != 0 || xoff != 0) {
            canvas.drawColor(0, PorterDuff.Mode.CLEAR);
        }
        // ...
        // view调用draw方法
        mView.draw(canvas);
        drawAccessibilityFocusedDrawableIfNeeded(canvas);
    } finally {
        // ...
    }
    return true;
}

上述关键代码可以看出,首先获取Canvas对象,然后锁定该canvas的区域,该区域由dirty决定,接着对canvas进行赋值,最后调用了mView.draw(canvas)方法。

View#draw()

@CallSuper
public void draw(Canvas canvas) {
    final int privateFlags = mPrivateFlags;
    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) (绘制装饰(例如滚动条))
     *   7. If necessary, draw the default focus highlight (如有必要,绘制焦点)
     */
    // Step 1, draw the background, if needed
    int saveCount;
 
    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);
 
        drawAutofilledHighlight(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);
 
        // Step 7, draw the default focus highlight
        drawDefaultFocusHighlight(canvas);
 
        if (isShowingLayoutBounds()) {
            debugDrawFocus(canvas);
        }
 
        // we're done...
        return;
    }
 
    /*
     * Here we do the full fledged routine...
     * (this is an uncommon case where speed matters less,
     * this is why we repeat some of the tests that have been
     * done above)
     */   
    boolean drawTop = false;
    boolean drawBottom = false;
    boolean drawLeft = false;
    boolean drawRight = false;
 
    float topFadeStrength = 0.0f;
    float bottomFadeStrength = 0.0f;
    float leftFadeStrength = 0.0f;
    float rightFadeStrength = 0.0f;
 
    // Step 2, save the canvas' layers
    int paddingLeft = mPaddingLeft;
 
    final boolean offsetRequired = isPaddingOffsetRequired();
    if (offsetRequired) {
        paddingLeft += getLeftPaddingOffset();
    }
 
    int left = mScrollX + paddingLeft;
    int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
    int top = mScrollY + getFadeTop(offsetRequired);
    int bottom = top + getFadeHeight(offsetRequired);
 
    if (offsetRequired) {
        right += getRightPaddingOffset();
        bottom += getBottomPaddingOffset();
    }
 
    final ScrollabilityCache scrollabilityCache = mScrollCache;
    final float fadeHeight = scrollabilityCache.fadingEdgeLength;
    int length = (int) fadeHeight;
 
    // clip the fade length if top and bottom fades overlap
    // overlapping fades produce odd-looking artifacts
    if (verticalEdges && (top + length > bottom - length)) {
        length = (bottom - top) / 2;
    }
 
    // also clip horizontal fades if necessary
    if (horizontalEdges && (left + length > right - length)) {
        length = (right - left) / 2;
    }
 
    if (verticalEdges) {
        topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength()));
        drawTop = topFadeStrength * fadeHeight > 1.0f;
        bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength()));
        drawBottom = bottomFadeStrength * fadeHeight > 1.0f;
    }
 
    if (horizontalEdges) {
        leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength()));
        drawLeft = leftFadeStrength * fadeHeight > 1.0f;
        rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength()));
        drawRight = rightFadeStrength * fadeHeight > 1.0f;
    }
 
    saveCount = canvas.getSaveCount();
    int topSaveCount = -1;
    int bottomSaveCount = -1;
    int leftSaveCount = -1;
    int rightSaveCount = -1;
 
    int solidColor = getSolidColor();
    if (solidColor == 0) {
        if (drawTop) {
            topSaveCount = canvas.saveUnclippedLayer(left, top, right, top + length);
        }
 
        if (drawBottom) {
            bottomSaveCount = canvas.saveUnclippedLayer(left, bottom - length, right, bottom);
        }
 
        if (drawLeft) {
            leftSaveCount = canvas.saveUnclippedLayer(left, top, left + length, bottom);
        }
 
        if (drawRight) {
            rightSaveCount = canvas.saveUnclippedLayer(right - length, top, right, bottom);
        }
    } else {
        scrollabilityCache.setFadeColor(solidColor);
    }
 
    // Step 3, draw the content
    onDraw(canvas);
 
    // Step 4, draw the children
    dispatchDraw(canvas);
 
    // Step 5, draw the fade effect and restore layers
    final Paint p = scrollabilityCache.paint;
    final Matrix matrix = scrollabilityCache.matrix;
    final Shader fade = scrollabilityCache.shader;
 
    // must be restored in the reverse order that they were saved
    if (drawRight) {
        matrix.setScale(1, fadeHeight * rightFadeStrength);
        matrix.postRotate(90);
        matrix.postTranslate(right, top);
        fade.setLocalMatrix(matrix);
        p.setShader(fade);
        if (solidColor == 0) {
            canvas.restoreUnclippedLayer(rightSaveCount, p);
        } else {
            canvas.drawRect(right - length, top, right, bottom, p);
        }
    }
 
    if (drawLeft) {
        matrix.setScale(1, fadeHeight * leftFadeStrength);
        matrix.postRotate(-90);
        matrix.postTranslate(left, top);
        fade.setLocalMatrix(matrix);
        p.setShader(fade);
        if (solidColor == 0) {
            canvas.restoreUnclippedLayer(leftSaveCount, p);
        } else {
            canvas.drawRect(left, top, left + length, bottom, p);
        }
    }
 
    if (drawBottom) {
        matrix.setScale(1, fadeHeight * bottomFadeStrength);
        matrix.postRotate(180);
        matrix.postTranslate(left, bottom);
        fade.setLocalMatrix(matrix);
        p.setShader(fade);
        if (solidColor == 0) {
            canvas.restoreUnclippedLayer(bottomSaveCount, p);
        } else {
            canvas.drawRect(left, bottom - length, right, bottom, p);
        }
    }
 
    if (drawTop) {
        matrix.setScale(1, fadeHeight * topFadeStrength);
        matrix.postTranslate(left, top);
        fade.setLocalMatrix(matrix);
        p.setShader(fade);
        if (solidColor == 0) {
            canvas.restoreUnclippedLayer(topSaveCount, p);
        } else {
            canvas.drawRect(left, top, right, top + length, p);
        }
    }
 
    canvas.restoreToCount(saveCount);
 
    drawAutofilledHighlight(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);
 
    // Step 7, draw the default focus highlight
    drawDefaultFocusHighlight(canvas);
 
    if (isShowingLayoutBounds()) {
        debugDrawFocus(canvas);
    }
}

可以看到,draw过程比较复杂,但是逻辑十分清晰,而官方注释也清楚地说明了每一步的做法。

绘制流程的七个步骤:

  1. 对View的背景进行绘制;
  2. 保存当前的图层信息;( skip step 2 & 5 if possible (common case))
  3. 绘制View的内容;
  4. 对View的子View进行绘制(如果有子View);
  5. 绘制View的褪色的边缘,类似于阴影效果并恢复图层信息;( skip step 2 & 5 if possible (common case))
  6. 绘制View的装饰;
  7. 绘制焦点部分。

drawBackground()

画背景

private void drawBackground(Canvas canvas) {
    // 如果视图没有背景色,则不用绘制
    final Drawable background = mBackground;
    if (background == null) {
        return;
    }
    // 根据布局的四个参数来确定背景的边界
    setBackgroundBounds();
    // ...
    // 获取当前view的scroll值
    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);
    }
}
void setBackgroundBounds() {
    if (mBackgroundSizeChanged && mBackground != null) {
        // 设置背景四个参数
        mBackground.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
        mBackgroundSizeChanged = false;
        rebuildOutline();
    }
}

保存图层和恢复图层暂不考虑。

//skip step 2 & 5 if possible (common case)

onDraw()

protected void onDraw(Canvas canvas) {
}

View中该方法是一个空实现,这里同理于之前的onMeasureonLayout因为不同的View有着不同的内容,这需要我们自己去实现,即在自定义View中重写该方法来实现。

dispatchDraw()

dispatchDraw()在view中是空实现,我们可以看看ViewGroup中的。

@Override
protected void dispatchDraw(Canvas canvas) {
    boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
    final int childrenCount = mChildrenCount;
    final View[] children = mChildren;
    int flags = mGroupFlags;
 
    if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {
        final boolean buildCache = !isHardwareAccelerated();
        for (int i = 0; i < childrenCount; i++) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
                final LayoutParams params = child.getLayoutParams();
                attachLayoutAnimationParameters(child, params, i, childrenCount);
                bindLayoutAnimation(child);
            }
        }
 
        final LayoutAnimationController controller = mLayoutAnimationController;
        if (controller.willOverlap()) {
            mGroupFlags |= FLAG_OPTIMIZE_INVALIDATE;
        }
 
        controller.start();
 
        mGroupFlags &= ~FLAG_RUN_ANIMATION;
        mGroupFlags &= ~FLAG_ANIMATION_DONE;
 
        if (mAnimationListener != null) {
            mAnimationListener.onAnimationStart(controller.getAnimation());
        }
    }
 
    int clipSaveCount = 0;
    final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;
    if (clipToPadding) {
        clipSaveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
        canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop,
                        mScrollX + mRight - mLeft - mPaddingRight,
                        mScrollY + mBottom - mTop - mPaddingBottom);
    }
 
    // We will draw our child's animation, let's reset the flag
    mPrivateFlags &= ~PFLAG_DRAW_ANIMATION;
    mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED;
 
    boolean more = false;
    final long drawingTime = getDrawingTime();
 
    if (usingRenderNodeProperties) canvas.insertReorderBarrier();
    final int transientCount = mTransientIndices == null ? 0 : mTransientIndices.size();
    int transientIndex = transientCount != 0 ? 0 : -1;
    // Only use the preordered list if not HW accelerated, since the HW pipeline will do the
    // draw reordering internally
    final ArrayList<View> preorderedList = usingRenderNodeProperties
        ? null : buildOrderedChildList();
    final boolean customOrder = preorderedList == null
        && isChildrenDrawingOrderEnabled();
    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;
            }
        }
 
        final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
        final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
            more |= drawChild(canvas, child, drawingTime);
        }
    }
    while (transientIndex >= 0) {
        // there may be additional transient views after the normal views
        final View transientChild = mTransientViews.get(transientIndex);
        if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
            transientChild.getAnimation() != null) {
            more |= drawChild(canvas, transientChild, drawingTime);
        }
        transientIndex++;
        if (transientIndex >= transientCount) {
            break;
        }
    }
    if (preorderedList != null) preorderedList.clear();
 
    // Draw any disappearing views that have animations
    if (mDisappearingChildren != null) {
        final ArrayList<View> disappearingChildren = mDisappearingChildren;
        final int disappearingCount = disappearingChildren.size() - 1;
        // Go backwards -- we may delete as animations finish
        for (int i = disappearingCount; i >= 0; i--) {
            final View child = disappearingChildren.get(i);
            more |= drawChild(canvas, child, drawingTime);
        }
    }
    if (usingRenderNodeProperties) canvas.insertInorderBarrier();
 
    if (isShowingLayoutBounds()) {
        onDebugDraw(canvas);
    }
 
    if (clipToPadding) {
        canvas.restoreToCount(clipSaveCount);
    }
 
    // mGroupFlags might have been updated by drawChild()
    flags = mGroupFlags;
 
    if ((flags & FLAG_INVALIDATE_REQUIRED) == FLAG_INVALIDATE_REQUIRED) {
        invalidate(true);
    }
 
    if ((flags & FLAG_ANIMATION_DONE) == 0 && (flags & FLAG_NOTIFY_ANIMATION_LISTENER) == 0 &&
        mLayoutAnimationController.isDone() && !more) {
        // We want to erase the drawing cache and notify the listener after the
        // next frame is drawn because one extra invalidate() is caused by
        // drawChild() after the animation is over
        mGroupFlags |= FLAG_NOTIFY_ANIMATION_LISTENER;
        final Runnable end = new Runnable() {
            @Override
            public void run() {
                notifyAnimationListener();
            }
        };
        post(end);
    }
}

dispatchDraw()里面主要遍历了所以子View,每个子View都调用了drawChild()

drawChild()

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

drawChild中,子view各自调用了draw方法。

其实dispatchDraw同理于我门之前的测量和布局,父亲取得所有子控件开始遍历,调用子控件让子控件自己调用自己的draw开始绘制自己。