Android中的任何一個布局、控件都是直接或間接繼承自View的,要想開發一個Android App,就肯定少不了要和View打交道。盡管Android已經提供了豐富的控件和布局,但是要想開發出有自己特色的App,自定義View是必須要掌握的。那么,今天我們就來聊聊View繪制的相關內容。
本文的要點如下:
- View簡介
- MeasureSpecs類
- onMeasure
- 單一View的測量
- ViewGroup的測量
- onLayout
- 單一View的layout過程
- ViewGroup的layout過程
- onDraw
- 總結
View簡介
在Android系統中View是所有控件的基類,其中也包括ViewGroup在內,ViewGroup是代表著控件的集合,其中可以包含多個View控件。
從某種角度上來講Android中的控件可以分為兩大類:View與ViewGroup。通過ViewGroup,整個界面的控件形成了一個樹形結構,上層的控件要負責測量與繪制下層的控件,并傳遞交互事件。
在每棵控件樹的頂部都存在著一個ViewParent對象,它是整棵控件樹的核心所在,所有的交互管理事件都由它來統一調度和分配,從而對整個視圖進行整體控制,如下圖所示:
繪制出整個界面肯定是要遍歷整個View樹,對這棵樹的所有節點分別進行測量,布局和繪制。萬事皆有源頭,繪制得從根節點頂級View開始畫起,即DecorView。
系統內部會依次調用DecorView的measure,layout和draw三大流程方法。measure方法又會調用onMeasure方法對它所有的子元素進行測量,如此反復調用下去就能完成整個View樹的遍歷測量。同樣的,layout和draw兩個方法里也會調用相似的方法去對整個View樹進行遍歷布局和繪制。
View的構造函數:共有4個,具體如下:
// 如果View是在Java代碼里面new的,則調用第一個構造函數
public CarsonView(Context context) {
super(context);
}
// 如果View是在.xml里聲明的,則調用第二個構造函數
// 自定義屬性是從AttributeSet參數傳進來的
public CarsonView(Context context, AttributeSet attrs) {
super(context, attrs);
}
// 不會自動調用
// 一般是在第二個構造函數里主動調用
// 如View有style屬性時
public CarsonView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
//API21之后才使用
// 不會自動調用
// 一般是在第二個構造函數里主動調用
// 如View有style屬性時
public CarsonView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
View的位置參數:
View的位置由4個頂點決定(View的位置是相對于父控件而言的)
- Top:子View上邊界到父view上邊界的距離
- Left:子View左邊界到父view左邊界的距離
- Bottom:子View下邊距到父View上邊界的距離
- Right:子View右邊界到父view左邊界的距離
MeasureSpecs類
MeasureSpecs類是View的內部類,用一個變量封裝了兩個數據(size、mode),其目的是減少對象的內存分配。
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;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
// 通過Mode 和 Size 生成新的SpecMode
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
//獲取測量模式(Mode)
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
//原理:保留measureSpec的高2位(即測量模式)、使用0替換后30位
//例如10 00..00100 & 11 00..00(11后跟30個0) = 10 00..00(AT_MOST)
//這樣就得到了mode的值
}
//獲取測量大小(Size)
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
// 原理類似上面,即 將MODE_MASK取反,也就是變成了00 111111(00后跟30個1),
//將32,31位替換成0也就是去掉mode,保留后30位的size
}
}
測量規格(MeasureSpec) = 測量模式(mode) + 測量大小(size)。
其中,測量模式占最高兩位,測量大小則是MeasureSpec的低30位。測量模式(Mode)的類型有3種:UNSPECIFIED、EXACTLY 和AT_MOST。具體如下:
UNSPECIFIED:父View不約束子View(即子View可以獲取任意尺寸),多用于系統內部View(ListView,ScrollView等),自定義View一般用不到。(這個模式主要用于系統內部多次Measure的情形,并不是真的說你想要多大最后就真有多大)
EXACTLY(精確模式):父View會為子View指定一個確切尺寸,子View必須在該尺寸之內。對應LayoutParams中的match_parent或具體數值。
AT_MOST(最大模式):父容器為子視圖指定一個最大尺寸SpecSize,View的大小不能大于這個值。對應LayoutParams中的wrap_content。
onMeasure
View的繪制流程中,第一步就是測量,即onMeasure()方法。我們知道,自定義View的類型可以分為兩種,一種繼承View,一種繼承ViewGroup(繼承現有View的也可以根據繼承的View的不同歸入這兩種類型之中)。那么測量的過程也有兩種:
- 單一View的測量
- ViewGroup的測量
那么下面我們就分別來看看兩種流程的不同。
單一View的measure過程
這種情況相對而言比較簡單,不用考慮子View,只有一個原始的View,通過measure()即可完成測量,具體過程如下:
那么我們來看看整個流程的源碼:
//measure是測量流程的開始,由于是final類型,因此不能被重寫,
//主要是用來進行基本測量邏輯的判斷。
//里面調用onMeasure進行測量邏輯。
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 {
...
}
measure方法是測量過程中最先調用的方法,View的這個方法是被它的父控件調用的。由于measure是final類型,不能被子類重寫,那么就只能重寫onMeasure方法來實現測量邏輯了。
//在onMeasure中就做了兩件事,
//1是根據View寬/高的測量規格用getDefaultSize()方法計算View的寬/高值,
//2是用setMeasuredDimension()方法存儲測量后的View寬 / 高。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
protected int getSuggestedMinimumWidth() {
//如果有設置背景,則獲取背景的寬度,如果沒有設置背景,則取xml中android:minWidth的值。
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
onMeasure里面用到了getSuggestedMinimumWidth和getSuggestedMinimumHeight,這兩個方法差不多的,我們就以getSuggestedMinimumWidth為例。mMinWidth屬性對應的就是xml布局里的android:minWidth屬性,設置最小寬度。mBackground.getMinimumWidth()方法返回的就是View背景Drawable的原始寬度,這個寬度跟背景的類型有關。比如我們給View的背景設置一張圖片,那這個方法返回的寬度就是圖片的寬度,而如果我們給View背景設置的是顏色,那么這個方法返回的寬度則是0。
所以,這個方法的返回的寬度是:如果View沒有設置背景,那就返回xml布局里的android:minWidth屬性定義的值,默認為0;如果View設置了背景,就返回背景的寬度和mMinWidth中的最大值。
//存儲測量后的View寬 / 高,該方法即為我們重寫onMeasure()所要實現的最終目的。
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
//判斷該view布局模式是否有一些特殊的邊界
boolean optical = isLayoutModeOptical(this);
////判斷view和該view的父view的布局模式情況,如果兩者不同步,則進行子view的size大小的修改
if (optical != isLayoutModeOptical(mParent)) {
//有兩種情況會進入到該if條件,
//一是子view有特殊的光學邊界,而父view沒有,此時optical為true,
//一種是父view有一個特殊的光學邊界,而子view沒有,此時optical為false
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
//存儲測量后的View寬 / 高的實際邏輯
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
//根據View寬/高的測量規格計算View的寬/高值
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:
// 模式為UNSPECIFIED時,使用提供的默認大小 = 參數Size
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
//模式為AT_MOST,EXACTLY時,
//使用View測量后的寬/高值 = measureSpec中的Size
result = specSize;
break;
}
return result;
}
可以看出,View在當測量模式為UNSPECIFIED時,返回的就是上面getSuggestedMinimumWidth/Height()方法里的大小。其實這對我們自定義控件并沒有什么影響,因為UNSPECIFIED模式是給系統內部用的。我們的重點還是應該放在AT_MOST和EXACTLY兩種情況下。對于這兩種情況,getDefaultSize十分簡單粗暴,直接返回了specSize,也就是View的測量規格里的測量尺寸。
這里就出現了一個問題在AT_MOST和EXACTLY兩種情況下返回的尺寸竟然都是specSize。
因此在自定義View控件時,我們需要重寫onMeasure方法并設置wrap_content時自身的大小。否則在xml布局中使用wrap_content時與match_parent的效果將會是一樣。
至此,單一View的寬/高值已經測量完成,即對于單一View的measure過程已經完成。
小小的總結一下,其是前面的源碼只是為了對View的測量有個完整的概念,清楚整個流程,主要我們實現還是在onMeasure方法中,因此可以寫一個自定義View的onMeasure方法的通用模版,其實最關鍵的也就是在AT_MOST模式時進行特殊處理,畢竟父類的onMeasure已經實現了大部分邏輯:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 必須調用,因為父類還是實現了很多東西的。
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 寬的測量模式
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
// 寬的測量尺寸
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
// 高度的測量模式
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
// 高度的測量尺寸
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
//根據View的邏輯得到,比如TextView根據設置的文字計算wrap_content時的大小。
//這兩個數據變量要根據實現需求計算。
int wrapWidth,wrapHeight;
// 如果有測量模式是AT_MOST則需要進行特殊處理
if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
//長寬都是AT_MOST,則都需要計算
setMeasuredDimension(wrapWidth, wrapHeight);
}else if(widthSpecMode == MeasureSpec.AT_MOST){
//只有寬是AT_MOST,則長用測量尺寸
setMeasuredDimension(wrapWidth, heightSpecSize);
}else if(heightSpecMode == MeasureSpec.AT_MOST){
//只有長是AT_MOST,則寬用測量尺寸
setMeasuredDimension(widthSpecSize, wrapHeight);
}
}
ViewGroup的measure過程
ViewGroup的情況就復雜一些了,畢竟它還有一堆子View要考慮,大多數時候要先確定子View的大小,再確定ViewGroup的的大小。不過原理也很簡單,就是遍歷測量所有子View的尺寸然后將所有子View的尺寸進行合并,最終得到ViewGroup父視圖的測量值。
看過源碼就知道ViewGroup并沒有重寫View的onMeasure方法,為什么呢?顯然這需要它的子類去根據相應的邏輯去實現,比如LinearLayout與RelativeLayout對child View的測量邏輯顯然是不同的。這個也是單一View的measure過程與ViewGroup過程最大的不同。
不過,ViewGroup倒是提供了一個measureChildren的方法,貌似可以用來測量child的樣子,看看源碼:
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);
}
}
}
這個方法的邏輯就很清晰嘛,就是遍歷子View,調用measureChild方法對其進行測量,那么我們接著來看看measureChild方法:
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
//取出子View的LayoutParams
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方法里,會取出child的LayoutParams,再結合父控件的測量規格和已被占用的空間Padding,作為參數傳遞給getChildMeasureSpec方法,在getChildMeasureSpec里會組合生成child控件的測量規格。
getChildMeasureSpec是ViewGroup里提供的一個靜態方法,用來用來獲取子控件的測量規格。
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//父view的測量模式
int specMode = MeasureSpec.getMode(spec);
//父view的測量大小
int specSize = MeasureSpec.getSize(spec);
//通過父view計算出的子view = 父大小-邊距(父要求的大小,但子view不一定用這個值)
int size = Math.max(0, specSize - padding);
//子view想要的實際大小和模式(需要計算)
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
/// 當父控件的測量模式 是 精確模式,也就是有精確的尺寸了
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
//如果child的布局參數有固定值(大于0),比如"layout_width" = "100dp"
//那么顯然child的測量規格也可以確定下來了,測量大小就是100dp,測量模式也是EXACTLY
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//當子view的LayoutParams為MATCH_PARENT時(-1)
//此時父控件是精確模式,也就是能確定自己的尺寸了,那child也能確定自己大小了
//子view大小為父view大小,模式為EXACTLY
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//當子view的LayoutParams為WRAP_CONTENT時(-2)
//比如TextView根據設置的字符串大小來決定自己的大小
//那就自己決定唄,不過你的大小肯定不能大于父控件的大小嘛
//所以測量模式就是AT_MOST,測量大小就是父控件的size
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 當父控件的測量模式是AT_MOST時,父view強加給子view一個最大的值
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
//同樣的,既然child能確定自己大小,盡管父控件自己還不知道自己大小,也會優先滿足孩子的需求
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//child想要和父控件一樣大,但父控件自己也不確定自己大小,所以child也無法確定自己大小
//但同樣的,child的尺寸上限也是父控件的尺寸上限size
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//child想要根據自己邏輯決定大小,那就自己決定唄
//同樣的,child的尺寸上限也是父控件的尺寸上限size
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 當父view的模式為UNSPECIFIED時,父容器不對view有任何限制,要多大給多大
// 多見于ListView、GridView
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);
}
至此,ViewGroup的測量流程也基本結束了,整體的流程如下圖:
不過還有一個問題不知道大家注意到沒有,measureChild方法只考慮了父View的padding,但是沒考慮到子View的margin。這就會導致子view在使用match_parent屬性的時候,margin屬性會有問題。
當然,ViewGroup也考慮到了這個問題,為此也提供了另一個測量child的方法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方法多考慮了個margin,源碼也跟前面的差不多,只是將margin考慮了進去,所以一般情況下,這個方法使用的更多一些。
至此,自定義View的中最重要、最復雜的measure過程就全部總結完了。下面就該Layout過程了。
onLayout
類似measure過程,layout過程根據View的類型分為2種情況,單一View和ViewGroup。對于單身View來說,一人吃飽全家不餓,調用layout方法確定好自己的位置,設置好位置屬性的值(mLeft/mRgiht,mTop/mBottom)就行。而對于父母ViewGroup來說,還得通過調用onLayout方法幫助孩子們確定好位置。
單一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;
}
//當前視圖的四個頂點
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
//調用setFrame / setOpticalFrame方法來給View的四個頂點屬性賦值,
//即mLeft,mRight,mTop,mBottom四個值
//判斷當前View大小和位置是否發生了變化 & 返回
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//調用onLayout方法
onLayout(changed, l, t, r, b);
if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
//監聽View位置變化
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
notifyEnterOrExitForAutoFillIfNeeded(true);
}
}
盡管代碼有點長,不過不難看出layout方法首先會調用isLayoutModeOptical這個方法,判斷是否有光學邊界的(光學邊界這里暫時用不到,其實關鍵是我也不會,想深入了解的請自行谷歌),之后調用setFrame或者setOpticalFrame方法來給View的四個頂點屬性賦值,即mLeft,mRight,mTop,mBottom四個值。那么我們再來看看setOpticalFrame方法:
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);
}
可以看到,這個setOpticalFrame方法,最終也是調用了setFrame,那好我們可以直接繼續看setFrame方法了:
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
if (DBG) {
Log.d("View", 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);
//省略部分代碼
return changed;
}
不難看出,setFrame中先比較了新位置和老位置是否有差異,如果有差異則會調用sizechanged來更新View的位置。
setFrame后這個View的位置就確定了。之后我們也就能通過調用getWidth()和getHeight()方法來獲取View的實際寬高了。
然后,才會調用onLayout方法,由于單一View是沒有子View的,因此在View類里的onLayout方法是個空方法。
另外,在layout方法的最后我們能看到一個OnLayoutChangeListener的集合,光看名字我們也知道,這是View位置發生改變時的回調接口。所以我們可以通過addOnLayoutChangeListener方法可以監聽一個View的位置變化,并做出想要的響應。(不看源碼根本不知道還有這樣的方法。。。)
ViewGroup的layout過程
ViewGroup的layout過程就比View復雜一些了,大致分為兩步,首先用layout方法計算自身ViewGroup的位置,之后在onLayout中遍歷子View并且確定自身子View在ViewGroup的位置(調用子View 的 layout方法)。
其實,ViewGroup的Layout的關鍵就是實現onLayout方法,在ViewGroup中onLayout方法被聲明成了抽象方法,這就強制繼承ViewGroup的類都得自己去實現自己定位子元素的邏輯。
由于onLayout方法之前的流程和View是一樣的,因此就不再贅述了。
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
// changed 當前View的大小和位置改變了
// left 左部位置
// top 頂部位置
// right 右部位置
// bottom 底部位置
// 遍歷子View:循環所有子View
for (int i=0; i<getChildCount(); i++) {
View child = getChildAt(i);
// 計算當前子View的四個位置值
//位置的計算邏輯
//TODO
// 需自己實現,也是自定義View的關鍵
// 對計算后的位置值進行賦值
int mLeft = Left
int mTop = Top
int mRight = Right
int mBottom = Bottom
// 根據上述4個位置的計算值,設置子View的4個頂點:調用子view的layout() & 傳遞計算過的參數
// 即確定了子View在父容器的位置
child.layout(mLeft, mTop, mRight, mBottom);
// 該過程類似于單一View的layout過程中的layout()和onLayout(),此處不作過多描述
}
}
}
onDraw
終于,在測量完畢,布局完成之后,我們來到了View繪制的最后一步,那就是將View繪制到屏幕上。不論是View還是ViewGroup,都是調用draw方法完成繪制,我們來看看draw的源碼:
public void draw(Canvas canvas) {
//省略部分代碼
int saveCount;
// 步驟1: 繪制本身View背景
if (!dirtyOpaque) {
drawBackground(canvas);
}
final int viewFlags = mViewFlags;
if (!verticalEdges && !horizontalEdges) {
// 步驟2:繪制本身View內容
if (!dirtyOpaque)
onDraw(canvas);
// View 中:默認為空實現,需復寫
// ViewGroup中:需復寫
// 步驟3:繪制子View
// 由于單一View無子View,故View 中:默認為空實現
// ViewGroup中:系統已經復寫好對其子視圖進行繪制我們不需要復寫
dispatchDraw(canvas);
// 步驟4:繪制裝飾,如滑動條、前景色等等
onDrawScrollBars(canvas);
return;
}
//省略部分代碼
}
和Measure以及Layout過程一樣,View和ViewGroup是有些區別的,不過根據draw方法的源碼來看,整體流程都是下面幾個步驟:
- 繪制背景 -- drawBackground()
- 繪制自己 -- onDraw()
- 繪制孩子 -- dispatchDraw()
- 繪制裝飾 -- onDrawScrollbars()
View和ViewGroup最大的差別就是dispatchDraw()方法,由于View中不用考慮子View,那么dispatchDraw()就是一個空實現,而ViewGroup則必須要實現dispatchDraw()。
那么我們再來一步一步看看源碼中都做了什么:
1. 繪制背景 -- drawBackground()
private void drawBackground(Canvas canvas) {
//mBackground是該View的背景參數,比如背景顏色
// 獲取背景 drawable
final Drawable background = mBackground;
//沒有背景則直接結束方法
if (background == null) {
return;
}
//根據在 layout 過程中獲取的 View 位置的四個參數來確定背景的邊界
setBackgroundBounds();
//省略部分代碼
//獲取當前View的mScrollX和mScrollY值
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
// 調用 Drawable 的 draw 方法繪制背景
background.draw(canvas);
} else {
//如果scrollX和scrollY有值,則對canvas的坐標進行偏移,再繪制背景
canvas.translate(scrollX, scrollY);
// 調用 Drawable 的 draw 方法繪制背景
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
2.繪制自己 -- onDraw()
由于不同的控件都有自己不同的繪制實現,所以View的onDraw方法肯定是空方法。在自定義繪制過程中,需由子類去實現復寫該方法,從而繪制自身的內容。也就是說我們在自定義View的時候要根據實際需求對onDraw方法進行實現。
3.繪制孩子 -- dispatchDraw()
protected void dispatchDraw(Canvas canvas) {
......
// 1. 遍歷子View
final int childrenCount = mChildrenCount;
......
for (int i = 0; i < childrenCount; i++) {
......
if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
transientChild.getAnimation() != null) {
// 繪制子View視圖
more |= drawChild(canvas, transientChild, drawingTime);
}
....
}
}
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
不難看出,dispatchDraw的邏輯就是遍歷繪制子View,即遍歷調用drawChild方法,drawChild方法又調用了child的draw(canvas, this, drawingTime)方法,最后還是調用到了child的draw(canvas)方法,這樣繪制流程也就一層一層的傳遞下去了。
4.繪制裝飾 -- onDrawScrollbars()
public void onDrawForeground(Canvas canvas) {
onDrawScrollIndicators(canvas);
onDrawScrollBars(canvas);
final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
if (foreground != null) {
if (mForegroundInfo.mBoundsChanged) {
mForegroundInfo.mBoundsChanged = false;
final Rect selfBounds = mForegroundInfo.mSelfBounds;
final Rect overlayBounds = mForegroundInfo.mOverlayBounds;
if (mForegroundInfo.mInsidePadding) {
selfBounds.set(0, 0, getWidth(), getHeight());
} else {
selfBounds.set(getPaddingLeft(), getPaddingTop(),
getWidth() - getPaddingRight(), getHeight() - getPaddingBottom());
}
final int ld = getLayoutDirection();
Gravity.apply(mForegroundInfo.mGravity, foreground.getIntrinsicWidth(),
foreground.getIntrinsicHeight(), selfBounds, overlayBounds, ld);
foreground.setBounds(overlayBounds);
}
foreground.draw(canvas);
}
}
這一步的目的是繪制裝飾,如 滾動指示器、滾動條、和前景等。
至此,View的draw過程分析完畢。
總結
View的繪制流程可以總結為下圖:
從View的測量、布局和繪制原理來看,要實現自定義View,根據自定義View的種類不同,可能分別要自定義實現不同的方法。盡管源碼中調用的方法很多,但是這些方法其實不外乎:onMeasure()方法,onLayout()方法,onDraw()方法。
onMeasure()方法:單一View,一般重寫此方法,針對wrap_content情況,規定View默認的大小值,避免于match_parent情況一致。ViewGroup,若不重寫,就會執行和單子View中相同邏輯,不會測量子View。一般會重寫onMeasure()方法,循環測量子View。
onLayout()方法:單一View,不需要實現該方法。ViewGroup必須實現,該方法是個抽象方法,實現該方法,來對子View進行布局。
onDraw()方法:無論單一View,或者ViewGroup都需要實現該方法。
圖片來源:Carson_Ho的自定義View
由于本人水平有限,若是文中有敘述不清晰或不準確的地方,希望大家能夠指出,謝謝大家!