目錄
初識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開始先繪製。
- 首先ViewGroup調用measure,進入測量入口也就是View#measure,爲了自定義ViewGroup的測量流程,會去重寫onMeasure()。
- 在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的值,
- 前面四個是view_1的輸出,由於是match_parent和指定的尺寸,所返回的約束是EXCTLY,看結果可以知道屏幕的寬度是1080px,高度的50dp根據對應的dpi轉化成150px。
- 後面四個是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));
}
一共有三個方法
- getSuggestedMinimumWidth/getSuggestedMinimumHeight
- getDefaultSize
- 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);
}
這一段源碼告訴我們
- 系統不會去區分AL_MOST和EXACTLY約束,我們在自定義時我們需要去區分。
- 在最後確認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:
其實首先就是去判斷,新的位置與就位置是否一樣,若一樣着不需要改變;若不一樣這需要去重新渲染。invalidate就是去渲染新的位置。 - 分析點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是一個性質,其實有時候源碼的註解可以給我們很多閱讀源碼的方向。
那麼對應的中文就是
繪製遍歷執行幾個繪製步驟,這些步驟必須按適當的順序執行
- 繪製背景
- 如果需要,保存圖層
- 繪製內容
- 繪製子View
- 恢復保存的圖層
- 繪製裝飾(例如滾動條)
那麼我們重點看看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的事件機制,以及衝突的解決方案。