在Android的知識體系中,View扮演著很重要的角色,簡單來理解,View是Android在視覺上的呈現。在介面上Android提供了一套GUI庫,裡面有很多控制元件,但是很多時候我們並不滿足於系統提供的控制元件,因為這樣就意味著這應用介面的同類比較嚴重,如何做出與眾不同的效果呢,就是自定義View。
初始ViewRoot和DecorView
首先,要先了解下View的一些基本概念,這樣才能更好理解View的measure、layout和draw過程。
ViewRoot對應於ViewRootImpl類,它是連線WindowManager和DecorView的紐帶,View的三大流程是通過VeiwRoot來完成的。在ActivityThread中,當Activity物件被建立完畢後,會將DecorVeiw新增到Window中,同時會建立ViewRootImpl物件,並將ViewRootImpl物件和DecorView建立關聯。
View的繪製流程是從ViewRoot的performTraversals方法開始的,它經過measure、layout和draw三個過程才能最終將一個View繪製出來。如圖:
從中,我們可以看到,performTraversals會依次呼叫performMeasure、performLayout、performDraw三個方法,這個三個方法分別完成頂級View的measure、layout、draw這三大方法,在onMeasure方法中則會對所有的子元素進行measure過程,這個時候measure流程就從父容器傳遞到子元素中了,這樣就完成一次measure過程,接著子元素會重複父容器的過程,如此反覆就完成了整個View樹的遍歷。同理,其他兩個步驟也是類似的過程。
measure過程決定了View的寬和高,Measure完成以後,可以通過getMeasureWidth和getMeasureHeight方法來獲取到View的測量後的寬和高。
理解MeasureSpec
確切來說,MeasureSpec在很大程度上決定了一個View的尺寸規格,之所以說是很大程度上是因為這個過程還是受父容器的影響,因為父容器影響View的MeasureSpec的建立過程。在測量過程中,系統會將View的LayoutParams根據父容器所施加的規則轉換成對應的MeasureSpec,然後再根據這個measureSpec來測量出View的寬和高。
MeasureSpec代表一個32位int值,高2位代表SpecMode,低30位代表SpecSize,SpecMode是指測量模式,而SpecSize是指在某種測量模式下的規格大小。MeasureSpec通過將SpecMode和SpecSize打包成一個int值來避免過多的物件記憶體分配,為了方便操作,其提供了打包和解包的方法。
SpecMode有三類,每一類都表示了特殊的含義。
- UNSPECIFIED。父容器不對View有任何限制,要多大就給多大,這種情況一般用於系統內部,表示一種測量的狀態。
- EXACTLY。父容器已經檢測出View所需要的精確大小,這個時候View的最終大小就是SpecSize所指定的值。它對應於LayoutParams中的match_parent和具體的數值這兩種模式。
- AT_MOST。父容器指定了一個可用大小即SpecSize,View的大小不能大於這個值,具體是什麼值要看不同的View的具體實現。它對應於LayoutParams中的wrap_content。
簡單來說,當View採用固定寬/高的時候,不管父容器的MeasureSpec是什麼,View的MeasureSpec都是EXACTLY模式並且大小遵循LayoutParams中的大小,當View的寬/高是match_parent時,如果父容器的模式是EXACTLY,那麼View也是精確模式並且其大小是父容器的剩餘空間,如果父容器是最大模式,那麼View也是最大模式並且其大小不會超過父容器的剩餘空間,當View的寬/高是wrap_content時,不管父容器的模式是精確還是最大化,View的模式總是最大化並且大小不能超過父容器的剩餘空間。
View的工作流程
View的工作流程主要是指measure、layout、draw這三大流程,即測量、佈局、繪製,其中measure確定View的測量寬/高,layout確定View的最終寬/高和四個頂點的位置,而draw則將View繪製到螢幕上。
measure過程
measure過程要分情況來看,如果是一個原始的View,那麼通過measure方法就完成了其測量過程,如果是一個ViewGroup,除了完成自己的測量過程外,還會遍歷去呼叫所有子元素的measure方法,各個子元素再遞迴去執行這個流程。
1,View的measure過程
measure方法是一個final型別的方法,這意味著子類不能重寫此方法,在View的measure方法中會去呼叫View的onMeasure方法,所以只需看onMeasure方法即可。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
上面的程式碼很簡潔,但是簡潔並不代表簡單,setMeasuredDimension方法會設定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的最終大小是在layout階段確定的,所以這裡必須要加以區分,但是幾乎所有情況下的View的測量大小和最終大小是相等的。
同時,直接繼承View的自定義控制元件需要重寫onMeasure方法並設定wrap_content時的自身大小,否則在佈局中使用wrap_content就相當於使用match_parent。為什麼呢,如果View在佈局中使用wrap_content,那麼它的specMode是AT_MOST模式,在這種模式下,它的寬/高等於specSize,也就是說,這種情況下的View的specSize是parentSize,而parentSize是父容器中目前當前剩餘使用的大小,也就是父容器當前剩餘的空間大小。
那麼該如何該解決這個問題,很簡單,程式碼如下:
@Override
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);
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, 200);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, 200);
}
}
給wrap_content設定一個預設值,比如都是寬/高都是200px。
2,ViewGroup的measure過程
對於ViewGroup來說,除了完成自己的measure過程以外,還會遍歷去呼叫所有子元素的measure方法,各個子元素再去遞迴執行這個過程。和View不同的是,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);
}
}
}
從上述程式碼來看,ViewGroup在measure時,會對每一個子元素進行measure。具體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);
}
很明顯,measureChild的思想就是取出子元素的LayoutParams,然後再通過getChildMeasureSpec來建立子元素的MeasureSpec,接著將MeasureSpec直接傳遞給View的measure方法進行測量。
Layout過程
Layout的作用是ViewGroup用來確定子元素的位置,當ViewGroup的位置被確定後,它在onLayout中會遍歷所有的子元素並呼叫其layout方法,在layout方法中onLayout方法又會被呼叫。先看下View中的layout方法
@SuppressWarnings({"unchecked"})
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方法的大致流程:首先會通過setFrame方法來設定View的四個頂點的位置,即初始化mLeft、mRight、mTop和mBottom這四個值,View的四個頂點一旦確定,那麼View在父容器中的位置也就確定了,接著就會呼叫onLayout方法,這個方法用途是父容器確定子元素的位置,和onMeasure方法類似,onLayout的具體實現同樣和具體的佈局有關,所以View和ViewGroup均沒有真正實現onLayout方法。
draw過程
draw過程就比較簡單了,它的作用是將View繪製到螢幕上面,View的繪製過程循序以下幾步:
- 繪製背景background.draw(canvas)
- 繪製自己(onDraw)
- 繪製children(dispatchDraw)
- 繪製裝飾(onDrawScrollBars)
這一點看程式碼,就能看出來。
public void draw(Canvas canvas) {
if (mClipBounds != null) {
canvas.clipRect(mClipBounds);
}
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) {
final Drawable background = mBackground;
if (background != null) {
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if (mBackgroundSizeChanged) {
background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
mBackgroundSizeChanged = false;
}
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
}
// 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);
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(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);
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);
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);
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);
canvas.drawRect(right - length, top, right, bottom, p);
}
canvas.restoreToCount(saveCount);
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
}
View的繪製過程的傳遞是通過dispatchDraw來實現的,dispatchDraw會遍歷呼叫所有子元素的draw方法,如此draw事件就一層層地傳遞下去。
自定義View
自定義View是一個綜合的技術體系,它涉及View的層次結構、事件分發機制和View的工作原理等技術細節。
自定義View的分類
- 繼承View重寫onDraw方法。這種方法主要用於實現一些不規則的效果,即這種效果不方便通過佈局的組合方式來達到,往往需要靜態或者動態地顯示一些不規則的圖形。這種方式需要重寫onDraw方法,同時需要自己支援wrap_content,並且padding也需要自己處理。
- 繼承ViewGroup派生特殊的Layout。這種方法主要用於實現自定義的佈局,即除了LinearLayout、RelativeLayout、FrameLayout這幾種系統的佈局之外,我們需要重新定義一種新的佈局。
- 繼承特定的View(比如TextView)。這種方法比較常見,一般是用於擴充套件某種已有的View的功能,比如TextView。
- 繼承特定的ViewGroup(比如LinearLayout)。這種效果看起來很像幾種View組合在一起的時候,可以採用這種方法實現。
自定義View須知
一些具體的注意事項。
- 讓View支援wrap_content
- 如果有必要,讓你的View支援padding
- 儘量不要在View中使用Handler,沒必要
- View中如果有執行緒或者動畫,需要及時停止,參考View#onDetachedFromWindow
- View帶有滑動巢狀情形時,需要處理好滑動衝突
閱讀擴充套件
源於對掌握的Android開發基礎點進行整理,羅列下已經總結的文章,從中可以看到技術積累的過程。
1,Android系統簡介
2,ProGuard程式碼混淆
3,講講Handler+Looper+MessageQueue關係
4,Android圖片載入庫理解
5,談談Android執行時許可權理解
6,EventBus初理解
7,Android 常見工具類
8,對於Fragment的一些理解
9,Android 四大元件之 " Activity "
10,Android 四大元件之" Service "
11,Android 四大元件之“ BroadcastReceiver "
12,Android 四大元件之" ContentProvider "
13,講講 Android 事件攔截機制
14,Android 動畫的理解
15,Android 生命週期和啟動模式
16,Android IPC 機制
17,View 的事件體系
18,View 的工作原理
19,理解 Window 和 WindowManager
20,Activity 啟動過程分析
21,Service 啟動過程分析
22,Android 效能優化
23,Android 訊息機制
24,Android Bitmap相關
25,Android 執行緒和執行緒池
26,Android 中的 Drawable 和動畫
27,RecylerView 中的裝飾者模式
28,Android 觸控事件機制
29,Android 事件機制應用
30,Cordova 框架的一些理解
31,有關 Android 外掛化思考
32,開發人員必備技能——單元測試