Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android View系統解析(下)

Android View系統解析(下)

編輯:關於Android編程

轉載請注明出處:http://blog.csdn.net/singwhatiwanna/article/details/38426471(來自singwhatiwanna的csdn博客)

Android View系統解析系列:

Android View系統解析(上)

介紹View的基礎知識、View的滑動、彈性滑動、滑動沖突解決方式、事件分發等

Android View系統解析(下)

介紹View的Framework層原理、View的measure / layout / draw三大流程和一些高級技巧

本次主要介紹下半部分,提綱如下

View的繪制過程

measure/layout/draw 工作流程

識別 MeasureSpec 並能夠 make 合適的 MeasureSpec

在渲染前獲取 View 的寬高

構造特殊的 View

自定義View

自定義View分類

自定義 View 須知


一 View的繪制過程

初識 ViewRoot

ViewRoot
對應於 ViewRootImpl 類,是連接 WindowManager 和 DecorView 的紐帶。
ActivityThread 中當 activity 對象被創建好後,會將 DecorView 加入到 Window中同時完成 ViewRootImpl 的創建並建立和 DecorView 的聯系。
root = new ViewRootImpl(view.getContext(), display);
root.setView(view, wparams, panelParentView);
view 的繪制流程從 ViewRoot 的 performTraversals 開始,代碼流程是這樣的:
performMeasure -> measure -> onMeasure
performLayout -> layout -> onLayout
performDraw -> draw -> onDraw

activity 界面的組成

由下圖可知,DecorView作為頂級View,一般情況下它有上下兩部分組成(具體情況會和api版本以及Theme有關),上面是title,下面是content,在activity中我們調用setContentView所設置的view其實就是被加到content中,而如何得到content呢,可以這樣:ViewGroup group = findViewById(R.android.id.content),如何得到我們所設置的view呢,可以這樣:group.getChildAt(0)。同時,通過源碼我們可以知道,DecorView其實是一個FrameLayout。這裡要說明的一點是View層的大部分事件都是從DecorView傳遞到我們的view中的。

\

MeasureSpec

MeasureSpec
封裝了父容器對 view 的布局上的限制,內部提供了寬高的信息( SpecMode 、 SpecSize ),SpecSize是指在某種SpecMode下的參考尺寸,其中SpecMode 有如下三種:
UNSPECIFIED
父容器不對 view 有任何限制,要多大給多大
EXACTLY<喎?/kf/ware/vc/" target="_blank" class="keylink">vc3Ryb25nPjxiciAvPri4yN3G99LRvq287LLis/YgdmlldyDL+dDo0qq1xLTz0KE8YnIgLz48c3Ryb25nPkFUX01PU1Q8L3N0cm9uZz48YnIgLz64uMjdxvfWuLaowcvSu7j2tPPQoaOsIHZpZXcgtcS089ChsrvE3LTz09rV4rj21rU8YnIgLz48c3Ryb25nPk1lYXN1cmVTcGVjcyC1xNLi0uU8L3N0cm9uZz48YnIgLz7NqLn9vasgU3BlY01vZGUgus0gU3BlY1NpemUgtPKw/LPJ0ru49iBpbnQg1rW/ydLUsdzD4rn9tuC1xLbUz/PE2rTmt9bF5KOszqrBy7e9seOy2df3o6zG5MzhuanBy7TysPwgLyC94rD8t723qDxiciAvPjwvcD48aDM+PHN0cm9uZz5NZWFzdXJlU3BlYyC1xMq1z9Y8L3N0cm9uZz48L2gzPjxwPk1lYXN1cmVTcGVjPC9wPjxwPrT6se3Su7j2IDMyIM67IGludCDWtTxiciAvPrjfIDIgzru0+rHtIFNwZWNNb2RlIKOstc0gMzAgzru0+rHtIFNwZWNTaXplPGJyIC8+PC9wPjxwPjwvcD48cD7PwsPmz8i/tNK7z8JNZWFzdXJlU3BlYyDE2rK/tcTSu9Cps6PBv7XEtqjS5aOszai5/c/Cw+a1xLT6wuujrNOmuMOyu8TRwO294k1lYXN1cmVTcGVjtcS5pNf31K3A7TwvcD48cD48L3A+PHByZSBjbGFzcz0="brush:java;">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; 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); }

MeasureSpec 與 LayoutParams

對於 DecorView ,其 MeasureSpec 由窗口的尺寸和其自身的LayoutParams 來共同確定
對於應用層 View ,其 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 來共同決定
MeasureSpec 一旦確定後, onMeasure 中就可以確定自身的寬高

MeasureSpec-DecorView

這裡分析下頂級容器DecorView的MeasureSpec的產生過程

childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

上述代碼描述了DecorView的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;
}
通過上述代碼,頂級容器DecorView的MeasureSpec的產生過程就很明確了,具體來說其遵守如下規則:

根據它的LayoutParams中的寬高的參數來分,

LayoutParams.MATCH_PARENT:其模式為精確模式,大小就是窗口的大小

LayoutParams.WRAP_CONTENT:其模式為最大模式,大小不定,但是不能超過窗口的大小

固定大小(比如100dp):其模式為精確模式,大小為LayoutParams中指定的大小

MeasureSpec- 應用層 View

關於應用層View,這裡是指我們布局中的view,其MeasureSpec的創建遵循下表中的規則

\

針對上表,這裡再做一下具體的說明。前面已經提到,對於應用層 View ,其 MeasureSpec 由父容器的 MeasureSpec 和自
身的 LayoutParams 來共同決定,那麼針對不同的父容器和view本身不同的LayoutParams,view就可以有多種MeasureSpec。這裡簡單說下,當view采用固定寬高的時候,不管父容器的MeasureSpec是什麼,view的MeasureSpec都是精確模式並且其大小遵循Layoutparams中的大小;當view的寬高是match_parent時,這個時候如果父容器的模式是精准模式,那麼view也是精准模式並且其大小是父容器的剩余空間,如果父容器是最大模式,那麼view也是最大模式並且其大小不會超過父容器的剩余空間;當view的寬高是wrap_content時,不管父容器的模式是精准還是最大化,view的模式總是最大化並且大小不能超過父容器的剩余空間。可能大家會發現,在我們的分析中漏掉了Unspecified模式,這個模式主要用於系統內部多次measure的情況下,一般來說,我們不需要關注此模式。

支持 View 的 wrap_content

view#onMeasure 的默認實現
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(),
widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

注意:
通過 onDraw 派生的 View ,需要重寫 onMeasure 並設置 wrap_content 時的自
身大小,否則使用 wrap_content 就相當於用 match_parent 。
原因分析:見上面的表格

那麼如何重寫onMeasure從而讓view支持wrap_content呢?請參看下面的典型代碼,需要注意的是,代碼中的mWidth和mHeight指的是view在wrap_content下的內部規格,而這一規格(寬高)應該由自定義view內部來指定。

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(mWidth, mHeight);
	} else if (widthSpecMode == MeasureSpec.AT_MOST) {
		setMeasuredDimension(mWidth, heightSpecSize);
	} else if (heightSpecMode == MeasureSpec.AT_MOST) {
		setMeasuredDimension(widthSpecSize, mHeight);
	}
}

View的measure 流程

view 的 measure 流程
簡單,直接完成
ViewGroup 的 measure 流程
除了完成自己的 measure ,還會遍歷去調用所有 child 的measure 方法,各個 child 再遞歸去執行這個流程
measure 的直接結果
getMeasuredWidth/Height 可以正確地獲取到
注:某些情況下,系統可能需要多次 measure 才能確定大小

在渲染前獲取 View 的寬高

這是一個比較有意義的問題,或者說有難度的問題,問題的背景為:有時候我們需要在view渲染前去獲取其寬高,典型的情形是,我們想在onCreate、onStart、onResume中去獲取view的寬高。如果大家嘗試過,會發現,這個時候view還沒有measure好,寬高都為0,那到底該怎麼做才能正確獲取其寬高呢,下面給出三種方法

Activity/View#onWindowFocusChanged :這個方法表明,view已經初始化完畢了,寬高已經准備好了
view.post(runnable) :通過post可以將一個runnable投遞到消息隊列的尾部,然後等待looper調用此runnable的時候,view也已經初始化好了
view.measure(int widthMeasureSpec, int heightMeasureSpec) :通過手動去measure來視圖得到view的寬高


前兩種方法都比較好理解也比較簡單,這裡主要介紹下第三種方法的詳細用法:

采用 view.measure 去提前獲取 view 的寬高,根據 view 的 layoutParams 來分
match_parent
直接放棄,無法 measure 出具體的寬高
具體的數值( dp/px )
比如寬高都是 100px ,如下 measure :
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec, heightMeasureSpec);
wrap_content
如下 measure :
int widthMeasureSpec = MeasureSpec.makeMeasureSpec( (1 << 30) - 1, MeasureSpec.AT_MOST);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec( (1 << 30) - 1, MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec, heightMeasureSpec);

注意到(1 << 30) - 1,通過分析MeasureSpec的實現可以知道,view的尺寸使用30位二進制表示的,也就是說最大是30個1即 2^30 - 1,也就是(1 << 30) - 1,在最大化模式下,我們用view理論上能支持的最大值去構造MeasureSpec是合理的。

關於view的measure,網絡上有兩個錯誤的用法,如下,為什麼說是錯誤的,首先違背了系統的內部實現規范(因為無法通過錯誤的MeasureSpec去得出合法的SpecMode從而導致measure出錯),其次不能保證一定能 measure 出正確的結果。

第一種錯誤用法
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(-1, MeasureSpec.UNSPECIFIED);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(-1, MeasureSpec.UNSPECIFIED);
view.measure(widthMeasureSpec, heightMeasureSpec);

第二種錯誤用法
view.measure(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)

View 的 layout 過程

layout 的主要作用
ViewGroup 用來確定子元素的位置。
流程
當 viewgroup 的位置被確定後,它在 onLayout 會遍歷所有的 child 並調用其 layout 。在 layout 中 onLayout 會被調用。
關鍵方法
public void layout(int l, int t, int r, int b)
onLayout(changed, l, t, r, b)

構造特殊的 View

問題:如何讓 getWidth 和 getMeasuredWidth 返回的值不一樣?
private void setChildFrame(View child, int left, int top, int measuredWidth, int measureHeight) {
child.layout(left, top, left + measuredWidth, top + measureHeight);
}
int width = right - left;
int height = bottom - top
方法
在父容器的 onLayout 中通過 child.layout 來放置 view 到任意位置
在自己的 onLayout 中修改 mLeft/mRight/mTop/mBottom

View 的 draw 過程

draw 的大致流程
a. 畫背景 background.draw(canvas)
b. 繪制自己( onDraw )
c. 繪制 children ( dispatchDraw )
d. 繪制裝飾( onDrawScrollBars )
備注:
dispatchDraw 會遍歷調用所有 child 的 draw ,如此 draw 事件就一層層地傳遞了下去

二 自定義 View

自定義View類型

繼承 View 重寫 onDraw
繼承 ViewGroup 派生特定的 Layout
繼承特定的 View (比如 TextView , ListView )
繼承特定的 Layout (比如 LinearLayout )

自定義View須知

讓 view 支持 wrap_content
如果有必要,讓你的 view 支持 padding
盡量不要在 view 中使用 Handler ,沒必要
view 中如果有線程或者動畫,需要及時停止,參考View#onDetachedFromWindow
view 帶有滑動嵌套情形時,需要處理好滑動沖突

更多資料

http://blog.csdn.net/singwhatiwanna
https://github.com/singwhatiwanna

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved