自定義View基礎,從源碼瞭解View的繪製過程和繪製規則

android提供給我們的View很多,但是在開發中我們會遇到各種不同的需求,對View的樣式也會有不同的要求。這時系統提供給我們的View就不夠用了,最好的方法就是使用自定義View,這樣的View是可控的,可以根據我們的需求去定製它的樣式。
而要自定義View就必須對View整個繪製過程瞭解透徹,這樣才能在自定義View的時候得心應手,而要了解其繪製過程當然是需要從源碼追蹤的,理解其繪製過程和步驟。

MeasureSpec

  在看View的繪製過程前,我們必須知道一些東西。如我們經常用的wrap_content,match_parent等,它們不是具體的值,因此,在測量的過程中就需要對其做特殊的操作來獲取寬高尺寸。
  而View內部進行測量的時候都是採用的MeasureSpec進行傳遞測量值和測量模式。而MeasureSpec實際上只是一個int值,但是在這裏它確實存儲着兩個參數:測量值和測量模式。它通過將一個32位的整數拆分,其中用高2位來存放佈局模式,而剩餘30位則用來存儲尺寸大小。
  而MeasureSpec則是提供用於拆解組合這個值和模式的一個工具類,它是View的一個靜態內部類,其中包括三種模式和多個方法,這些方法就是用於將模式和尺寸組合成int值的。

// View.java

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

	// 無限制模式,通常是系統所使用的,對大小沒有限制
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;
   
   // 精確模式,對應於layoutParams的match_parent和具體的dp值
    public static final int EXACTLY     = 1 << MODE_SHIFT;

	// 最大模式,這個模式提供一個建議尺寸,這只是View所能取到的最大的值,實際上這個值應當
	// 根據實際的佔用來設置,對應wrao_content
    public static final int AT_MOST     = 2 << MODE_SHIFT;

    // 組合尺寸和模式
    public static int makeMeasureSpec(int size, int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }

    public static int makeSafeMeasureSpec(int size, int mode) {
        if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
            return 0;
        }
        return makeMeasureSpec(size, mode);
    }
    // 獲取模式
    public static int getMode(int measureSpec) {
        return (measureSpec & MODE_MASK);
    }
    // 獲取尺寸
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }
	...    
}

  從這個工具類可以看到,它一共有三種模式,UNSPECIFIED ,EXACTLY,AT_MOST ,然後其他的就是用來拆分合成數據的工具方法。

UNSPECIFIED

  翻譯過來是未指明的。這種模式是系統佈局所使用的,對於寬高沒有限制,想要多大都可以的那種。

EXACTLY

  精確模式,即尺寸是精確大小的,對應起來的話是佈局文件中的match_parent和具體dp值。

AT_MOST

  最大模式,就是有個最大的限制值,不能超過該值。對應wrap_content。

在View中對應的佈局參數會放置在ViewGroup.LayoutParams中,其中寬高對應的模式就在其中。
 	public static final int MATCH_PARENT = -1;
    public static final int WRAP_CONTENT = -2;
在我們使用的時候,可以通過LayoutParams.width/height 來獲取佈局文件設置的寬高。

  瞭解了這些後就可以繼續向下看View的繪製流程了。


Measure

  我們知道,對於View而言,顯示在屏幕上要經歷三個步驟,measure,layout和draw。而這個過程,是ViewRootImpl通過依次調用performMeasure/Layout/Draw這三個方法實現的,他們分別對應着View的再這三個過程。而這三個步驟也將會從頂層View開始,然後逐步分發到其子View,從而實現整個繪製的過程。
  而頂層View則是DecorView,也就是說,繪製的整個流程都是從DecorView開始的。那麼,接下來就開始分析DecorView的繪製過程。
  通過源碼我們可以知道,measure方法是被final所修飾的,也就是說不會被override,那麼DecorView的measure其實也就是View的measure。

// View.java

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    /* widthMeasureSpec ,heightMeasureSpec
	* 這兩個參數是父View傳遞進來的,該值是根據父View的尺寸和模式然後結合子View的尺寸和模式計算所得
	* 這兩個參數是父View所給的建議尺寸,子View應當根據這兩個值進行適當的測量處理
	* 對於頂層View,由於沒有父View,因此這兩個值是根據屏幕大小來生成的。
	* 每一個View測量子View的時候都應當爲子View生成該尺寸模式傳遞下去,而這個尺寸模式實際上可以理解爲
	* 約束條件,子View需要準守這個約束條件來進行自己的測量過程
	*/
	boolean optical = isLayoutModeOptical(this);
    if (optical != isLayoutModeOptical(mParent)) {
        Insets insets = getOpticalInsets();
        int oWidth  = insets.left + insets.right;
        int oHeight = insets.top  + insets.bottom;
        widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
        heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
    }

    // Suppress sign extension for the low bytes
    long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & ffffffL;
    if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);

	// 強制佈局,在View#requestLayout中會設置該標誌
    final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;

    // 佈局參數是否發生了變化
    final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
            || heightMeasureSpec != mOldHeightMeasureSpec;
    // 寬高的模式是否都是Exactly,即固定值或者match_parent
    final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
            && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
    // 是否已經測量過了,並且尺寸沒有發生變化
    final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
            && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
       
    final boolean needsLayout = specChanged
            && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);

	// 當需要重新測量的時候,即View調用了requestLayout或者佈局發生了變化
    if (forceLayout || needsLayout) {
        // first clears the measured dimension flag
        mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
			
        resolveRtlPropertiesIfNeeded();

		// 若不是因爲requestLayout而引起的重新佈局,則先從緩存中查找
        int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
        if (cacheIndex < 0 || sIgnoreMeasureCache) {
            // measure ourselves, this should set the measured dimension flag back
            // 若是requestLayout或者忽略緩存,則重新測量自身
            onMeasure(widthMeasureSpec, heightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        } else {
            long value = mMeasureCache.valueAt(cacheIndex);
            // Casting a long to int drops the high 32 bits, no mask needed
           // 否則的話直接設置爲緩存中獲取的值
           setMeasuredDimensionRaw((int) (value >> 32), (int) value);
            mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        // flag not set, setMeasuredDimension() was not invoked, we raise
        // an exception to warn the developer
        if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
            throw new IllegalStateException("View with id " + getId() + ": "
                    + getClass().getName() + "#onMeasure() did not set the"
                    + " measured dimension by calling"
                    + " setMeasuredDimension()");
        }

        mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
    }

    mOldWidthMeasureSpec = widthMeasureSpec;
    mOldHeightMeasureSpec = heightMeasureSpec;

	// 將測量結果保存在緩存中
    mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
            (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}

  我們可以看到,在measure中主要做了兩件事,測量自身和設置測量結果值。
  在需要測量的時候,會調用onMeasure方法進行測量;不需要測量的時候直接設置測量結果爲從緩存中讀取到的值setMeasuredDimensionRaw。因此,當需要調用onMeasure測量的時候,我們必須要通過setMeasuredDimension/Raw設置測量結果。
  繼續看View的onMeasure方法。

// View.java

// 多層嵌套,最終調用setMeasuredDimensionRaw將結果保存下來
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(
        getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
        getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

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

  從上面可以看到,在View的onMeasure中並沒有做什麼太多的測量,只是將獲取的默認值(這個值是父View所傳遞進來的,父View通過計算所傳遞給子View的建議尺寸)通過setMeasuredDimension繼而調用setMeasuredDimensionRaw方法進行設置,這就完成了保存測量值的過程。
  另外可以看到在setMeasuredDimensionRaw中,直接將測量的結果值保存在了mMeasureWidth和mMeasureHeight中,此時就可以通過View的getMeasuredWidth和getMeasuredHeight方法來獲取View的測量寬高了。但是注意的是,這個寬高並不一定是View的實際大小。實際大小是以佈局時的座標而定的,當然大部分情況下它們是一致的。

public final int getMeasuredWidth() {
    return mMeasuredWidth & MEASURED_SIZE_MASK;
}
public final int getMeasuredHeight() {
    return mMeasuredHeight & MEASURED_SIZE_MASK;
}

  View的onMeasure方法還沒有分析完,這裏只是分析了最外層的setMeasuredDimension方法。而內層的對於寬高的處理還沒有分析,那麼繼續看內層的方法。

// View.java

/*
* 這個方法是View提供的用於獲取View的實際寬高的方法
* 該方法沒有具體計算View本身應該的寬高,而是直接拿來父View給的建議寬高來直接當作本身的寬高
* 而對於系統方法所提供的UNSPECIFIED模式,則設置自身寬高爲minWidth/Height和背景寬高相比的較大值
*/ 
public static int getDefaultSize(int size, int measureSpec) {
    // measureSpec : 父View給的建議寬高
    // size :默認最小值,android:minWidth和背景寬高相比的,該值的獲取在後面有說明

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

protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

  getDefaultSize方法有兩個參數,第一個是getSuggestedMinimumWidth()的返回值,第二個是父View給的建議尺寸參數。可以看到,當測量模式是UNSPECIFIED的時候,獲取的默認值就是getSuggestedMinimumWidth的值。而在AT_MOST和EXACTLY模式中的值都是一樣的,是父View傳遞的建議值的大小。
  getSuggestedMinimumWidth則會先判斷View是否設置了背景,若是沒設置,則使用mMinWidth 的值,該值是在佈局中的android:minWidth設置的,默認爲0。若是設置了背景,則比較該值與背景的寬,取較大的值,高的尺寸也是一樣。
  另外可以看出在AT_MOST和EXACTLY模式的時候值是一樣的,也就是說在默認情況下,View的寬高就是父View給的建議值。因此,如是我們自定義View的時候應該重寫onMeasure方法,然後在原測量的基礎上處理wrap_content的情況。至於match_parent和具體數值則可以直接使用默認的這種測量情況,因爲match_parent的時候構建的子View的測量參數是父View的寬高,具體數值的時候構建的子View的寬高也是該具體數值,這都是滿足我們條件的。而構建子View的測量參數的規則在ViewGroup中,我們稍後會分析。
  而我們知道在佈局文件中設置View的寬高爲wrap_content和match_parent的結果是一樣的,都是鋪滿父View的,其中就與這裏有關。那麼就可以推斷在ViewGroup中,構建子View的測量參數的時候,在子View爲的寬高爲match_parent和wrap_content的時候,所構建的寬高是一樣的,而且都是與父View的寬高一致。

 到這裏就可以看出了View的測量過程:
  1,首先調用measure方法判斷是否需要重新測量。
  2,需要重新測量先從緩存中查找,有的話直接設置緩存中的值。
  3,緩存中沒有的話則使用onMeasure方法進行測量,然後設置測量值。

  由於View沒有子View,所以它的測量比較簡單,但是ViewGroup就不一樣了(雖然ViewGroup也是View,但是由於它可以包含子View,故這裏將它與普通View區分開來),它不僅需要測量自身,還需要測量子View。
  由上面的分析可以知道,measure是final修飾的不能重寫的。也就是說View與ViewGroup的測量的過程的區別只在onMeasure中會有所區分。而是在ViewGroup中又沒有重寫onMeasure,所以需要我們自己進行測量的實現。但是在ViewGroup中卻爲我們定義了一些方法來輔助我們進行測量。
  常用的是measureChildWithMargins

// ViewGroup.java

// 參數分別是父View的佈局參數和已使用過的寬高
protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {
    // 子View的屬性參數
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    // 設置margin,構建給子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去進行自己的測量。注意的是該方法在獲取子View的測量參數的時候是去掉了margin和padding的。

// ViewGroup.java

// 參數是父View的spec參數,padding,子View的佈局參數的寬高
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    // 父佈局的測量模式和尺寸
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);

	// 去掉了padding(在measureChildWithMargins中該值還包括了margin)後的父佈局的大小。
    int size = Math.max(0, specSize - padding);

    int resultSize = 0;
    int resultMode = 0;

    switch (specMode) {
    // 父佈局是精確模式,對應match_parent或者具體值
    case MeasureSpec.EXACTLY:
    	// 子佈局是具體值,則大小爲子佈局的大小,並設爲精確模式
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        // 子佈局爲match_parent,則設置爲父佈局的大小(size在上面已經計算過),設爲精確模式。
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
         // 子佈局爲wrap_content,則設爲父佈局大小(該設置的是實際大小最大隻能取到這個值),設爲最大模式。
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // 父佈局是最大模式值,對應wrap_content
    case MeasureSpec.AT_MOST:
        // 子佈局是具體值,則大小爲子佈局的大小,並設爲精確模式
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        // 子View爲match_parent,則設爲父佈局大小(該設置的是實際大小最大隻能取到這個值),設爲最大模式。
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // 未指定模式,無限制模式,由系統設置調用,
    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);
}

  上面的方法就是生成子View 的建議測量參數的規則,它不單純是根據父View或者子View來進行構建,而是基於二者共同的影響下進行構建,返回值就是構建出的給子View的建議測量寬高。一直在說建議測量寬高,因爲這個值是父View給子View傳遞的寬高模式,子View將會根據這個參數來設置自己寬高,而建議二字則是說子View並不一定必須根據這兩個參數進行設置,只不過大部分情況下子View都要準守的。
子View的測量參數的生成規則
  上述圖片就是構建子View測量參數的規則,這是在父View 和子View共同影響下產生的。是從ViewGroup的getMeasureSpec方法中總結出的。

規則:

1,尺寸:

  當子View爲具體的數值的時候,構建的參數的尺寸就是該數值,其餘情況都是父View的尺寸。

2,模式

A:父View非UNSPECIFIED模式下:
  a1,當子View爲具體的數值的時候,構建的模式都是EXACTLY
  a2,當子View爲match_parent並且父View爲EXACTLY的時候,構建的模式是EXACTLY
  a3,其餘均是AT_MOST
B:父View爲UNSPECIFIED 模式下:
  b1,子View爲具體數值的時候模式爲EXACTLY
  b2,其餘情況爲UNSPECIFIED

  上述爲生成子View的建議測量參數時的規則,當我們自定義View的時候也應當遵守這種規則。

  這就是measureChildWithMargins的流程。在上面說到View的測量過程的時候說到在方法getDefaultSize中,子View的match_parent和wrap_content的結果是一樣的,是因爲在ViewGroup的getChildMeasureSpec方法中,只要子View是wrap_content和match_parent,ViewGroup所給的大小都是父佈局的大小。因此,若是不重寫View的onMeasure而是由默認的onMeasure測量的話,wrap_content模式和match_parent 模式就都是一樣的填充父佈局。這也滿足上面分析子View時提到的猜測。

  此外ViewGroup還有一個方法用來測量子View,與measureChildWithMargins類似,只不過是少測量了margin而已。

//ViewGroup.java

// 循環遍歷子View進行測量(幾乎用不到)
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    // 循環遍歷測量子View
    for (int i = 0; i < size; ++i) {
        final View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

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可以看出與measureChildWithMargins是一致的,區別就是measureChild沒有測量margin。另外measureChild還提供了一個遍歷的方法measureChildren來幫助遍歷子View進行測量。而帶margin的則沒有,需要我們手動遍歷。這兩個方法都是ViewGroup提供給我們的,我們可以選擇合適的方法去調用,但是普遍情況下,對於View而言還是帶有margin屬性比較友好一些。因此在使用measureChild的時候,我們最好自己計算它的margin然後進行設置。而measureChildren則用的很少,它只適用於不含margin屬性的View。

  到這裏測量的過程就已經結束了,那麼接下來就可以繼續走流程了。繼續上面的,我們知道DecorView的measure實際上是調用了View的measure方法,而View的measure又調用了onMeasure方法。那麼接着看DecorView吧,而DecorView是繼承自FrameLayout的,那麼實際上它的onMeasure也就是FrameLayout的onMeasure。

// FrameLayout.java

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int count = getChildCount();

    final boolean measureMatchParentChildren =
            MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
            MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
    mMatchParentChildren.clear();

    int maxHeight = 0;
    int maxWidth = 0;
    int childState = 0;

	// 循環遍歷子View進行測量
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
		// mMeasureAllChildren是在FrameLayout的屬性android:measureAllChildren,默認爲false
		// 若是不指定測量所有子View,則只會測量不是GONE的View
        if (mMeasureAllChildren || child.getVisibility() != GONE) {
            // 測量View的具體實現,上面已經介紹過
			measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            // 記錄子View的最大寬高
			final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            maxWidth = Math.max(maxWidth,
                    child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
            maxHeight = Math.max(maxHeight,
                    child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
            childState = combineMeasuredStates(childState, child.getMeasuredState());
            if (measureMatchParentChildren) {
                if (lp.width == LayoutParams.MATCH_PARENT ||
                        lp.height == LayoutParams.MATCH_PARENT) {
                    mMatchParentChildren.add(child);
                }
            }
        }
    }

    
    maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
    maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

    // 比較獲取到的最大寬高和建議寬高的大小,其中建議寬高在上面也有提過
	// 返回的結果是android:minHeight/Width和background的寬高中的較大值
    maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
    maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

    // 比較前景與獲取的最大值
    final Drawable drawable = getForeground();
    if (drawable != null) {
        maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
        maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
    }

	// 設置自身的測量結果
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            resolveSizeAndState(maxHeight, heightMeasureSpec,
                    childState << MEASURED_HEIGHT_STATE_SHIFT));

    count = mMatchParentChildren.size();
    if (count > 1) {
		// 對於子View含有match_parent模式的進行再次測量
        for (int i = 0; i < count; i++) {
            final View child = mMatchParentChildren.get(i);
            final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

			//寬的模式
            final int childWidthMeasureSpec;
			// 若是爲match_parent,則設置父View的模式爲EXACTLY。
            if (lp.width == LayoutParams.MATCH_PARENT) {
                final int width = Math.max(0, getMeasuredWidth()
                        - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
                        - lp.leftMargin - lp.rightMargin);
                childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                        width, MeasureSpec.EXACTLY);
            } else {
                childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                        getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                        lp.leftMargin + lp.rightMargin,
                        lp.width);
            }

            final int childHeightMeasureSpec;
			// 若是爲match_parent,則設置父View的模式爲EXACTLY。
            if (lp.height == LayoutParams.MATCH_PARENT) {
                final int height = Math.max(0, getMeasuredHeight()
                        - getPaddingTopWithForeground() - getPaddingBottomWithForeground()
                        - lp.topMargin - lp.bottomMargin);
                childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                        height, MeasureSpec.EXACTLY);
            } else {
                childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
                        getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
                        lp.topMargin + lp.bottomMargin,
                        lp.height);
            }
			// 測量子View的時候
			// 以父ViewGroup爲EXACTLY模式,進行測量
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    }
}

  從上面的分析可以看到,FrameLayout也是循環子View然後調用measureChildWithMargins方法來測量子View的,這與我們的分析是一致的,通過measureChildWithMargins內部又調用child.measure進行子View的測量,從而將測量過程傳遞下去。
  另外可以看到,在FrameLayout中對於部分子View的測量不只是一次,而是兩次。也就是對於寬/高模式爲match_parent的子View進行了二次測量,這次的測量中,是以父View(也就是FrameLayout)的對應的寬/高模式設爲EXACTLY來進行測量的。

  從這裏看,measure的過程就已經結束了,可以總結出measure的過程最重要的就是onMeasure方法,這是佈局的主要方法。這裏只分析了FrameLayout的onMeasure方法,但是和其他的View大同小異,都是根據自身特性進行不同的測量。

總結:
自定義View的話佈局階段需要以下幾個步驟:
	1,重寫onMeasure方法,在onMeasure中實現測量
	2,若是ViewGroup則需要循環遍歷子View
		2.1,對子View進行measureChildWithMargins,該方法會測量子View的寬高並將測量過程傳遞下去
		2.2,通過子View的getMeasuredWidth/Height獲取子View的測量寬高,然後進行測量自己的寬高
	3,測量自己的時候需要處理寬高爲wrap_content的情況
	4,將測量結果通過setMeasuredDimension保存
	5,測量結束

  測量結束,那麼接下來繼續看layout方法:


// View.java

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;

	// 最終會調用setFram來設置位置
    boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

	// 若是位置發生了改變,則調用onLayout方法進行重新佈局
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        onLayout(changed, l, t, r, b);

        ...
    }
	...
}

  同樣的,layout方法雖然不是final修飾的,但是也是沒有View覆寫它,實際上,measure,layout,draw都沒有被覆寫,他們是android提供的一套佈局框架。從它的實現來看,首先通過setFrame(setOpticalFrame內部也是調用setFrame)方法進行設置,該方法會判斷View的位置是否發生變化,若是發生了變化就會返回true,然後在layout中就會調用onLayout 方法進行重新佈局。
  在View中,onLayout方法是個空實現,而在ViewGroup中,該方法是被重寫爲抽象方法。因爲ViewGroup可以存在子View,因此必須實現該方法來對子View進行佈局。

// View.java

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

	// 新設置的位置與原來的座標不一致
    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(sizeChanged);

		// 重新賦值新的位置
        mLeft = left;
        mTop = top;
        mRight = right;
        mBottom = bottom;
        mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

        mPrivateFlags |= PFLAG_HAS_BOUNDS;

        if (sizeChanged) {
            sizeChange(newWidth, newHeight, oldWidth, oldHeight);
        }

        if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
            mPrivateFlags |= PFLAG_DRAWN;
            invalidate(sizeChanged);
            invalidateParentCaches();
        }
        mPrivateFlags |= drawn;

        mBackgroundSizeChanged = true;
        mDefaultFocusHighlightSizeChanged = true;
        if (mForegroundInfo != null) {
            mForegroundInfo.mBoundsChanged = true;
        }

        notifySubtreeAccessibilityStateChangedIfNeeded();
    }
    return changed;
}

  從setFrame中可以看到的當layout的位置發生變化的時候,會保存新的位置,然後根據尺寸是否發生變化進行不同的重繪過程。這與View的measure一樣,都是默認的一種方法,而我們想要自己佈局的話,只需要在onLayout方法中改變即可。
  在setFrame中,若是位置發生了變化,則會進行保存新的位置,這時候View的寬高就確定了,可以通過getWidth/Height方法進行獲取。

// View.java

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

  佈局的過程相對於測量比較簡單,就只有這些。
  所以我們接着看DecorView的onLayout方法進行了什麼操作。
  DecorView的onLayout方法依舊是FrameLayout提供的。

//FrameLayout.java

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}

// 對子View進行佈局,前四個參數是自己的座標,最後一個參數可以從名字看出是強制使用left的gravity進行佈局
void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
    final int count = getChildCount();

    final int parentLeft = getPaddingLeftWithForeground();
    final int parentRight = right - left - getPaddingRightWithForeground();

    final int parentTop = getPaddingTopWithForeground();
    final int parentBottom = bottom - top - getPaddingBottomWithForeground();

    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
		// 循環遍歷非Gone的子View
        if (child.getVisibility() != GONE) {
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            // 獲取子View的測量後的寬高
			final int width = child.getMeasuredWidth();
            final int height = child.getMeasuredHeight();

			// 存儲View左上角的座標
            int childLeft;
            int childTop;

            int gravity = lp.gravity;
            if (gravity == -1) {
                gravity = DEFAULT_CHILD_GRAVITY;
            }

            final int layoutDirection = getLayoutDirection();
            final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
            final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;

			// 根據不同的gravity進行佈局
			// 從下面可以看出,layout的位置都是相對於父View的。也就是設父View的左上角的位置爲座標零點。
			// 同樣的,它並沒有對子View的位置進行其他的操作,這裏也就滿足了FrameLayout的特性,
			// 即所有的子View都會擺放在左上角甚至重疊
            switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                case Gravity.CENTER_HORIZONTAL:
                    childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                    lp.leftMargin - lp.rightMargin;
                    break;
                case Gravity.RIGHT:
                    if (!forceLeftGravity) {
                        childLeft = parentRight - width - lp.rightMargin;
                        break;
                    }
                case Gravity.LEFT:
                default:
                    childLeft = parentLeft + lp.leftMargin;
            }

            switch (verticalGravity) {
                case Gravity.TOP:
                    childTop = parentTop + lp.topMargin;
                    break;
                case Gravity.CENTER_VERTICAL:
                    childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                    lp.topMargin - lp.bottomMargin;
                    break;
                case Gravity.BOTTOM:
                    childTop = parentBottom - height - lp.bottomMargin;
                    break;
                default:
                    childTop = parentTop + lp.topMargin;
            }

			// 調用子View的layout將佈局過程傳遞下去
            child.layout(childLeft, childTop, childLeft + width, childTop + height);
        }
    }
}

  在FrameLayout中,onLayout的佈局全部由layoutChildren來實現。而在layoutChildren中也只是循環子View對於非GONE的View進行layout,而layout的依據就是measure的測量結果mMeasuredWidth/mMeasuredHeight。
  相比於measure而言Layout可以說很簡單了,另外在這裏分析的是FrameLayout,因此佈局過程比較簡單,直接相對於左上角的座標進行設置就行了。但是對於像LinearLayout這種,佈局過程就會有所不同了,此時就不會放置在左上角而是根據上一個View的放置位置來設置下一個位置了。

總結:
	1,layout階段只需要重寫onLayout方法並在其中進行佈局的操作
	2,若是普通View甚至可以不重寫onLayout方式,默認方式進行佈局即可
	3,若是ViewGroup,則需要在onLayout中進行循環遍歷子View,然後計算子View的位置並調用子View的layout將佈局分發下去
	4,layout結束

Draw

  measure和Layout都已經分析了一遍了,剩下的就只剩一個draw了。

// View.java

public void draw(Canvas canvas) {
    final int privateFlags = mPrivateFlags;
    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)
     */

    // Step 1, draw the background, if needed
    int saveCount;

    if (!dirtyOpaque) {
        drawBackground(canvas);
    }

    // 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
        if (!dirtyOpaque) onDraw(canvas);

        // Step 4, draw the children
        dispatchDraw(canvas);

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

        // Step 7, draw the default focus highlight
        drawDefaultFocusHighlight(canvas);

        if (debugDraw()) {
            debugDrawFocus(canvas);
        }

        // we're done...
        return;
    }
	...
}

  從上面的Draw方法可以看到,繪製過程分爲6個部分:

	1,繪製背景
	2, 如果必要,保存canvas的layer來準備fading
	3, 繪製本身
	4, 繪製孩子View
	5, 如果必要,繪製衰退效果並恢復layer
	6, 繪製裝飾效果,例如滾動條等

  而就上面的6條而言,2和5通常是不必要的,可以忽略的。那麼繪製的重點就留在了1346這幾條上了。
  其中,繪製背景和繪製裝飾一般也是不需要我們關注的。所以我們主要關注的就是繪製本身和繪製子View。

// View.java

 // 繪製自身
 protected void onDraw(Canvas canvas) {
}

// 繪製子View
protected void dispatchDraw(Canvas canvas) {
}

  在View中這兩個方法都是空實現,因爲每個View的繪製都各不相同,因此這裏沒有具體的實現,而是交由我們自己來實現的。
  另外對於普通子View而言,它是不會有子View,那麼dispatchDraw方法當然也是一個空實現。上面說到onDraw是因爲每個view的表現都不一樣,所以必須交由它的子類進行實現,但是繪製子View卻是可以保持一致的,那麼,在ViewGroup中肯定會重寫這個方法進行具體的實現。

// ViewGroup.java

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();
        // 循環子View,綁定本身帶的動畫
        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());
        }
    }

    int clipSaveCount = 0;
    final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;
    if (clipToPadding) {
        clipSaveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
        canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop,
                mScrollX + mRight - mLeft - mPaddingRight,
                mScrollY + mBottom - mTop - mPaddingBottom);
    }

    // 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();
    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) {
                more |= drawChild(canvas, transientChild, drawingTime);
            }
            transientIndex++;
            if (transientIndex >= transientCount) {
                transientIndex = -1;
            }
        }

        final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
        final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
            more |= drawChild(canvas, child, drawingTime);
        }
    }
    while (transientIndex >= 0) {
        // there may be additional transient views after the normal views
        final View transientChild = mTransientViews.get(transientIndex);
        if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                transientChild.getAnimation() != null) {
            more |= drawChild(canvas, transientChild, drawingTime);
        }
        transientIndex++;
        if (transientIndex >= transientCount) {
            break;
        }
    }
    if (preorderedList != null) preorderedList.clear();

    // Draw any disappearing views that have animations
    if (mDisappearingChildren != null) {
        final ArrayList<View> disappearingChildren = mDisappearingChildren;
        final int disappearingCount = disappearingChildren.size() - 1;
        // Go backwards -- we may delete as animations finish
        for (int i = disappearingCount; i >= 0; i--) {
            final View child = disappearingChildren.get(i);
            more |= drawChild(canvas, child, drawingTime);
        }
    }
    if (usingRenderNodeProperties) canvas.insertInorderBarrier();

    if (debugDraw()) {
        onDebugDraw(canvas);
    }

    if (clipToPadding) {
        canvas.restoreToCount(clipSaveCount);
    }

    // mGroupFlags might have been updated by drawChild()
    flags = mGroupFlags;

    if ((flags & FLAG_INVALIDATE_REQUIRED) == FLAG_INVALIDATE_REQUIRED) {
        invalidate(true);
    }

    if ((flags & FLAG_ANIMATION_DONE) == 0 && (flags & FLAG_NOTIFY_ANIMATION_LISTENER) == 0 &&
            mLayoutAnimationController.isDone() && !more) {
        // We want to erase the drawing cache and notify the listener after the
        // next frame is drawn because one extra invalidate() is caused by
        // drawChild() after the animation is over
        mGroupFlags |= FLAG_NOTIFY_ANIMATION_LISTENER;
        final Runnable end = new Runnable() {
           @Override
           public void run() {
               notifyAnimationListener();
           }
        };
        post(end);
    }
}

  上面的dispatchDraw比較複雜,考慮的比較多,包括隱藏的View,以及View的動畫,但是我們可以看到的是它最終都是是調用了drawChild方法從而進一步調用child的draw方法進行繪製子View。因此,對於繪製過程,我們也只需要關注onDraw方法,即只繪製自身,而不用擔心子View的分發繪製。

總結
View的繪製過程分爲六步
其中大部分的都不用我們去關注,而只用關心onDraw方法即可,通過onDraw方法繪製自身。

總結:

  View整個表現在屏幕上總共有三個過程,分別是measure,layout和draw。其中,measure過程要根據父View的模式和子View的寬高模式共同計算,得出measuredWidth/Height。而layout過程則根據measuredWidth/Height來設置View的位置,一旦layout結束,View的大小也就確定了。draw按照繪製背景->自身->子View->裝飾,這個過程進行繪製。

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