- 文章独家授权公众号:码个蛋
- 更多分享:http://www.cherylgood.cn
前言
-
大家好!本次我们将继续学习 Android 之自定义 View 的死亡三部曲中的最后一部(Draw):画出最真实的自己
-
在此之前,我们在 Android 之自定义 View 的死亡三部曲之(Measure) 中分析了 View 测测量过程,获得了 View 的三围数据-测量后获得高和宽,在 Android 之自定义 View 的死亡三部曲之(Layout) 中分析了 View 的测量过程,经过测量后,我们就能拿到 View 的 left、top、right、bottom 四个点的值。那么我们剩下最后一步,将我的的 View 绘制出来。
-
Ok,这次我们依然是以 ViewRootImpl 的 performTraversals 方法起点。
private void performTraversals() { ... if (!mStopped) { //1、获取顶层布局的childWidthMeasureSpec int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); //2、获取顶层布局的childHeightMeasureSpec int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); //3、测量开始测量 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); } } if (didLayout) { //4、执行布局方法 performLayout(lp, desiredWindowWidth, desiredWindowHeight); ... } if (!cancelDraw && !newSurface) { ... //5、开始绘制了哦 performDraw(); } } ... }
-
这次我们分析到 performDraw 方法了。好的,我们一起来看下 performDraw 里面的代码吧,我只保留来于本次分析相关的关键代码。
private void performDraw() { ...... //1、fullRedrawNeeded这个变量标识了本次绘制是否需要完全重新绘制 final boolean fullRedrawNeeded = mFullRedrawNeeded; try { //2、此处调用了ViewRootImpl的draw方法 draw(fullRedrawNeeded); } finally { mIsDrawing = false; Trace.traceEnd(Trace.TRACE_TAG_VIEW); } ...... }
-
看 1 处,既然有完全绘制,当然也会有局部绘制了,这样做是为了提高性能
-
OK,我们看下 draw 这个方法里面的代码
private void draw(boolean fullRedrawNeeded) { ...... //1、获得dirty,也就是我们要绘制的区域 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; } //2、判断是否需要完全绘制 if (fullRedrawNeeded) { mAttachInfo.mIgnoreDirtyState = true; //3、需要完全绘制时,将dirty的值设置为神歌屏幕的大小 dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f)); } ...... //3、调用drawSoftware进行绘制 if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) { return; } }
-
从上面的代码分析中,我们看到,最后时通过调用 drawSoftware 进行绘制,那么我们看下 drawSoftware 方法的代码
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, boolean scalingRequired, Rect dirty) { //1、哈哈,看到了canvas,是不是感觉里绘制越来越近了 final Canvas canvas; try { //2、取出绘制区域的四个位置的值 final int left = dirty.left; final int top = dirty.top; final int right = dirty.right; final int bottom = dirty.bottom; //3、传入我们的绘制区域,创建一个被锁定了绘制区域的canvas 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; } //4、设置画布的密度 canvas.setDensity(mDensity); } try { if (!canvas.isOpaque() || yoff != 0 || xoff != 0) { //5、清除画布的颜色 canvas.drawColor(0, PorterDuff.Mode.CLEAR); } dirty.setEmpty(); mIsAnimating = false; attachInfo.mDrawingTime = SystemClock.uptimeMillis(); mView.mPrivateFlags |= View.PFLAG_DRAWN; try { //7、设置画布的偏离值 canvas.translate(-xoff, -yoff); if (mTranslator != null) { mTranslator.translateCanvas(canvas); } canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0); attachInfo.mSetIgnoreDirtyState = false; //8、调用mView大的draw方法开始绘制 mView.draw(canvas); } } return true; }
-
Ok,我们分析到第 8 步知道,最终调用了 mView 的 draw 开始绘制了,而 mView 也就是 DecorView,我们前面分析过 DecorView 是一个 FrameLayout,而 FrameLayout 并没有重现 draw 方法,ViewGroup 也没有重写,所以,我们直接看 View 的 draw 方法,代码有点长,但是思路分清晰,官方给出的解释也是非常清晰的
@CallSuper public void draw(Canvas canvas) { final int privateFlags = mPrivateFlags; //(1)、dirtyOpaque标识了当前View是否时透明的 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) */ //上面的解释大知识,绘制过程中有一系列的步骤,但是有几个是必须要执行的 //1、绘制背景2、如果有需要,在可以先保存当前canvas的层级数据,3、绘制View的内容4、绘制View的子类5、如果又需要,在退出此次绘制时恢复之前的canvas的层级数据 //6、绘制一些装饰的效果 // Step 1, draw the background, if needed int saveCount; //(2)、透明时不需要绘制背景 if (!dirtyOpaque) { //(3)、不透明时,绘制背景 drawBackground(canvas); } //他说可以跳过第2步和第5步,说明第2和第5步时很重要 // 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 //(4)、如果不透明,绘制View的内容 if (!dirtyOpaque) onDraw(canvas); // Step 4, draw the children //(5)将canvas传递给childView,将绘制事件传递下去 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; } ...... }
-
OK,第 2 步和第 5 步是保存 canves 状态和恢复的操作,我们这次就分析其他步骤就好
-
首先,我们看第 1 步,在 View 非透明情况下,执行背景的绘制操作
private void drawBackground(Canvas canvas) { final Drawable background = mBackground; //1、背景为null当然是直接返回了 if (background == null) { return; } //2、确认背景的边界值 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; } } //3、获取当前的scrollX和scrollY的值 final int scrollX = mScrollX; final int scrollY = mScrollY; if ((scrollX | scrollY) == 0) { //此时没有滚动,开始绘制背景 background.draw(canvas); } else { //正在滚动,移动canvas后绘制 canvas.translate(scrollX, scrollY); background.draw(canvas); canvas.translate(-scrollX, -scrollY); } }
-
从上面的分析,我有又个意外的发现,当 scrllX 或者 scrollY 的值不为 0 时,先使 canvas 偏移后在绘制,这就是为什么如果我们是使用 Scoller 来实现 View 的滑动时,实际上移动的是 View 的可视区域,而不是 View 本身
-
我们看下 setBackgroundBounds 里面是如何确认背景边界的
void setBackgroundBounds() { if (mBackgroundSizeChanged && mBackground != null) { //1、直接根据layout中获得的四个位置的值直接确定 mBackground.setBounds(0, 0, mRight - mLeft, mBottom - mTop); mBackgroundSizeChanged = false; rebuildOutline(); } }
-
介绍完绘制背景,我们接下来分析绘制内容部分,我们看 onDraw 方法,没错,又是空的,因为这是我们在自定义 View 的时候需要自己去实现的
protected void onDraw(Canvas canvas) { }
-
OK,那我们看下一步,传递绘制事件给 child 们
-
我们先看 View 的 dispatchDraw,没错,还是空的,View 就是最原始的了,哪里有 child 嘛。
protected void dispatchDraw(Canvas canvas) { }
-
那么我们来看 ViewGroup 中的吧,源码优点长,我保留于本次分析相关就好
@Override protected void dispatchDraw(Canvas canvas) { boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode); //1、获取child的数据 final int childrenCount = mChildrenCount; final View[] children = mChildren; int flags = mGroupFlags; ...... for (int i = 0; i < childrenCount; i++) { ...... final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder); final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex); if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { //2、调用drawChild传递canvas、child进去绘制child more |= drawChild(canvas, child, drawingTime); } } ...... }
-
ok,重点是 drawChild 这个方法,我们看下 drawChild 里面做什么操作
protected boolean drawChild(Canvas canvas, View child, long drawingTime) { return child.draw(canvas, this, drawingTime); }
-
里面直接调用了 child 的 draw 方法。不过这个方法跟我们前面的分析的 draw 有点区别哦,没错,参数个数不同,那么我们看下到底却别在哪呢,这个方法的代码很长,我截取关键代码
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) { ...... if (!drawingWithDrawingCache) { if (drawingWithRenderNode) { mPrivateFlags &= ~PFLAG_DIRTY_MASK; ((DisplayListCanvas) canvas).drawRenderNode(renderNode); } else { if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) { mPrivateFlags &= ~PFLAG_DIRTY_MASK; dispatchDraw(canvas); } else { // 1、这里调用子View的draw方法,并将调整好的canvas传进去 draw(canvas); } } } else if (cache != null) // 2、如果是cache模式,则利用cache mPrivateFlags &= ~PFLAG_DIRTY_MASK; if (layerType == LAYER_TYPE_NONE) { 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 { int layerPaintAlpha = mLayerPaint.getAlpha(); mLayerPaint.setAlpha((int) (alpha * layerPaintAlpha)); canvas.drawBitmap(cache, 0.0f, 0.0f, mLayerPaint); mLayerPaint.setAlpha(layerPaintAlpha); } } ...... }
-
上面主要做的事情就是,如果有 cache,就利用 cache 进行绘制,没有则直接调用 View 的 draw 方法。然后根据前面的分析,最终会调用个个 View 的 onDraw 进行绘制操作
-
接下来到了第六部,绘制装饰物(例如 recyclerView 的滚动条),OK,我们来看下 onDrawForeground 方法
public void onDrawForeground(Canvas canvas) { //1、绘制滚动指示器 onDrawScrollIndicators(canvas); //2、绘制滚动条 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 foreground.draw(canvas); } }
-
通过以上的分析,我们就把 View 的 Draw 分析完了,
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于