高级UI4-UI绘制流程(Draw绘制)
前言
从前面的几篇文章我们能够知道UI的测量和布局摆放,那么这次我们来关注下UI是怎么具体画出来的,首先要了解的是具体绘制的流程以及Canvas和Paint在这中间所扮演的角色。
绘制流程
从ViewRootImpl的performTraversals方法开始入手:
// Remember if we must report the next draw.
if ((relayoutResult & WindowManagerGlobal.RELAYOUT_RES_FIRST_TIME) != 0) {
reportNextDraw();
}
boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
if (!cancelDraw && !newSurface) {
if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).startChangingAnimations();
}
mPendingTransitions.clear();
}
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();
}
}
mIsInTraversal = false;
在上面的代码我们可以看到一个关键点就是在isViewVisble=true(也就是view为显示的状态下,这里会再次发起一次scheduleTraversals,所以,这也是为什么onMeasure方法会调用两次的原因Android7.0以上版本)
接着进入performDraw方法:
private void performDraw() {
if (mAttachInfo.mDisplayState == Display.STATE_OFF && !mReportNextDraw) {
return;
} else if (mView == null) {
return;
}
final boolean fullRedrawNeeded = mFullRedrawNeeded;
mFullRedrawNeeded = false;
mIsDrawing = true;
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");
try {
draw(fullRedrawNeeded);
} finally {
mIsDrawing = false;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
// For whatever reason we didn't create a HardwareRenderer, end any
// hardware animations that are now dangling
if (mAttachInfo.mPendingAnimatingRenderNodes != null) {
final int count = mAttachInfo.mPendingAnimatingRenderNodes.size();
for (int i = 0; i < count; i++) {
mAttachInfo.mPendingAnimatingRenderNodes.get(i).endAllAnimators();
}
mAttachInfo.mPendingAnimatingRenderNodes.clear();
}
if (mReportNextDraw) {
mReportNextDraw = false;
// if we're using multi-thread renderer, wait for the window frame draws
if (mWindowDrawCountDown != null) {
try {
mWindowDrawCountDown.await();
} catch (InterruptedException e) {
Log.e(mTag, "Window redraw count down interruped!");
}
mWindowDrawCountDown = null;
}
if (mAttachInfo.mThreadedRenderer != null) {
mAttachInfo.mThreadedRenderer.fence();
mAttachInfo.mThreadedRenderer.setStopped(mStopped);
}
if (LOCAL_LOGV) {
Log.v(mTag, "FINISHED DRAWING: " + mWindowAttributes.getTitle());
}
if (mSurfaceHolder != null && mSurface.isValid()) {
SurfaceCallbackHelper sch = new SurfaceCallbackHelper(this::postDrawFinished);
SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks();
sch.dispatchSurfaceRedrawNeededAsync(mSurfaceHolder, callbacks);
} else {
pendingDrawFinished();
}
}
}
再接着看draw方法:
private void draw(boolean fullRedrawNeeded) {
Surface surface = mSurface;
if (!surface.isValid()) {
return;
}
if (DEBUG_FPS) {
trackFPS();
}
if (!sFirstDrawComplete) {
synchronized (sFirstDrawHandlers) {
sFirstDrawComplete = true;
final int count = sFirstDrawHandlers.size();
for (int i = 0; i< count; i++) {
mHandler.post(sFirstDrawHandlers.get(i));
}
}
}
scrollToRectOrFocus(null, false);
if (mAttachInfo.mViewScrollChanged) {
mAttachInfo.mViewScrollChanged = false;
mAttachInfo.mTreeObserver.dispatchOnScrollChanged();
}
boolean animating = mScroller != null && mScroller.computeScrollOffset();
final int curScrollY;
if (animating) {
curScrollY = mScroller.getCurrY();
} else {
curScrollY = mScrollY;
}
if (mCurScrollY != curScrollY) {
mCurScrollY = curScrollY;
fullRedrawNeeded = true;
if (mView instanceof RootViewSurfaceTaker) {
((RootViewSurfaceTaker) mView).onRootViewScrollYChanged(mCurScrollY);
}
}
final float appScale = mAttachInfo.mApplicationScale;
final boolean scalingRequired = mAttachInfo.mScalingRequired;
int resizeAlpha = 0;
//获取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;
}
//如果fullRedrawNeeded为真,则把dirty区域置为整个屏幕,表示整个视图都需要绘制
//第一次绘制流程,需要绘制所有视图
if (fullRedrawNeeded) {
mAttachInfo.mIgnoreDirtyState = true;
dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
}
if (DEBUG_ORIENTATION || DEBUG_DRAW) {
Log.v(mTag, "Draw " + mView + "/"
+ mWindowAttributes.getTitle()
+ ": dirty={" + dirty.left + "," + dirty.top
+ "," + dirty.right + "," + dirty.bottom + "} surface="
+ surface + " surface.isValid()=" + surface.isValid() + ", appScale:" +
appScale + ", width=" + mWidth + ", height=" + mHeight);
}
mAttachInfo.mTreeObserver.dispatchOnDraw();
int xOffset = -mCanvasOffsetX;
int yOffset = -mCanvasOffsetY + curScrollY;
final WindowManager.LayoutParams params = mWindowAttributes;
final Rect surfaceInsets = params != null ? params.surfaceInsets : null;
if (surfaceInsets != null) {
xOffset -= surfaceInsets.left;
yOffset -= surfaceInsets.top;
// Offset dirty rect for surface insets.
dirty.offset(surfaceInsets.left, surfaceInsets.right);
}
boolean accessibilityFocusDirty = false;
final Drawable drawable = mAttachInfo.mAccessibilityFocusDrawable;
if (drawable != null) {
final Rect bounds = mAttachInfo.mTmpInvalRect;
final boolean hasFocus = getAccessibilityFocusedRect(bounds);
if (!hasFocus) {
bounds.setEmpty();
}
if (!bounds.equals(drawable.getBounds())) {
accessibilityFocusDirty = true;
}
}
mAttachInfo.mDrawingTime =
mChoreographer.getFrameTimeNanos() / TimeUtils.NANOS_PER_MS;
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();
}
mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
} else {
// If we get here with a disabled & requested hardware renderer, something went
// wrong (an invalidate posted right before we destroyed the hardware surface
// for instance) so we should just bail out. Locking the surface with software
// rendering at this point would lock it forever and prevent hardware renderer
// from doing its job when it comes back.
// Before we request a new frame we must however attempt to reinitiliaze the
// hardware renderer if it's in requested state. This would happen after an
// eglTerminate() for instance.
if (mAttachInfo.mThreadedRenderer != null &&
!mAttachInfo.mThreadedRenderer.isEnabled() &&
mAttachInfo.mThreadedRenderer.isRequested()) {
try {
mAttachInfo.mThreadedRenderer.initializeIfNeeded(
mWidth, mHeight, mAttachInfo, mSurface, surfaceInsets);
} catch (OutOfResourcesException e) {
handleOutOfResourcesException(e);
return;
}
mFullRedrawNeeded = true;
scheduleTraversals();
return;
}
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
return;
}
}
}
if (animating) {
mFullRedrawNeeded = true;
scheduleTraversals();
}
}
上面代码中先获取了mDirty值,这里保存了需要重绘的区域的信息,最后调用了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;
}
// TODO: Do this in native (在本地设置密度)
canvas.setDensity(mDensity);
} catch (Surface.OutOfResourcesException e) {
handleOutOfResourcesException(e);
return false;
} catch (IllegalArgumentException e) {
Log.e(mTag, "Could not lock surface", e);
// Don't assume this is due to out of memory, it could be
// something else, and if it is something else then we could
// kill stuff (or ourself) for no reason.
mLayoutRequested = true; // ask wm for a new surface next time.
return false;
}
try {
if (DEBUG_ORIENTATION || DEBUG_DRAW) {
Log.v(mTag, "Surface " + surface + " drawing to bitmap w="
+ canvas.getWidth() + ", h=" + canvas.getHeight());
//canvas.drawARGB(255, 255, 0, 0);
}
// If this bitmap's format includes an alpha channel, we
// need to clear it before drawing so that the child will
// properly re-composite its drawing on a transparent
// background. This automatically respects the clip/dirty region
// or
// If we are applying an offset, we need to clear the area
// where the offset doesn't appear to avoid having garbage
// left in the blank areas.
对于上面的翻译是:
如果位图的格式包含alpha通道,我们
需要在画之前清除它,以便子控件重新组合其图纸上的透明背景
这将自动地对剪辑区域进行关联。
或
如果我们申请抵销,我们需要清理这个地区。
在没有出现偏移的情况下避免垃圾
留在空白区域。
if (!canvas.isOpaque() || yoff != 0 || xoff != 0) {
canvas.drawColor(0, PorterDuff.Mode.CLEAR);
}
dirty.setEmpty();
mIsAnimating = false;
mView.mPrivateFlags |= View.PFLAG_DRAWN;
if (DEBUG_DRAW) {
Context cxt = mView.getContext();
Log.i(mTag, "Drawing: package:" + cxt.getPackageName() +
", metrics=" + cxt.getResources().getDisplayMetrics() +
", compatibilityInfo=" + cxt.getResources().getCompatibilityInfo());
}
try {
canvas.translate(-xoff, -yoff);
if (mTranslator != null) {
mTranslator.translateCanvas(canvas);
}
canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);
attachInfo.mSetIgnoreDirtyState = false;
mView.draw(canvas);
drawAccessibilityFocusedDrawableIfNeeded(canvas);
} finally {
if (!attachInfo.mSetIgnoreDirtyState) {
// Only clear the flag if it was not set during the mView.draw() call
attachInfo.mIgnoreDirtyState = false;
}
}
} finally {
try {
surface.unlockCanvasAndPost(canvas);
} catch (IllegalArgumentException e) {
Log.e(mTag, "Could not unlock surface", e);
mLayoutRequested = true; // ask wm for a new surface next time.
//noinspection ReturnInsideFinallyBlock
return false;
}
if (LOCAL_LOGV) {
Log.v(mTag, "Surface " + surface + " unlockCanvasAndPost");
}
}
return true;
}
上面的代码可以看出,首先实例化了Canvas对象,最后锁定该canvas的区域,由dirty区域决定,接着对canvas进行一系列的属性赋值,最后调用了mView.draw(canvas)方法,那么之前就讲过这里的mView就是我们的DectorView,所以是从DectorView顶层开始绘制,那么之前的一切都是在进行准备一块画板,具体的绘制是在mView.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(画子view)
* 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);
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 (debugDraw()) {
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 solidColor = getSolidColor();
if (solidColor == 0) {
final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;
if (drawTop) {
canvas.saveLayer(left, top, right, top + length, null, flags);
}
if (drawBottom) {
canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
}
if (drawLeft) {
canvas.saveLayer(left, top, left + length, bottom, null, flags);
}
if (drawRight) {
canvas.saveLayer(right - length, top, right, bottom, null, flags);
}
} else {
scrollabilityCache.setFadeColor(solidColor);
}
// 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
final Paint p = scrollabilityCache.paint;
final Matrix matrix = scrollabilityCache.matrix;
final Shader fade = scrollabilityCache.shader;
if (drawTop) {
matrix.setScale(1, fadeHeight * topFadeStrength);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, top, right, top + length, p);
}
if (drawBottom) {
matrix.setScale(1, fadeHeight * bottomFadeStrength);
matrix.postRotate(180);
matrix.postTranslate(left, bottom);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, bottom - length, right, bottom, p);
}
if (drawLeft) {
matrix.setScale(1, fadeHeight * leftFadeStrength);
matrix.postRotate(-90);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, top, left + length, bottom, p);
}
if (drawRight) {
matrix.setScale(1, fadeHeight * rightFadeStrength);
matrix.postRotate(90);
matrix.postTranslate(right, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(right - length, top, right, bottom, 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);
if (debugDraw()) {
debugDrawFocus(canvas);
}
}
上面的代码可以看到,draw过程比较复杂,但是逻辑十分清晰,官方注释也清楚的说明每一步的做法。我们首先来看一开始的标记位dirtyOpaque,该标记位的作用是判断当前View是否是透明的,如果View是透明的,那么根据下面的逻辑可以看出,将不会执行一些步骤,比如绘制背景、绘制内容等。这样很容易理解,因为一个View既然是透明的,那么就没必要绘制它了,接着是绘制流程的六个步骤:
1.对View 的背景进行绘制;
2.保存当前的图层信息;
3.绘制View的内容;
4.对View的子View进行绘制(如果有子View);
5.绘制View的褪色的边缘,类似于阴影效果;
6.绘制View的装饰。
我们只要关注3和4这两点,通过源码发现,View也是暴露了onDraw方法让子类去实现。
由此同理与我们之前的测量和摆放,父控件取得所有子控件开始遍历,调用子控件让子控件自己调用自己的draw开始绘制自己。
逻辑很清晰,都是先设定绘制区域,然后利用canvas进行绘制。
那么,到目前为止,view的绘制流程也讲述完毕了。
其实测量、布局摆放、绘制这三个流程都是调用到onMeasure/onLayout/onDraw让子控件自己去完成的,只不过系统组件(FrameLayout或者ViewGroup)它帮我们按照规则实现了自己本身的效果,那么我们也同样可以根据顺序、原理去施加自己的业务,完成自己想要的自定义控件。