Android View 原始碼解析(三) – View的繪製過程

Android架構發表於2019-02-26

Android View 原始碼解析(一) – setContentView

Android View 原始碼解析(二) – LayoutInflater

現在開始分析View的繪製機制

View的測量 佈局 繪製過程

測量之前的事情

View的整個繪製流程是開始於ViewRootImpl類的performTraversals方法(1k行) 根據相關設定來覺得十分要重新執行相關功能

  private void performTraversals() {
    // cache mView since it is used so much below...
    final View host = mView;
    ...
    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
    ...
    //measure
    mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    ...
    //layout
    mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
    ...
    //draw
    mView.draw(canvas);
    ...
  }
複製程式碼
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
      int measureSpec;
      switch (rootDimension) {

      case ViewGroup.LayoutParams.MATCH_PARENT:
          // Window can`t resize. Force root view to be windowSize.
          measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
          break;
      ...
      }
      return measureSpec;
  }
複製程式碼

View 繪製流程圖如下

Android View 原始碼解析(三) – View的繪製過程

measure原始碼分析

結論:

  • measure的過程就是父View向子View遞迴呼叫view.measure方法 (measure中回撥onMeasure方法)的過程

  • measure方法是 final的 只能過載onMeasure方法

  • 最頂層的DocerView的MeasureSpec由ViewRootImpl的getRootMeasureSpec方法提供 LayoutParams的引數為MATCH_PARENT specMode是EXACTLY,specSize為物理螢幕大小

  • 只要是ViewGroup的子類就必須要求LayoutParams繼承子MarginLayoutParams 否則無法使用layout_margin引數

  • View的getMeasuredWidth()和getMeasuredHeight()方法來獲取View測量的寬高,要必須保證這兩個方法在onMeasure流程之後被呼叫才能返回有效值。

View measure過程
/**
 * <p>
 * This is called to find out how big a view should be. The parent supplies constraint information in the width and height parameters.
 * </p>
 *
 * <p>
 * The actual measurement work of a view is performed in
 * {@link #onMeasure(int, int)}, called by this method. Therefore, only
 * {@link #onMeasure(int, int)} can and must be overridden by subclasses.
 * </p>
 *
 *
 * @param widthMeasureSpec Horizontal space requirements as imposed by the
 *        parent
 * @param heightMeasureSpec Vertical space requirements as imposed by the
 *        parent
 *
 * @see #onMeasure(int, int)
 */
 //沒捨得刪這些註釋  感覺重要的事情都說了   為了計算整個View樹的實際大小 設定實際的高和寬 每個子View都是根據父檢視和自身決定實際寬高的 在onMeasure()方法中進行實際測量.傳入widthMeasureSpec和heightMeasureSpec引數來表示了父View的規格 不但傳入了模式 還傳入了size 而對於DecorView來說 傳入的模式一般為EXACTLY模式 size對應螢幕的寬高. 所以說子View的大小是父子View共同決定的
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {

               // measure ourselves, this should set the measured dimension flag back
               onMeasure(widthMeasureSpec, heightMeasureSpec);
   }
複製程式碼

MeasureSpec內部類

MeasureSpec是View的內部類 int型,由高2位規格模式specMode和低30位具體尺寸specSize組成 其中specMode只有三種

  • MeasureSpec.EXACTLY //確定模式,父View希望子View的大小是確定的,由specSize決定;
  • MeasureSpec.AT_MOST //最多模式,父View希望子View的大小最多是specSize指定的值;
  • MeasureSpec.UNSPECIFIED //未指定模式,父View完全依據子View的設計值來決定;

onMeasure()方法

    /**
     * <p>
     * Measure the view and its content to determine the measured width and the
     * measured height. This method is invoked by {@link #measure(int, int)} and
     * should be overridden by subclasses to provide accurate and efficient
     * measurement of their contents.
     * </p>
     *
     * <p>
     * <strong>CONTRACT:</strong> When overriding this method, you
     * <em>must</em> call {@link #setMeasuredDimension(int, int)} to store the
     * measured width and height of this view. Failure to do so will trigger an
     * <code>IllegalStateException</code>, thrown by
     * {@link #measure(int, int)}. Calling the superclass`
     * {@link #onMeasure(int, int)} is a valid use.
     * </p>
     *
     * <p>
     * The base class implementation of measure defaults to the background size,
     * unless a larger size is allowed by the MeasureSpec. Subclasses should
     * override {@link #onMeasure(int, int)} to provide better measurements of
     * their content.
     * </p>
     *
     * <p>
     * If this method is overridden, it is the subclass`s responsibility to make
     * sure the measured height and width are at least the view`s minimum height
     * and width ({@link #getSuggestedMinimumHeight()} and
     * {@link #getSuggestedMinimumWidth()}).
     * </p>
     *
     * @param widthMeasureSpec horizontal space requirements as imposed by the parent.
     *                         The requirements are encoded with
     *                         {@link android.view.View.MeasureSpec}.
     * @param heightMeasureSpec vertical space requirements as imposed by the parent.
     *                         The requirements are encoded with
     *                         {@link android.view.View.MeasureSpec}.
     *
     * @see #getMeasuredWidth()
     * @see #getMeasuredHeight()
     * @see #setMeasuredDimension(int, int)
     * @see #getSuggestedMinimumHeight()
     * @see #getSuggestedMinimumWidth()
     * @see android.view.View.MeasureSpec#getMode(int)
     * @see android.view.View.MeasureSpec#getSize(int)
     */
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
複製程式碼

getDefaultSize方法相關

  public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    //通過measureSpec得到mode和size
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
  }

  //最小寬度和高度由View的Background尺寸和View的minXXX共同決定
  protected int getSuggestedMinimumHeight() {
      return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());

  }
  protected int getSuggestedMinimumWidth() {
      return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
  }
複製程式碼

setMeasuredDimension方法 對View的成員變數measuredWidth和measuredHeight變數賦值 也就是說該方法最終決定了View的大小

  protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
      boolean optical = isLayoutModeOptical(this);
      if (optical != isLayoutModeOptical(mParent)) {
          Insets insets = getOpticalInsets();
          int opticalWidth  = insets.left + insets.right;
          int opticalHeight = insets.top  + insets.bottom;

          measuredWidth  += optical ? opticalWidth  : -opticalWidth;
          measuredHeight += optical ? opticalHeight : -opticalHeight;
      }
      setMeasuredDimensionRaw(measuredWidth, measuredHeight);
  }

  public boolean isLayoutRequested() {
    return (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
  }

  private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
    mMeasuredWidth = measuredWidth;
    mMeasuredHeight = measuredHeight;

    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
  }
複製程式碼

至此一次最基礎的View的measure過程就完成了 但是由於View可以巢狀 所以measure是遞迴傳遞的所以ViewGroup中需要對其子類進行measure過程 measureChildren方法實質為迴圈呼叫measureChild方法

而measureChild和measureChildWithMargins的區別是後者將margin和padding也作為了子檢視的大小

一下分析measureChildWithMargins方法

  protected void measureChildWithMargins(View child,
          int parentWidthMeasureSpec, int widthUsed,
          int parentHeightMeasureSpec, int heightUsed) {
      //獲取當前子檢視的LayoutParams
      final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
      //設定子View的測量規格
      final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
              mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                      + widthUsed, lp.width);
      final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
              mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                      + heightUsed, lp.height);
      //子view的繼續呼叫
      child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
  }

  //在getChildMeasureSpec中通過父View和本身的模式共同決定當前View的size
  public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        //獲取當前父View的mode和size
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
        //獲取父View的的剩餘大小
        int size = Math.max(0, specSize - padding);
        //定義結果變數
        int resultSize = 0;
        int resultMode = 0;
        //根據對應的mode做處理
        //通過父View和本身的模式共同決定當前View的size
        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can`t be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can`t be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //將size和mode整合為MeasureSpec模式後返回
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
複製程式碼

layout原始碼分析

View layout整體流程與measure過程基本一樣

結論:

  • 需要根據ViewGroup本身的情況討論 LinearLayout下會更看重子View的height和width 來安排對應位置 而RelativeLayout則更加關注子View的left right top bottom值 並且優先順序高於width和height 甚至在部分自定義ViewGroup中 measure可能是無用的 直接使用layout方法來設定子View的位置也可以
  • ViewGroup需要實現自己的layout邏輯
  • layout_XXX中的各個熟悉都是針對子View的父ViewGroup的
  • 同樣使用View的getWidth()和getHeight()方法來獲取View測量的寬高 必須保證這兩個方法在onLayout流程之後被呼叫才能返回有效值
  /**
     * Assign a size and position to a view and all of its
     * descendants
     *
     * <p>This is the second phase of the layout mechanism.
     * (The first is measuring). In this phase, each parent calls
     * layout on all of its children to position them.
     * This is typically done using the child measurements
     * that were stored in the measure pass().</p>
     *
     * <p>Derived classes should not override this method.
     * Derived classes with children should override
     * onLayout. In that method, they should
     * call layout on each of their children.</p>
     *
     * @param l Left position, relative to parent
     * @param t Top position, relative to parent
     * @param r Right position, relative to parent
     * @param b Bottom position, relative to parent
     */

     //同樣註解寫的很好了  分派給他和他的所有的子檢視大小和位置
    @SuppressWarnings({"unchecked"})
    public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }
         //呼叫setFrame方法把引數分別賦值於
        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;
        //判斷view的位置是否發生過變化 , 確定是否對當前view重新layout
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            //呼叫onLayout
            onLayout(changed, l, t, r, b);
            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i < numListeners; ++i) {
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    }
複製程式碼

onLyayout方法

  View中
  protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
  }
  ViewGroup中
  protected abstract void onLayout(boolean changed,
        int l, int t, int r, int b);
複製程式碼

均是空方法 後面會就LinearLayout和RelativeLayout原始碼進行分析

draw原始碼分析

View的draw流程圖如下

View draw流程

結論:

  • View需要在子類中實現onDraw的過程
  • 在ViewGroup中 會呼叫其子View的方法 順序與子view的新增順序一致

draw的原始碼也很長 但是官方也給出給出了draw的過程

  public void draw(Canvas canvas) {
     ...
     /*
      * 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
     ...
     if (!dirtyOpaque) {
         drawBackground(canvas);
     }

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

     // Step 2, save the canvas` layers
     ...
         if (drawTop) {
             canvas.saveLayer(left, top, right, top + length, null, flags);
         }
     ...

     // 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
     ...
     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);
     }
     ...

     // Step 6, draw decorations (scrollbars)
     onDrawScrollBars(canvas);
     ...
 }
複製程式碼

Step 1, draw the background, if needed

  // Step 1, draw the background, if needed
  //如果需要的話繪製背景

  if (!dirtyOpaque) {
      drawBackground(canvas);
  }
複製程式碼
  private void drawBackground(Canvas canvas) {

        //通過xml中屬性background或者程式碼中setBackGroundColorsetBackgroundResource等方法賦值的背景drawable
        final Drawable background = mBackground;
        if (background == null) {
            return;
        }

        //根據layout中確定的view位置來設定背景的繪製區域
        setBackgroundBounds();

        // 如果需要的話使用顯示列表
        //canvas.isHardwareAccelerated() 硬體加速判定
        //硬體加速時會將圖層快取到GPU上 而不是重繪View的每一層
        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;
            }
        }

        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        //呼叫Drawable的draw方法來完成背景的繪製工作
        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();
    }
  }
複製程式碼

Step 2, save the canvas` layers

  // Step 2, save the canvas` layers
  //儲存繪製圖層

         if (drawTop) {
             canvas.saveLayer(left, top, right, top + length, null, flags);
         }
複製程式碼

Step 3, draw the content

  // Step 3, draw the content
  //對View的內容進行繪製
  if (!dirtyOpaque) onDraw(canvas);
複製程式碼
  /**
  * Implement this to do your drawing.
  *
  * @param canvas the canvas on which the background will be drawn
  */
  //onDraw也是空方法需要子類根據自身去實現相應的
  protected void onDraw(Canvas canvas) {
  }
複製程式碼

Step 4, draw the children

  // Step 4, draw the children
  //繪製其子View
  dispatchDraw(canvas);
複製程式碼
  /**
   * Called by draw to draw the child views. This may be overridden
   * by derived classes to gain control just before its children are drawn
   * (but after its own view has been drawn).
   * @param canvas the canvas on which to draw the view
   */
  protected void dispatchDraw(Canvas canvas) {
  //dispatchDraw同樣空方法 與onDraw不同的是dispatchDraw在ViewGroup中被重寫
  }
複製程式碼

ViewGroup

  //dispatchDraw方法中根據子View的不同情況 包括但不只包括該View是否顯示 是否有進入或消失動畫等進行了部分的調整
  protected void dispatchDraw(Canvas canvas) {
      ...
        more |= drawChild(canvas, transientChild, drawingTime);
      ...    
  }

  protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    return child.draw(canvas, this, drawingTime);
  }
複製程式碼

Step 5, draw the fade effect and restore layers

  // Step 5, draw the fade effect and restore layers
  //繪製過度效果和恢復圖層
  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);
  }
複製程式碼

Step 6, draw decorations (scrollbars)

  // Step 6, draw decorations (scrollbars)
  //對滾動條進行繪製
  onDrawScrollBars(canvas);
複製程式碼

至此 View的繪製過程全部分析完了

相關文章