Android之自定义View的死亡三部曲之(Draw)


前言

  • 大家好!本次我们将继续学习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分析完了,


总结:好吧,直接上一个收集的时序图

123.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容