View的繪製流程

View的繪製流程主要包括measure,layout,draw三大流程,measure用來確定view的測量寬/高,layout用來確定view的最終寬/高和四個頂點的位置,而draw則將View繪製到屏幕上


Measure

  1. 如果只是一個原始的View,那麼通過meaure方法就完成了其測量過程,如果是一個ViewGroup,除了完成自己的測量過程外,還會遍歷去調用所有子View的measure方法,各個子元素再去遞歸調用這個流程

  2. view的measure過程由measure方法來完成,measure方法是一個final方法,這意味着子類不能重寫此方法,在View的measure方法中回去調用view的onMeasure方法

  3. onMeasure方法如下(直接繼承原始View)

    /**
     * <p>
     * 測量視圖及其內容以確定測量的寬度和測量的高度,這個方法由measure方法調用,並且應該被子類重寫,以
     * 準確和有效的測量其內容
     * 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>
     * 當子類重寫了此方法,則必須調用setMeasuredDimension方法來存儲view的測量寬/高,如果不這樣做將會觸發由
     *measure方法拋出的異常throw new IllegalStateException("View with id " + getId() + ": "
                            + getClass().getName() + "#onMeasure() did not set the"
                            + " measured dimension by calling"
                            + " setMeasuredDimension()");
     * <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() 取決於背景尺寸和minWidth的二者中的較大值minWidth默認爲0
     * @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));
    }
    

    onMeasure方法中必須調用setMeasuredDimension方法,否則將拋出異常,在這裏我們只需關注getDefaultSize方法

     /**
      * 一個用來返回默認大小的實用方法,如果MeasureSpec沒有提供任何限制則用系統提供的尺寸(背景尺寸或者minWidth
      * 的尺寸取二者最大);如果MeasureSpec允許,尺寸將會變大
      * Utility to return a default size. Uses the supplied size if the
      * MeasureSpec imposed no constraints. Will get larger if allowed
      * by the MeasureSpec.
      *
      * @param size Default size for this view
      * @param measureSpec Constraints imposed by the parent
      * @return The size this view should be.
      */
     public static int getDefaultSize(int size, int measureSpec) {
         int result = 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;
    

    可以看出getDefaultSize的邏輯很簡單,我們只需要看AT_MOSTEXACTLY這兩種情況,返回的大小就是MeasureSpec中的specSize,也就是View測量後的大小,直接繼承view的自定義控件需要重寫onMeasure方法並設置WRAP_CONTENT時自身的大小,否則在佈局中使用WRAP_CONTENT就相當於MATCH_PARENT

  4. ViewGoup的measure過程

    對於ViewGroup來說,除了完成自己的measure過程外,還會遍歷去調用所有子元素的measure方法,各個子元素再去遞歸執行這個過程,ViewGroup是一個抽象類,沒有重寫onMeasure方法,但是提供了measureChildrenmeasureChildmeasureChildWithMargins方法

    /**
     * Ask all of the children of this view to measure themselves, taking into
     * account both the MeasureSpec requirements for this view and its padding.
     * We skip children that are in the GONE state The heavy lifting is done in
     * getChildMeasureSpec.
     *
     * @param widthMeasureSpec The width requirements for this view
     * @param heightMeasureSpec The height requirements for this view
     */
    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }
    
    /**
     * Ask one of the children of this view to measure itself, taking into
     * account both the MeasureSpec requirements for this view and its padding.
     * The heavy lifting is done in getChildMeasureSpec.
     *
     * @param child The child to measure
     * @param parentWidthMeasureSpec The width requirements for this view
     * @param parentHeightMeasureSpec The height requirements for this view
     */
    protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
    

    measureChild的思想就是就是取出子元素的LayoutParams,然後再通過getChildMeasureSpec來創建子元素的MeasureSpec從而傳遞給子元素的measure方法。ViewGroup並沒有onMeasure方法,這是因爲ViewGroup是一個抽象類,其測量過程需要各個子類去具體實現,比如LinearLayout、RelativeLayout都有不同的佈局特徵,它們在onMeasure中都會調用measureChildWithMargins或者measureChild去傳遞測量過程,最終傳遞到單一View的onMeasure,最終父元素會先測量子元素,測量完成子元素後根據子元素的測量情況來測量自己的大小


    獲取View的寬高的一種方式

​ 手動調用View的measure方法view.measure(int widthMeasureSpec, int heightMeasureSpec),這種方法比較複雜,這裏要分情況處理,根據View的LayoutParams區分:

  1. match_parent

    直接放棄,根據getChildMeasureSpec方法得知,構造此種MeasureSpec需要知道父元素剩餘可用空間,而這個我們是無從得知的,所以理論上是不可能測量出View的大小

  2. 具體的豎直(dp/px)

    比如寬高都是100px,如下measure

    int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
    int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
    view.measure(widthMeasureSpec,heightMeasureSpec);
    
  3. wrap_content 如下measure

    int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((1 << 30) -1, View.MeasureSpec.AT_MOST);
    int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec((1 << 30) -1, View.MeasureSpec.AT_MOST);
    view.measure(widthMeasureSpec,heightMeasureSpec);
    

    注意到(1 << 30) -1,因爲View的尺寸用30位二進制表示,也就是最大是30個1(即2^30-1),也就是(1 << 30) -1,在最大化模式下,我們用View理論上能支持的最大值去構造MeasureSpec是合理的

layout過程

Layout的作用是ViewGroup用來確定子元素的位置,當ViewGroup的位置被確定後,它會在onLayout中會遍歷所有子元素並調用其layout方法,在layout方法中,onLayout又會被調用。layout方法確定view本身的位置,而onLayout方法確定所有子元素的位置(當然單一View會做一下其它事情參考TextView)。先看View的layout方法如下

/**
 * 爲view及其子view分匹配大小和位置
 * Assign a size and position to a view and all of its
 * descendants
 *
 * 這是佈局機制的第二階段,第一階段是測量(measure),在這一階段,每個父級視圖都會調用子元素的layout方法以確定子元素
 * 的位置
 * <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>
 *
 * 派生類不應該重寫此方法,派生類如果有子元素,那應該重寫onLayout方法,在onLayout方法中去調用每個子元素
 * 的layout方法
 * <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;
    }
    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;//四個頂點的位置
    boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        onLayout(changed, l, t, r, b);//調用onLayout方法
        if (shouldDrawRoundScrollbar()) {
            if(mRoundScrollbarRenderer == null) {
                mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
            }
        } else {
            mRoundScrollbarRenderer = null;
        }
        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);
            }
        }
    }
    //........後面省略

從代碼註釋也可以看出,我們自定義View只需重寫onLayout方法,在onLayout方法中去調用子元素(如果有)的layout方法;layout的大致流程如下:首先會通過setOpticalFrame或者setFrame來設置View的四個頂點的位置,View的四個頂點一旦確定,那麼View在父容器中的位置也就確定了,接着會調用onLayout方法,這個方法用來確定每個子元素的位置,和onMeasure類似,onLayout的具體實現同樣和具體的佈局有關,所以View和ViewGroup均沒有真正實現onLayout方法

draw過程

draw的過程比較簡單但是最能玩出花樣的,它的作用是將View繪製到屏幕上,View的繪製過程一般遵循以下幾步

  1. Draw the background,繪製背景
  2. If necessary, save the canvas’ layers to prepare for fading,如果有需要保存畫布的圖層
  3. Draw view’s content,繪製自己的內容(onDraw)
  4. Draw children 繪製children(diapatchDraw)
  5. If necessary, draw the fading edges and restore layers
  6. Draw decorations (scrollbars for instance)繪製裝飾
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章