本文來自網(wǎng)絡(luò)店乐,也可能略有改動魏割,如有任何不妥可以聯(lián)系刪除来惧,原文地址 http://www.reibang.com/p/af266ff378c6
本文的目的有兩個:
- 給對自定義 View 感興趣的人一些入門的指引
- 給正在使用自定義 View 的人一些更深入的解析
自定義 View 一直都被認為是 Android 開發(fā)高手的必備技能暇赤,而穩(wěn)中帶皮的學(xué)習(xí) View 的基礎(chǔ)體系心例,這是自定義 View 的必經(jīng)之路宵凌,如果自定義 View 如果設(shè)計的不好或者不考慮性能的話會造成很大的問題鞋囊。所以我們進入 View 工作流程的分析。
一瞎惫、Android 的 UI 層級繪制體系
Android 中的 Activity 是作為應(yīng)用程序的載體存在的溜腐,它代表一個完整的用戶界面并提供了窗口進行視圖繪制。
-
在這里瓜喇,我們這里所說的視圖繪制挺益,實質(zhì)上就是在對 View 及其子類進行操作。而 View 作為視圖控件的頂層父類乘寒,在本文中會對其進行詳細分析望众。我們以 Android 的 UI 層級繪制體系為切入點對 View 進行探究。
圖 1 View 的層級結(jié)構(gòu)
Android 的 UI 層級繪制體系如圖 1 所示伞辛。
繪制體系中做了這些事情 |
---|
①當調(diào)用 Activity 的 setContentView 方法后會調(diào)用 PhoneWindow 類的 setContentView 方法(PhoneWindow 是抽象類 Windiw 的實現(xiàn)類烂翰,Window 用來描述 Activity 視圖最頂端的窗口的顯示內(nèi)容和行為動作)。 |
②PhoneWindow 類的 setContentView 方法中最終會生成一個 DecorView 對象(DectorView 是是 PhoneWindow 的內(nèi)部類蚤氏,繼承自 FrameLayout)甘耿。 |
③DecorView 容器中包含根布局,根布局中包含一個 id 為 content 的 FrameLayout 布局竿滨,Activity 加載布局的 xml 最后通過 LayoutInflater 將 xml 文件中的內(nèi)容解析成 View 層級體系佳恬,最后填加到 id 為 content 的 FrameLayout 布局中。 |
至此于游,View 最終就會顯示到手機屏幕上毁葱。
二、View 的視圖繪制流程剖析
1贰剥、DecorView 被加載到 Window 中
DecorView 被加載到 Window 的過程中倾剿,WindowManager 起到了關(guān)鍵性的作用,最后交給 ViewRootImpl 做詳細處理鸠澈,通過如下的局部 ActivityThread 的源碼分析這一點可以得到印證 (在這里我只展示核心源碼柱告,詳細源碼可以在代碼中查看)截驮。
final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
ActivityClientRecord r = mActivities.get(token);
...
r = performResumeActivity(token, clearHide, reason);
...
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
...
if (r.mPreserveWindow) {
...
ViewRootImpl impl = decor.getViewRootImpl();
...
}
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
wm.addView(decor, l);
}
...
}
...
}
WindowManager 將 DecorView 添加到 PhoneWindow 中,即 addView() 方法執(zhí)行時將視圖添加的動作交給了 ViewRoot际度,ViewRoot 作為接口葵袭,其實現(xiàn)類 ViewRootImpl 具體實現(xiàn)了 addView() 方法,最后乖菱,視圖的具體繪制在 performTraversals() 中展開坡锡,如下圖 2.1 所示:
圖 2.1 View 繪制的代碼層級分析
2、ViewRootImpl 的 performTraversals() 方法完成具體的視圖繪制流程
在源碼中 ViewRootImpl 中視圖具體繪制的流程如下:
private void performTraversals() {
final View host = mView;
if (host == null || !mAdded)
return;
mIsInTraversal = true;
mWillDrawSoon = true;
...
int desiredWindowWidth;
int desiredWindowHeight;
...
if (mFirst) {
mFullRedrawNeeded = true;
mLayoutRequested = true;
if (lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL
|| lp.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD) {
Point size = new Point();
mDisplay.getRealSize(size);
desiredWindowWidth = size.x;
desiredWindowHeight = size.y;
} else {
DisplayMetrics packageMetrics =
mView.getContext().getResources().getDisplayMetrics();
desiredWindowWidth = packageMetrics.widthPixels;
desiredWindowHeight = packageMetrics.heightPixels;
}
}
...
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
...
performDraw();
}
該方法主要流程就體現(xiàn)了 View 繪制渲染的三個主要步驟窒所,分別是測量鹉勒,擺放,繪制三個階段吵取。流程圖如下圖 2.2 所示:
圖 2.2 View 的繪制流程
接下來禽额,我們對于 performMeasure()、performLayout()皮官、 performDraw() 完成具體拆解分析脯倒。實質(zhì)上最后就需要定位到 View 的 onMeasure()、onLayout()捺氢、onDraw() 方法中藻丢。
三、MeasureSpec 在 View 體系中的作用
1摄乒、MeasureSpec 的作用
首先我們從 performMeasure() 入手分析悠反,在上面的內(nèi)容中,我們通過源碼可以看到 performMeasure() 方法中傳入了 childWidthMeasureSpec馍佑、childHeightMeasureSpec 兩個 int 類型的值斋否,performMeasure 方法的源碼如下所示:
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
這兩個值又傳遞到 mView.measure(childWidthMeasureSpec, childHeightMeasureSpec) 方法中,其中 measure 方法的核心源碼如下:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
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);
}
...
if (forceLayout || needsLayout) {
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
resolveRtlPropertiesIfNeeded();
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
onMeasure(widthMeasureSpec, heightMeasureSpec);
...
}
...
}
到這里我們應(yīng)該明確挤茄,childWidthMeasureSpec, childHeightMeasureSpec 是 MeasureSpec 根據(jù)原有寬高計算獲取不同模式下的具體寬高值如叼。
2、MeasureSpec 剖析
MeasureSpec 是 View 的內(nèi)部類穷劈,內(nèi)部封裝了 View 的規(guī)格尺寸笼恰,以及 View 的寬高信息。在 Measure 的流程中歇终,系統(tǒng)會將 View 的 LayoutParams 根據(jù)父容器是施加的規(guī)則轉(zhuǎn)換為 MeasureSpec社证,然后在 onMeasure() 方法中具體確定控件的寬高信息。源碼及分析如下所示:
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
@IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
@Retention(RetentionPolicy.SOURCE)
public @interface MeasureSpecMode {}
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;
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
@MeasureSpecMode 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);
}
@MeasureSpecMode
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
static int adjust(int measureSpec, int delta) {
final int mode = getMode(measureSpec);
int size = getSize(measureSpec);
if (mode == UNSPECIFIED) {
return makeMeasureSpec(size, UNSPECIFIED);
}
size += delta;
if (size < 0) {
Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
") spec: " + toString(measureSpec) + " delta: " + delta);
size = 0;
}
return makeMeasureSpec(size, mode);
}
public static String toString(int measureSpec) {
int mode = getMode(measureSpec);
int size = getSize(measureSpec);
StringBuilder sb = new StringBuilder("MeasureSpec: ");
if (mode == UNSPECIFIED)
sb.append("UNSPECIFIED ");
else if (mode == EXACTLY)
sb.append("EXACTLY ");
else if (mode == AT_MOST)
sb.append("AT_MOST ");
else
sb.append(mode).append(" ");
sb.append(size);
return sb.toString();
}
}
MeasureSpec 的常量中指定了兩種內(nèi)容评凝,一種為尺寸模式追葡,一種為具體的寬高信息。其中高 2 位表示尺寸測量模式,低 30 位表示具體的寬高信息宜肉。
尺寸測量模式有如下三種:
尺寸測量模式的 3 種類型 |
---|
①UNSPECIFIED:未指定模式匀钧,父容器不限制 View 的大小,一般用于系統(tǒng)內(nèi)部的測量 |
②AT_MOST:最大模式谬返,對應(yīng)于在 xml 文件中指定控件大小為 wrap_content 屬性之斯,子 View 的最終大小是父 View 指定的大小值,并且子 View 的大小不能大于這個值 |
③EXACTLY :精確模式遣铝,對應(yīng)于在 xml 文件中指定控件為 match_parent 屬性或者是具體的數(shù)值佑刷,父容器測量出 View 所需的具體大小 |
我?guī)湍憧偨Y(jié)一下
對于每一個 View,都持有一個 MeasureSpec酿炸,MeasureSpec 保存了該 View 的尺寸測量模式以及具體的寬高信息瘫絮,MeasureSpec 受自身的 LayoutParams 和父容器的 MeasureSpec 共同影響。
四填硕、View 的 Measure 流程分析
1麦萤、View 樹的 Measure 測量流程邏輯圖
2、View 的 Measure 流程分析
那么在上文 3.1 的分析中廷支,我們能夠明確在 measure 方法中最后調(diào)用 onMeasure() 方法完成子 View 的具體測量频鉴,onMeasure() 方法的源碼如下所示:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
setMeasuredDimension() 方法在 onMeasure() 中被調(diào)用,被用于存儲測繪的寬度恋拍、高度,而不這樣做的話會觸發(fā)測繪時的異常藕甩。
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);
}
在 setMeasuredDimension() 方法中傳入的是 getDefaultSize()施敢,接著分析 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;
}
通過上文對 MeasureSpec 的分析,在這里我們就能明確狭莱,getDefaultSize 實質(zhì)上就是根據(jù)測繪模式確定子 View 的具體大小僵娃,而對于自定義 View 而言,子 View 的寬高信息不僅由自身決定腋妙,如果它被包裹在 ViewGroup 中就需要具體測量得到其精確值默怨。
3、View 的 Measure 過程中遇到的問題以及解決方案
View 的 measure 過程和 Activity 的生命周期方法不是同步執(zhí)行的骤素,因此無法保證 Activity 執(zhí)行了 onCreate匙睹、onStart、onResume 時某個 View 已經(jīng)測量完畢了济竹。如果 View 還沒有測量完畢痕檬,那么獲得的寬和高都是 0。下面是 3 種解決該問題的方法:
①Activity/View 的 onWindowsChanged() 方法
onWindowFocusChanged() 方法表示 View 已經(jīng)初始化完畢了送浊,寬高已經(jīng)準備好了梦谜,這個時候去獲取是沒問題的。這個方法會被調(diào)用多次,當 Activity 繼續(xù)執(zhí)行或者暫停執(zhí)行的時候唁桩,這個方法都會被調(diào)用闭树,代碼如下:
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
if(hasWindowFocus){
int width=view.getMeasuredWidth();
int height=view.getMeasuredHeight();
}
}
②View.post(runnable) 方法
通過 post 將一個 Runnable 投遞到消息隊列的尾部,然后等待 Looper 調(diào)用此 runnable 的時候 View 也已經(jīng)初始化好了
@Override
protected void onStart() {
super.onStart();
view.post(new Runnable() {
@Override
public void run() {
int width=view.getMeasuredWidth();
int height=view.getMeasuredHeight();
}
});
}
③ViewTreeObsever
使用 ViewTreeObserver 的眾多回調(diào)方法可以完成這個功能荒澡,比如使用 onGlobalLayoutListener 接口蔼啦,當 View 樹的狀態(tài)發(fā)生改變或者 View 樹內(nèi)部的 View 的可見性發(fā)生改變時,onGlobalLayout 方法將被回調(diào)仰猖。伴隨著 View 樹的變化捏肢,這個方法也會被多次調(diào)用。
@Override
protected void onStart() {
super.onStart();
ViewTreeObserver viewTreeObserver=view.getViewTreeObserver();
viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
int width=view.getMeasuredWidth();
int height=view.getMeasuredHeight();
}
});
}
當然饥侵,在這里你可以通過 setMeasuredDimension() 方法對子 View 的具體寬高以及測量模式進行指定鸵赫。
五、View 的 layout 流程分析
1躏升、View 樹的 layout 擺放流程邏輯圖
2辩棒、View 的 layout 流程分析
layout 的作用是 ViewGroup 來確定子元素的位置,當 ViewGroup 的位置被確定后膨疏,在 layout 中會調(diào)用 onLayout 一睁,在 onLayout 中會遍歷所有的子元素并調(diào)用子元素的 layout 方法。
在代碼中設(shè)置 View 的成員變量 mLeft佃却,mTop者吁,mRight,mBottom 的值饲帅,這幾個值是在屏幕上構(gòu)成矩形區(qū)域的四個坐標點复凳,就是該 View 顯示的位置,不過這里的具體位置都是相對與父視圖的位置而言灶泵,而 onLayout 方法則會確定所有子元素位置育八,ViewGroup 在 onLayout 函數(shù)中通過調(diào)用其 children 的 layout 函數(shù)來設(shè)置子視圖相對與父視圖中的位置,具體位置由函數(shù) layout 的參數(shù)決定赦邻。下面我們先看 View 的 layout 方法 (只展示關(guān)鍵性代碼) 如下:
public void layout(int l, int t, int r, int b) {
...
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);
...
}
...
}
六髓棋、View 的 draw 流程分析
1、View 樹的 draw 繪制流程邏輯圖
2惶洲、View 的 draw 流程分析
在 View 的 draw() 方法的注釋中按声,說明了繪制流程中具體每一步的作用,源碼中對于 draw() 方法的注釋如下湃鹊,我們在這里重點分析注釋中除第 2儒喊、第 5 步外的其他步驟。
①View 中的 drawBackground() 繪制背景
核心源碼如下:
private void drawBackground(Canvas canvas) {
final Drawable background = mBackground;
if (background == null) {
return;
}
...
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
如果背景有偏移币呵,實質(zhì)上對畫布首先做偏移處理怀愧,然后在其上進行繪制侨颈。
②View 內(nèi)容的繪制
View 內(nèi)容的繪制源碼如下所示:
protected void onDraw(Canvas canvas) {
}
該方法是空實現(xiàn),就根據(jù)不同的內(nèi)容進行不同的設(shè)置芯义,自定義 View 中就需要重寫該方法加入我們自己的業(yè)務(wù)邏輯哈垢。
③子 View 的繪制
子 View 的繪制源碼如下所示:
protected void dispatchDraw(Canvas canvas) {
}
該方法同樣為空實現(xiàn),而對于 ViewGroup 而言對子 View 進行遍歷扛拨,并最終調(diào)用子 View 的 onDraw 方法進行繪制耘分。
④裝飾繪制
裝飾繪制的源碼如下所示 (只展示核心源碼):
public void onDrawForeground(Canvas canvas) {
onDrawScrollIndicators(canvas);
onDrawScrollBars(canvas);
...
foreground.draw(canvas);
}
很明顯,在這里 onDrawForeground() 方法用于繪制例如 ScrollBar 等其他裝飾绑警,并將它們顯示在視圖的最上層求泰。
七、視圖重繪
1计盒、requestLayout 重新繪制視圖
子 View 調(diào)用 requestLayout 方法渴频,會標記當前 View 及父容器,同時逐層向上提交北启,直到 ViewRootImpl 處理該事件卜朗,ViewRootImpl 會調(diào)用三大流程,從 measure 開始咕村,對于每一個含有標記位的 view 及其子 View 都會進行測量场钉、布局、繪制懈涛。
2逛万、invalidate 在 UI 線程中重新繪制視圖
當子 View 調(diào)用了 invalidate 方法后,會為該 View 添加一個標記位肩钠,同時不斷向父容器請求刷新泣港,父容器通過計算得出自身需要重繪的區(qū)域,直到傳遞到 ViewRootImpl 中价匠,最終觸發(fā) performTraversals 方法,進行開始 View 樹重繪流程 (只繪制需要重繪的視圖)呛每。
3踩窖、postInvalidate 在非 UI 線程中重新繪制視圖
這個方法與 invalidate 方法的作用是一樣的,都是使 View 樹重繪晨横,但兩者的使用條件不同洋腮,postInvalidate 是在非 UI 線程中調(diào)用,invalidate 則是在 UI 線程中調(diào)用手形。
我要總結(jié)了
- 總結(jié)一下
一般來說啥供,如果 View 確定自身不再適合當前區(qū)域,比如說它的 LayoutParams 發(fā)生了改變库糠,需要父布局對其進行重新測量伙狐、擺放涮毫、繪制這三個流程,往往使用 requestLayout贷屎。而 invalidate 則是刷新當前 View罢防,使當前 View 進行重繪,不會進行測量唉侄、布局流程咒吐,因此如果 View 只需要重繪而不需要測量,布局的時候属划,使用 invalidate 方法往往比 requestLayout 方法更高效恬叹。