View的工作原理
知識準備
ViewRoot
- ViewRoot對應ViewRootImpl類,是連接WindowManager與DecorView的紐帶。
- View的三大流程都是通過ViewRoot完成的。
- ActivityThread中,Activity對象被回收時,會將DecorView添加到Window中,同時創建ViewRootImpl對象,並將ViewRootImpl對象和DecorView對象建立關聯。
//創建ViewRootImpl對象
root = new ViewRootImpl(view.getContext(),display);
//添加關聯
root.setView(view,wparams,panelparentView);
- View繪製流程從ViewRoot的performTraversals方法開始,經過measure、layout、draw三大流程後纔將View繪製出來。
performTraversals方法(8W多行代碼),會依次調用performMeasure、performLayout、performDraw方法(這三個方法分別完成頂級View - DecorView 的measure、layout、draw方法)
其中performMeasure方法會調用measure方法,在measure方法中又會調用onMeasure方法,onMeasure方法則會對所有子元素進行measure,從而達到measure流程從父容器傳遞到子元素中的目的。接着子元素會重複父容器的measure過程,如此反覆完成整個View樹的遍歷。
最後,perfromLayout、performDraw的傳遞過程也是類似的,唯一不同,而performDraw的傳遞過程在draw方法中通過dispatchDraw來實現。
DecorView - 繼承FrameLayout
- DecorView作爲頂級View,一般情況下會包含一個LinearLayout,該LinearLayout分爲上下兩部分,titlebar部分、android.R.id.content部分(所以,setContentView方法其實就是將佈局添加到id爲content的FrameLayout中)正如小標題,DecorView本質是FrameLayout,View層時間都必先經過DecorView然後在傳遞給其中的View。
MeasureSpec - 很大程度決定View的尺寸規格
- MeasureSpec代表一個32位int值,高2位代表SpecMode(測量模式),低30位代表SpecSize(規格大小)
小白科普:Java中int爲4個字節,Android使用第一個高位字節存儲Mode,剩下三個字節存儲Size - 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_parenet和具體數值這兩種模式
*/
public static final int EXACTLY = 1 << MODE_SHIFT;
/**
* 子控件可隨意大小,但不可大於父控件大小,對應於LayoutParams中的 wrap_content
*/
public static final int AT_MOST = 2 << MODE_SHIFT;
/**
* 根據提供的測量模式以及大小創建 measure specification
*/
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
小白科普:<< 是移位運算,3<<30表示的是首先把3變成二進制的11然後右邊補30個0所組成的一個二進制的數。
- MearsureSpec和LayoutParams對應關係
系統內部是通過MeasureSpec進行View測量的,但正常情況下,都是使View指定MeasureSpec。
View測量時候系統會將LayoutParams在父容器約束下轉換成對應的MeasureSpec,然後再根據這個MeasureSpec來確定View測量後的寬高。因此,Measure需要由LayoutParams和父容器一起決定。
另外,對於頂級View(DecorView),其Measure
Spec由窗口尺寸和其自身的LayoutParams共同決定。Measure一旦決定後,onMeasure中in個即可獲得View的測量寬高。
/**
* DecorView中,MeasureSpec產生過程
* 根據LayoutParams劃分,併產生MeasureSpec
*/
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
對於普通View(佈局中的View),View的measure方法需要由ViewGroup傳遞過來
再看看ViewGroup中的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);
}
上述方法,對子元素進行measure,調喲in個子元素measure之前會獲取子元素的MeasureSpec。顯然,子元素MeasureSpec的創建與父容器的MeasureSpec和子元素本身的LayoutParams有關,還與View的margin以及padding有關(具體需要研究ViewGroup的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;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
上述方法,主要根據父容器的MeasureSpec同時結合View本身的LayoutParams來確定子元素的MeasureSpec。
另外注意,子元素可用大小爲父容器尺寸減去padding。
View工作流程
- measure 確定View寬高
- layout確定View最終寬高和四個頂點位置
- draw將View繪製到屏幕
View的生命週期與工作流程
View生命週期示意圖
View工作流程示意圖
探究Measure過程
- 兩種情況,若只是一個原始的View,通過measure方法就完成了測量過程;如果是ViewGroup,除了完成自身的measure過程,還需要遍歷子元素的measure方法,各個子元素遞歸去執行這個部分。(如上面的示意圖所述)
- View的measure方法是一個final類型的方法 - 意味着子類不能重寫該方法,因此仔細研究onMeasure方法的實現效果會更好。
這裏貼出View中Measure方法,有部分註釋,供有興趣的讀者閱讀研究。
/**
* View中的Measure方法
*/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
boolean optical = isLayoutModeOptical(this);
//先判斷當前Mode是不是特例LAYOUT_MODE_OPTICAL_BOUNDS
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 & 0xffffffffL;
if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
widthMeasureSpec != mOldWidthMeasureSpec ||
heightMeasureSpec != mOldHeightMeasureSpec) {
// first clears the measured dimension flag
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
resolveRtlPropertiesIfNeeded();
int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
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
//如果自定義View重寫了onMeasure方法而沒有調用setMeasureDimension()方法,將會在這裏拋出異常
//判斷原理:通過解析狀態位mPrivateFlags,setMeasureDimension()方法會將mPrivateFlags設置爲已計算狀態(PFLAG_MEASURED_DIMENSION_SET),只需要檢查mPrivateFlags是否含有PFLAG_MEASURED_DIMENSION_SET即可。
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;
//計算出的key作爲鍵,量算結果作爲值,將該鍵值對放入成員變量mMeasureCache中,實現本次計算結果的環緩存
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}
好啦,接下來,繼續研究onMeasure
/**
* View中的onMeasure方法
*/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
setMeasuredDimension()方法會設置View寬高測量值,接下來進一步深入,研究getDefaultSize()方法。
/**
* 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;
}
getDefaultSize方法直接就是根據測量模式返回measureSpec中的specSize,而這個specSize就是View測量後的大小。
注意:View測量後大小 與 View最終大小 需要區分,是兩個東西,因爲View最終大小是在layout階段確定的,但兩者幾乎所有情況都是相等的。
接下來,再繼續探究getDefaultSize方法的第一個參數,從onMeasure方法中可知,該參數來源於下面兩個方法
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
上述兩個方法實現原理都一致,判斷有沒有背景,如果有,返回兩者較大的寬高,沒有則返回自己的寬高(android:minwith這個屬性指定的值)。
那麼,問題來了,背景最小寬高原理是什麼?
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
上述代碼中可見,Drawable的原始寬度,如果沒有原始寬度,則返回0。
小白科普:ShapeDrawable無原始寬高,而BimapDrawable有原始寬高(即圖片尺寸)
再談談ViewGroup的measure過程
與上面的主要區別:
1. 除了完成自己measure過程還要遍歷調用子元素的measure方法,各個子元素再遞歸執行該過程。
2. ViewGroup是抽象類,沒有重寫View的onMeasure方法,而是提供了一個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);
}
}
}
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);
}
View在measure過程中會對每一個子元素進行measure。
再細說下,measureChild方法的思路:
1. 取出子元素的LayoutParams
2. 通過getChildMeasureSpec離開創建子元素的MeasureSpec
3. 將MeasureSpec傳遞給View的Measure方法進行測量。
問題:爲什麼ViewGroup不像View一樣對其onMeasure方法做統一實現?
因爲不同的ViewGroup子類會有不同的特性,因此其中的onMeasure細節不相同。
獲取View寬高方法不當,可能會獲取錯誤。
原因:View的measure過程和Activity生命週期方法執行順序是不確定的,無法保證Activity執行了onCreate、onStart、onReasume時,View測量完畢。
如果View還沒有完成測量,則獲取的寬高會是0.
給出四種方法解決:onWindowFocusChanged - 該方法被調用時候,View已經測量完畢,能夠正確獲取View寬高。
注意:該方法會被調用多次,Activity窗口得到焦點與失去焦點時均會被調用一次(繼續執行,暫停執行)。
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
if(hasWindowFocus){
//獲取寬高
int with = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
}
- view.post(runnable)
通過post將一個runnable投遞到消息隊列隊尾,等待Looper調用此runnable時,View已初始化完畢。
protected void onStart(){
super.onStart();
view.post(new Runnable(){
@override
public void run(){
//獲取寬高
int with = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
})
}
- ViewTreeObserver
該類有衆多回調接口,其中的OnGlobalLayoutListener接口,當View樹狀態發生變化或者View樹內部的View的可見性發生改變,該方法都會被回調,利用此特性,可獲得寬高。
protected void onStart(){
super.onStart();
viewTreeObserver observer = view.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new OnGlobalListener(){
public void onGlobalLayout(){
view.getViewTreeObserver().removeGlobalOnlayoutListener(this);
//獲取寬高
int with = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
})
}
- view.measure(int widthMeasureSpec,int heightMeasureSpec)
手動對View進行獲取,根據View的LayoutParams不同,而採取不同手段。(因不常用,這裏就不詳細說明)
探究layout過程
layout主要作用是ViewGroup用來確定子元素位置(遞歸)。
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
//設定四個頂點位置
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
layout方法流程:
1. setFrame方法設定View四個頂點位置
2. 調用onLayout方法,父容器確定子元素位置
另外,與onMeasure方法相似,onLayout方法也是各不相同的。
onLayout方法(LinearLayout、RelativeLayout等基本控件需要嘗試研究下)
探究draw過程
draw主要作用是將View繪製到屏幕上
繪製過程:
1. 繪製背景(background.draw(canvas))
2. 繪製自己(onDraw)
3. 繪製children(dispatchDraw)
4. 繪製裝飾(onDrawScrollBars)
/**
* View中的draw方法
*/
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);
// 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);
// we're done...
return;
}
/*
* Here we do the full fledged routine...
* (this is an uncommon case where speed matters less,
* this is why we repeat some of the tests that have been
* done above)
*/
boolean drawTop = false;
boolean drawBottom = false;
boolean drawLeft = false;
boolean drawRight = false;
float topFadeStrength = 0.0f;
float bottomFadeStrength = 0.0f;
float leftFadeStrength = 0.0f;
float rightFadeStrength = 0.0f;
// Step 2, save the canvas' layers
int paddingLeft = mPaddingLeft;
final boolean offsetRequired = isPaddingOffsetRequired();
if (offsetRequired) {
paddingLeft += getLeftPaddingOffset();
}
int left = mScrollX + paddingLeft;
int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
int top = mScrollY + getFadeTop(offsetRequired);
int bottom = top + getFadeHeight(offsetRequired);
if (offsetRequired) {
right += getRightPaddingOffset();
bottom += getBottomPaddingOffset();
}
final ScrollabilityCache scrollabilityCache = mScrollCache;
final float fadeHeight = scrollabilityCache.fadingEdgeLength;
int length = (int) fadeHeight;
// clip the fade length if top and bottom fades overlap
// overlapping fades produce odd-looking artifacts
if (verticalEdges && (top + length > bottom - length)) {
length = (bottom - top) / 2;
}
// also clip horizontal fades if necessary
if (horizontalEdges && (left + length > right - length)) {
length = (right - left) / 2;
}
if (verticalEdges) {
topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength()));
drawTop = topFadeStrength * fadeHeight > 1.0f;
bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength()));
drawBottom = bottomFadeStrength * fadeHeight > 1.0f;
}
if (horizontalEdges) {
leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength()));
drawLeft = leftFadeStrength * fadeHeight > 1.0f;
rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength()));
drawRight = rightFadeStrength * fadeHeight > 1.0f;
}
saveCount = canvas.getSaveCount();
int solidColor = getSolidColor();
if (solidColor == 0) {
final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;
if (drawTop) {
canvas.saveLayer(left, top, right, top + length, null, flags);
}
if (drawBottom) {
canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
}
if (drawLeft) {
canvas.saveLayer(left, top, left + length, bottom, null, flags);
}
if (drawRight) {
canvas.saveLayer(right - length, top, right, bottom, null, flags);
}
} else {
scrollabilityCache.setFadeColor(solidColor);
}
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 5, draw the fade effect and restore layers
final Paint p = scrollabilityCache.paint;
final Matrix matrix = scrollabilityCache.matrix;
final Shader fade = scrollabilityCache.shader;
if (drawTop) {
matrix.setScale(1, fadeHeight * topFadeStrength);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, top, right, top + length, p);
}
if (drawBottom) {
matrix.setScale(1, fadeHeight * bottomFadeStrength);
matrix.postRotate(180);
matrix.postTranslate(left, bottom);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, bottom - length, right, bottom, p);
}
if (drawLeft) {
matrix.setScale(1, fadeHeight * leftFadeStrength);
matrix.postRotate(-90);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, top, left + length, bottom, p);
}
if (drawRight) {
matrix.setScale(1, fadeHeight * rightFadeStrength);
matrix.postRotate(90);
matrix.postTranslate(right, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(right - length, top, right, bottom, p);
}
canvas.restoreToCount(saveCount);
// 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);
}
View的繪製過程傳遞是通過dispatchDraw來實現,dispatchDraw會遍歷所有子元素的draw方法,另外,View還有一個特殊的方法setWillNotDraw
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
setFlags - 該方法可設置優化標記
如果View不需要繪製任何內容,那麼將設置標記爲true,系統會相應優化。默認情況下,View不啓用這個標記位,但ViewGroup會默認啓動該優化標記。
而實際開發意義:當我們自定義控件繼承於ViewGroup並且本身不具備繪製功能,則開啓標記。而如果明確知道一個ViewGroup需要通過onDraw來繪製內容時候,則需要顯式關閉WILL_NOT_DRAW這個標記位。