View繪製那些事兒

初識ViewRoot和DecorView

這裏咱的重點是View的繪製,但是想讓大家有一個整體的認識,我覺得還是從頂層開始說起,要對應的標題初識,咱這會大致講解,後面Window機制中回去好好說。

其實每一個頁面的根佈局是一個DecorView,而我們在Activity中調用setContentView去設置佈局,其實只不過是Decorview中的一個子View。DecorView這個所有頁面的根視圖,其實是一個FrameLayout,裏面有一個LinearLayout的子View,該LinearLayout是垂直佈局,又有兩個子View一個是Title,還有一個是Content也就是我們調用Activity的setContentView所設置的View。
在這裏插入圖片描述
而這個DecorView不是依附在Activity上的而是依附在Window上的,也就是說真正的視圖是有window來顯示的。window中視圖的增刪對應的操作類是WindowManager,ViewRoot則是負責連接DecorView和WindowManager的樞紐,通過ViewRoot纔開始去繪製DecorView,呈樹形結構模型,依次ViewGroup的measure–>layout—>draw去繪製,接着再去調用子View的measure—>layout---->draw完成整個視圖的繪製流程。

View繪製流程

雖然說繪製流程是呈現樹狀的形式,從樹頂依次往下繪製,也就是說先從ViewGroup開始繪製,依次往子View下繪製。由於ViewGroup是繼承View,所以繪製流程的開始方法measure,layout,draw方法都在View中,故我們按照方法去講解。

  • measure:測量,來測量自身View的高寬
  • layout:位置,表示位於父View的什麼位置
  • draw:繪製,就是將View繪製到屏幕上

measure

咱們先來看看測量的入口,measure方法

  public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        ...
        int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
                mMeasureCache.indexOfKey(key);
        if (cacheIndex < 0 || sIgnoreMeasureCache) {
            onMeasure(widthMeasureSpec, heightMeasureSpec);
        } else {
            ...
    }

這麼說,不管你是View還是ViewGroup,測量的入口都是View#measure方法,因爲ViewGroup繼承View。但是ViewGroup和View的測量流程一定不一樣,爲了區分故ViewGroup和View的視圖需要重寫onMeasure方法,並且該方法還返回了寬高對應的參數,可以去實現對應的測量邏輯。

那麼ViewGroup的測量流程和View的測量流程有什麼先後順序的關聯呢?關聯如下
在這裏插入圖片描述
由於繪製是從頂部開始,而頂部是DecorView一定是一個ViewGroup,所以一定是ViewGroup開始先繪製。

  1. 首先ViewGroup調用measure,進入測量入口也就是View#measure,爲了自定義ViewGroup的測量流程,會去重寫onMeasure()。
  2. 在onMeasure方法裏會去獲取所有的Child,然後去調用Child#measure方法,每一個Child也需要去自定義測量,從而間接的調用了Child#onMeasure方法。

View

正如上訴流程描述,我們先來講解一下View的onMeasure(),可以幫助我們做什麼。

  @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //下面爲自己添加部分
        MeasureSpec.getSize(widthMeasureSpec);
        MeasureSpec.getMode(widthMeasureSpec);
        MeasureSpec.getSize(heightMeasureSpec);
        MeasureSpec.getMode(heightMeasureSpec);
    }

其實widthMeasureSpec和heightMeasureSpec所表示的確實和寬高有關,但是並不能拿來直接用,它所表達的是父View結合子View自身的寬高再施加規則所返回的值,需要通過MeasureSpec去解析用法如上。

MeasureSpec

MeasureSpec則代表着一個32位的int值,高兩位表示SpecMode,低30位表示SpecSize。SpecMode表示測量模式,SpecSize則表示某種測量模式下的規格大小。

  • 可以通過MeasureSpec.getSize(xx)去獲取到SpecSize
  • 可以通過MeasureSpec.getMode(xx)去獲取到SpecMode
SpecMode

測量模式對應的值有三種如下圖
在這裏插入圖片描述
注意:因爲SpecMode所表示的模式是有父View的寬高和子View的寬高共同約束的,下面結論僅針對父親寬高都是match_parent的時候

模式 約束意義 對應的值
EXACTLY 父控件決定給子View一個精確的尺寸 1073741824
AL_MOST 父控件會給子View一個儘可能大的尺寸 -2147483648
UNSPECIFIED 父控件不強加任何約束,它可以是它想要的任何大小 0

可以這麼理解

  • EXACTLY:既然父控件可以給予子View精確的尺寸,那麼子View自身的寬/高一定是確定的,xml中對應的就是match_parent/準確的尺寸
  • AL_MOST:父空間會給子View一個儘可能大的尺寸,但不能超過自己,也就說明View自身的寬/度不確定,xml中對應的就是wrap_content

UNSPECIFIED
一般用不到,一般會出現在系統View繪製中。由於一般博客都沒怎麼講解到,在此爲了彌補這一空缺,咱們要好好的解釋一波。

就拿系統的RecycleView爲例子,在Item進行measure也就是測量的時候,如果可以滑動且Item寬高是wrap_content的話,那麼接下來Item的onMeasure方法就會收到MeasureSpec.UNSPECIFIED。

**爲什麼此時是不是AL_MOST?**我們來看看RecycleView去生成Child的約束的方法getChildMeasureSpec

  public static int getChildMeasureSpec(int parentSize, int parentMode, int padding,
                int childDimension, boolean canScroll) {
            int size = Math.max(0, parentSize - padding);
            int resultSize = 0;
            int resultMode = 0;
            if (canScroll) {
                if (childDimension >= 0) {
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.MATCH_PARENT) {
                    switch (parentMode) {
                        case MeasureSpec.AT_MOST:
                        case MeasureSpec.EXACTLY:
                            resultSize = size;
                            resultMode = parentMode;
                            break;
                                                // MATCH_PARENT can't be applied since we can scroll in this dimension, wrap
                    // instead using UNSPECIFIED.
                        case MeasureSpec.UNSPECIFIED:
                            resultSize = 0;
                            resultMode = MeasureSpec.UNSPECIFIED;
                            break;
                    }
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    resultSize = 0;
                    resultMode = MeasureSpec.UNSPECIFIED;
                }
            } else {
                if (childDimension >= 0) {
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.MATCH_PARENT) {
                    resultSize = size;
                    resultMode = parentMode;
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    resultSize = size;
                    if (parentMode == MeasureSpec.AT_MOST || parentMode == MeasureSpec.EXACTLY) {
                        resultMode = MeasureSpec.AT_MOST;
                    } else {
                        resultMode = MeasureSpec.UNSPECIFIED;
                    }

                }
            }
            //noinspection WrongConstant
            return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
        }

可以看到當可滑動的時候,child爲wrap_content的時候,child的約束的UNSPECIFIED;當不能滾動的時候,若父控件的約束是UNSPECIFIED且child是wrap_content則是UNSPECIFIED。

上面還有一段官方的註解,它表達的意思大致就是能滾動的時候,不應該去限制child的大小。

因爲本身RecycleView就是可以滾動的,哪怕是child的的寬高超出了屏幕的範圍,也還是可以通過滾動去查看顯示,若此時約束爲AL_MOST,那麼child最大最大的寬高只能是父控件的寬高,這樣顯然是不合理的。

所以這時候需要UNSPECIFIED的約束,因爲父控件不強加任何約束,那麼子View想要多少就有多少,想放哪裏就放哪裏,這才形成了超出屏幕的範圍,最終才呈現出滑動的效果。


實踐看一看

接下來我們來看看子View的寬高對應的SpecSize和SpecMode,首先我們自定義一個View然後日誌輸出。

public class MyView extends View {
    public MyView(Context context) {
        super(context);
    }
    public MyView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        System.out.println("width mode " + MeasureSpec.getMode(widthMeasureSpec));
        System.out.println("width size " + MeasureSpec.getSize(widthMeasureSpec));
        System.out.println("height mode " + MeasureSpec.getMode(heightMeasureSpec));
        System.out.println("height size " + MeasureSpec.getSize(heightMeasureSpec));
    }
}

創建了兩個View一個是寬:match_parent,高:50dp;另一個是寬:wrap_content,寬:wrap_content.

  <com.sinosun.csdnnote.views.MyView
  		android:id="@+id/view_1"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        tools:ignore="MissingConstraints"></com.sinosun.csdnnote.views.MyView>

    <com.sinosun.csdnnote.views.MyView
      	android:id="@+id/view_2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tools:ignore="MissingConstraints"></com.sinosun.csdnnote.views.MyView>

那麼最後的日誌如下圖,可以根據值去查看SpecMode的值,

在這裏插入圖片描述

  1. 前面四個是view_1的輸出,由於是match_parent和指定的尺寸,所返回的約束是EXCTLY,看結果可以知道屏幕的寬度是1080px,高度的50dp根據對應的dpi轉化成150px。
  2. 後面四個是view_2的輸出,由於是wrap_content,所以生成的約束是AL_MOST,但是最後顯示的寬高是整個屏幕的寬高,按照道理AL_MOST約束下,父親會給子View一個儘可能大的尺寸。什麼意思?就是說給的尺寸剛剛好,與結果相違背。這恰恰是自定義View的一個注意點,咱們來好好分析一下
注意點

先找到View#onMeasure方法

   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

一共有三個方法

  1. getSuggestedMinimumWidth/getSuggestedMinimumHeight
  2. getDefaultSize
  3. setMeasuredDimension
  • getSuggestedMinimumWidth/getSuggestedMinimumHeight
    先來分析getSuggestedMinimumWidth/getSuggestedMinimumHeight,就拿其中的getSuggestedMinimumWidth來說事兒吧,就看這名字的意思就是獲取建議的最小寬度
   protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

代碼裏也很明白,如果沒有設置背景,那麼就View內容的寬度;如果有背景,就比較背景和內容的大小選取最大。

  • getDefaultSize
    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;
    }

這一段代碼似乎解決了,爲什麼AT_MOST所顯示的尺寸和EXACTLY,也就是上面view_2的輸入日誌問題所在。在View的源碼裏,默認把AT_MOST和EXACTLY類型同一處理,所以也就爲什麼wrap_content獲取到的值是match_parent,所以在自定義VIew的時候,需要對約束進行判斷,若EXACTLY則返回對應的SpecSize的值,若是AT_MOST則要返回實際的尺寸

  • setMeasuredDimension
    這方法很簡單,最後就是去設置View的寬高
   protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {//判斷是不是ViewGroup
            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);
    }

這一段源碼告訴我們

  1. 系統不會去區分AL_MOST和EXACTLY約束,我們在自定義時我們需要去區分。
  2. 在最後確認View的寬高之後,需要調用setMeasuredDimension()方法去設置View的寬高。

ViewGroup

根據measure的流程圖,ViewGroup到底是如何調用child#measure方法呢?我們來看看內部提供的三個方法。

  • measureChildren
  • measureChild
  • measureChildWithMargins
    我們來看看具體是怎麼樣實現的。

measureChildren

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

代碼其實很簡單,就是後去所有的Child,調用measureChild方法,傳入Child和ViewGroup自身的約束。所以該方法主要是遍歷Child。

measureChild

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

這一段代碼也很好理解,通過父控件的約束、child自身的寬高和padding去生成最終的測量約束MeasureSpec,然後在調用Child#measure,從而可以在重寫child#onMeasure方法中去回調到最終的約束值。也就是說padding會參與到約束條件中去。

measureChildWithMargins

 protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        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);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

其實measureChildWithMargins和measureChild類似,只不過measureChildWithMargins會把margin參與到約束的計算中去。

getChildMeasureSpec

似乎約束是有該方法生成的,那麼我們不妨去看看約束條件是如何生成的

  public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        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;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

看到switch,就可以那些約束最終會生成什麼SpecMode了

父約束 子View寬高 子約束
EXACTLY match_parent/準確的尺寸 EXCTLY
EXACTLY wrap_content AL_MOST
AL_MOST 準確的尺寸 EXCTLY
AL_MOST match_parent/wrap_content AL_MOST
UNSPECIFIED 準確的尺寸 EXCTLY
UNSPECIFIED match_parent/wrap_content UNSPECIFIED

例子

可能有些人還是覺得很抽象,那麼我來看一下ScrollView的onMeasure看看做了什麼?

      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
		...
        if (getChildCount() > 0) {
            final View child = getChildAt(0);
            final int widthPadding;
            final int heightPadding;
            final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
            final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
            if (targetSdkVersion >= VERSION_CODES.M) {
                widthPadding = mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin;
                heightPadding = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin;
            } else {
                widthPadding = mPaddingLeft + mPaddingRight;
                heightPadding = mPaddingTop + mPaddingBottom;
            }
			//獲取ScrollView的測量高度-padding部分
            final int desiredHeight = getMeasuredHeight() - heightPadding;
            //若ScrollView的可用空間大於Child需要的空間
            if (child.getMeasuredHeight() < desiredHeight) {
                final int childWidthMeasureSpec = getChildMeasureSpec(
                        widthMeasureSpec, widthPadding, lp.width);
                final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                        desiredHeight, MeasureSpec.EXACTLY);
                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }
    }

上訴是ScrollView#onMeasure方法,重在思路。由於ScrollView只能擁有一個Child需我們只能看到一個Child調用measure方法。

就拿ScrollView爲例,ScrollView的測量先調用到了measure,由於系統把測量權交給了我們,所以ScrollView需要重寫onMeasure,然後計算出自身的約束,再去調用Child#measure方法,從而間接的觸發Child#onMeasure方法。

layout

layout的分析流程其實和measure一樣的,雖然會最先設置ViewGroup的layout(位置),但是layout入口的實現其實是其父類也就是View的layout。整一個ViewGroup與View的關係如下圖
在這裏插入圖片描述

View

正如上圖流程,那麼我們先來看看View#layout的入口源碼

 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;
        }
		//分析1
        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;
		//分析2 setOpticalFrame和setFrame
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
		//分析3
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);
     	...
        }
	...
  • a.分析1處:
    該layout的目的就是確定及設置View的位置,該處的oldL分別表示當前位置,也爲了緩存,因爲分析2處,就會根據新傳入的參數重新設置位置。
  • b.分析2處
    該處主要的作用就是通過setOpticalFrame/setFrame來確定新的位置,判斷是否需要發送位置的改變。那麼先來看看setFrame方法
  protected boolean setFrame(int left, int top, int right, int bottom) {
        boolean changed = false;

        if (DBG) {
            Log.d(VIEW_LOG_TAG, this + " View.setFrame(" + left + "," + top + ","
                    + right + "," + bottom + ")");
        }
		//分析點1
        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
            changed = true;

            // Remember our drawn bit
            int drawn = mPrivateFlags & PFLAG_DRAWN;

            int oldWidth = mRight - mLeft;
            int oldHeight = mBottom - mTop;
            int newWidth = right - left;
            int newHeight = bottom - top;
            boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

            // Invalidate our old position
            invalidate(sizeChanged);
			//分析點2
            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
            mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
		...
   
        return changed;
    }
  1. 分析點1:
    其實首先就是去判斷,新的位置與就位置是否一樣,若一樣着不需要改變;若不一樣這需要去重新渲染。invalidate就是去渲染新的位置。
  2. 分析點2
    然後並記錄下新的位置

setOpticalFrame該方法內部實則還是調用setFrame方法

  private boolean setOpticalFrame(int left, int top, int right, int bottom) {
        Insets parentInsets = mParent instanceof View ?
                ((View) mParent).getOpticalInsets() : Insets.NONE;
        Insets childInsets = getOpticalInsets();
        return setFrame(
                left   + parentInsets.left - childInsets.left,
                top    + parentInsets.top  - childInsets.top,
                right  + parentInsets.left + childInsets.right,
                bottom + parentInsets.top  + childInsets.bottom);
    }

  • c.分析點3
    該處會去調用onLayout,此時需要分類討論。若當前是一個ViewGroup需要去重寫一個onLayout方法,作用同onMeasure一樣,我們需要去遍歷所有的Child,再調用Child#layout方法,賦予Child具體的位置;若當前是一個View則是一個空實現,因爲View沒有子View。

可以看看ViewGroup#onLayout,該方法還是一個抽象方法,如果繼承ViewGroup必定要實現onLayout

  @Override
    protected abstract void onLayout(boolean changed,
            int l, int t, int r, int b);

例子

那就結合LinearLayout#onLayout來看看,畢竟理論要結合實際嘛

    void layoutVertical(int left, int top, int right, int bottom) {
        final int paddingLeft = mPaddingLeft;

        int childTop;
        int childLeft;

        // Where right end of child should go
        final int width = right - left;
        int childRight = width - mPaddingRight;

        // Space available for child
        int childSpace = width - paddingLeft - mPaddingRight;

        final int count = getVirtualChildCount();

        final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
        final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
	//先計算出第一個Child的top
        switch (majorGravity) {
           case Gravity.BOTTOM:
               // mTotalLength contains the padding already
               childTop = mPaddingTop + bottom - top - mTotalLength;
               break;

               // mTotalLength contains the padding already
           case Gravity.CENTER_VERTICAL:
               childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
               break;

           case Gravity.TOP:
           default:
               childTop = mPaddingTop;
               break;
        }

        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                childTop += measureNullChild(i);
            } else if (child.getVisibility() != GONE) {
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();

                final LinearLayout.LayoutParams lp =
                        (LinearLayout.LayoutParams) child.getLayoutParams();

                int gravity = lp.gravity;
                if (gravity < 0) {
                    gravity = minorGravity;
                }
                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                //設置left
                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                                + lp.leftMargin - lp.rightMargin;
                        break;

                    case Gravity.RIGHT:
                        childLeft = childRight - childWidth - lp.rightMargin;
                        break;

                    case Gravity.LEFT:
                    default:
                        childLeft = paddingLeft + lp.leftMargin;
                        break;
                }
				//添加分隔符
                if (hasDividerBeforeChildAt(i)) {
                    childTop += mDividerHeight;
                }
				
                childTop += lp.topMargin;
                //調用child.layout
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

                i += getChildrenSkipCount(child, i);
            }
        }
    }
    
   private void setChildFrame(View child, int left, int top, int width, int height) {
        child.layout(left, top, left + width, top + height);
    }

其實大體思路很清晰,先計算出第一個Child的Top,然後其餘的Child在此基礎上依次累加。遍歷所有的Child,根據padding和margin計算出left,最後在通過setChildFrame方法,去設置Child#layout方法。

draw

draw表示繪製流程,大體流程也是同measure,layout是一個性質,其實有時候源碼的註解可以給我們很多閱讀源碼的方向。
在這裏插入圖片描述
那麼對應的中文就是
繪製遍歷執行幾個繪製步驟,這些步驟必須按適當的順序執行

  1. 繪製背景
  2. 如果需要,保存圖層
  3. 繪製內容
  4. 繪製子View
  5. 恢復保存的圖層
  6. 繪製裝飾(例如滾動條)

那麼我們重點看看3 4點
在這裏插入圖片描述我們可以看到若要實現繪製內容,則需要去重寫onDraw方法即可。那麼dispatchDraw則表示繪子View,此時還是需要分類討論,既然是繪製子View,那麼View沒有Child所以是空實現;如果是ViewGroup則會去重寫dispatchDraw方法。

那麼我們來看看LinearLayout的dispatchDraw,來看看是如何實現的

    protected void dispatchDraw(Canvas canvas) {
        boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
        final int childrenCount = mChildrenCount;
        final View[] children = mChildren;
        int flags = mGroupFlags;
	
        if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {
            final boolean buildCache = !isHardwareAccelerated();
            //遍歷所有Child去設置動畫
            for (int i = 0; i < childrenCount; i++) {
                final View child = children[i];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
                    final LayoutParams params = child.getLayoutParams();
                    attachLayoutAnimationParameters(child, params, i, childrenCount);
                    bindLayoutAnimation(child);
                }
            }

            final LayoutAnimationController controller = mLayoutAnimationController;
            if (controller.willOverlap()) {
                mGroupFlags |= FLAG_OPTIMIZE_INVALIDATE;
            }
			//開始動畫
            controller.start();

            mGroupFlags &= ~FLAG_RUN_ANIMATION;
            mGroupFlags &= ~FLAG_ANIMATION_DONE;
		//設置動畫監聽
            if (mAnimationListener != null) {
                mAnimationListener.onAnimationStart(controller.getAnimation());
            }
        }
		...
        // We will draw our child's animation, let's reset the flag
        mPrivateFlags &= ~PFLAG_DRAW_ANIMATION;
        mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED;

        boolean more = false;
        final long drawingTime = getDrawingTime();

        if (usingRenderNodeProperties) canvas.insertReorderBarrier();
        final int transientCount = mTransientIndices == null ? 0 : mTransientIndices.size();
        int transientIndex = transientCount != 0 ? 0 : -1;
        // Only use the preordered list if not HW accelerated, since the HW pipeline will do the
        // draw reordering internally
        final ArrayList<View> preorderedList = usingRenderNodeProperties
                ? null : buildOrderedChildList();
        final boolean customOrder = preorderedList == null
                && isChildrenDrawingOrderEnabled();
                //遍歷所有Child
        for (int i = 0; i < childrenCount; i++) {
            while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
                final View transientChild = mTransientViews.get(transientIndex);
                if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                        transientChild.getAnimation() != null) {
                      //調用drawChild,內部實則調用child.draw
                    more |= drawChild(canvas, transientChild, drawingTime);
                }
                transientIndex++;
                if (transientIndex >= transientCount) {
                    transientIndex = -1;
                }
            }
		...
}

 protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }

所以我們可看到LinearLayout先調用draw方法,通過重寫dispatchDraw,去遍歷所有Child,接着調用Child#draw,從而觸發到我們重寫的onDraw去繪製內容。

補充

對於View的寬高獲取需要特別注意,不能隨隨便便的getHeight/Width,因爲你無法確定此時的View是否已經繪製完成,所以需要額外注意。

view.post

最常用的方式使用很簡單,通過post可以將一個runnable投遞到消息隊列的尾部,等待Looper調用runnable的時候,view已經初始化了

view.post(new Runnable() {
    @Override
    public void run() {
        int width = view.getMeasuredWidth();
        int height = view.getMeasuredHeight();
    }
});

activity/view#onWindowFocusChanged

@Override
public void onWindowFocusChanged(boolean hasFocus) {
    super.onWindowFocusChanged(hasFocus);
    if (hasFocus) {
        int width = view.getMeasuredWidth();
        int height = view.getMeasuredHeight();
    }
}

重寫onWindowFocusChanged方法即可,不過該方法會被多次調用,onResume和onPause都會被調用

getMeasuredWidth和getWidth的區別

getWidth

  public final int getWidth() {
        return mRight - mLeft;
    }

從源碼來看,getWidth所返回的值是mRight和mLeft的差值,而mRight,mLeft表示該View的位置,在layout中會被設置,也就是說當layout調用完畢,確定好View的位置,那麼getWidth的值就有了

getMeasuredWidth

    public final int getMeasuredWidth() {
        return mMeasuredWidth & MEASURED_SIZE_MASK;
    }
    
  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);
    }
    
 private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;

        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }

可以看到getMeasuredWidth的值與mMeasuredWidth有關,而mMeasuredWidth是在onMeasure中得到測量後的寬高通過setMessuredDimension方法去設置保存的,所以getMeasuredWidth的調用最好是在setMessuredDimension方法之後。若在setMessuredDimension之前調用getMeasuredWidth則會返回0。

下篇預告

那麼 measure layout draw 三個流程大致講完,下一篇就是View的事件機制,以及衝突的解決方案。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章