第四章,View的工作原理
本章主要介紹兩方面的內容
1. View的工作原理
2. 自定義View的實現方式
需要掌握:View的三大流程;View的常見回調方法;View滑動(上一章中的滑動衝突處理)
大綱
ViewRoot 和 DecorView
MeasureSpec
View工作流程
自定義View
初識ViewRoot 和 DecorView
ViewRoot
的實現類是ViewRootImpl
,是鏈接WindowManager
和DecorView
的紐帶,View
的三大流程都是通過ViewRoot
來完成的
一. ViewRoot
View
的繪製流程是從ViewRoot
的performTraversals
開始的.
//這裏面的三大流程中,前面的方法會調用後面的方法,ps:performMeasure會調用measure,measure會調用onMeasure
ViewRoot.performTraversals ->
(performMeasure)measure(onMeasure) ->
(performLayout)layout(onLayout)->
(performDraw)draw(onDraw)
- 其中
measure
用來測量View
的寬和高,measure
後即可獲取到View
測量的寬高.layout
用來確定View
在父容器中的放置位置
,- 而
draw
則負責將View繪製
在屏幕上,draw會
調用dispatchDraw
來對子View
進行draw
,只有draw
方法完成後View
才能顯示在屏幕上.
二. DecorView
DecorView
是一個FrameLayout
DecorView
是一個頂級View
,一般裏面包含一個LinearLayout
,上面的是標題欄
,下面的是內容欄
.setContentView
就是將佈局文件設置到內容區(id爲android.R.id.content
的FrameLayout
中);
理解MeasureSpec
MeasureSpec
有點像測量規格
或者測量說明書
,View
的尺寸和規格受MeasureSpec
和父容器
影響.
系統會將View
的LayoutParams
根據父容器所施加的規則
轉換成對應的MeasureSpec
MeasureSpec
- 一個32位的int值,
SpecMode
(高2位),SpecSize
(低30位).- 有三類
SpecMode
,UNSPECIFIED
(沒有限制),EXACTLY
(精確性),AT_MOST
(不能大於這個值)
MeasureSpec與LayoutParams的關係
對於DecorView
,其MeasureSpec
有窗口尺寸
和自身的LayoutParams
共同決定.
DecorView
的MeasureSpec
創建過程,可以查看ViewRootImpl#measureHierarchy,其中會調用ViewRootImpl#getRootMeasureSpec,- EXACTLY模式下,DecorView大小就是窗口大小
- AT_MOST模式下,DecorView大小不定,不超過窗口大小
- 固定大小(EXACTLY),大小爲LayoutParams中指定的大小.
對於普通View
,其MeasureSpec
有父容器的MeasureSpec
和自身的LayoutParams
共同決定.
- 查看ViewGroup#measureChildWithMargins,其中會調用ViewGroup#getChildMeasureSpec得到子元素的
MeasureSpec
.- 子元素的
MeasureSpec
與父容器的MeasureSpec
和本身的LayoutParams
及View的Margin與Padding
有關.
View工作流程
一. measure 過程
View的measure過程
//View#onMeasure
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
- 一般情況下
getDefaultSize
得到的就是View
測量後的大小. 在
setMeasuredDimension()
方法調用之後,我們才能使用getMeasuredWidth()
和getMeasuredHeight()
來獲取視圖測量出的寬高,以此之前調用這兩個方法得到的值都會是0.View
的最終大小在onLayout
中確定,而測量大小
在onMeasure
中確定,大多數情況下他們是相等的getDefaultSize
一般情況下,返回的是測量後的大小
,在UNSPECIFIED
模式下才返回getSuggestedMinimumWidth()
和getSuggestedMinimumHeight()
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;
}
getSuggestMinimumWidth
對應如下情況:view
沒背景則對應android:minWidth
,如果此屬性沒指定
,則爲0
view
有背景則是minWidth(上面屬性對應的值)和 background的minimumWidth
的最大值,- 通過Drawable#getMinimumWidth看出
background的minimumWidth
返回的是Drawable的原始寬高
.
直接繼承
View的自定義控件
需要重寫onMeasure
方法並設置wrap_content
時的自身大小,否則在佈局中使用wrap_content
時就相當於使用match_parent
解決方法: 在wrap_content時,給View設置一個默認的內部寬/高
//解決示例
@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);
}
}
ViewGroup的measure過程
ViewGroup
是一個抽象類,沒有重寫onMeasure
,提供了一個measurechild
的方法(先拿到子View的LayoutParams,根據LayoutParams確定其MeasureSpec
,接着將MeasureSpec
傳遞給子view
進行測量)
因爲不同的ViewGroup的佈局特性不一樣,導致其測量細節各不相同.
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);
}
在某些極端情況下,系統可能需要多次measure才能確定view的大小.
比較好的習慣是在onLayout
中獲取view的寬高
四種方法獲取View的寬高
通過如下四種方法
獲取View的寬高
Activity/View#onWindowFocusChanged
view.post(runnable)
將runnable
將消息投遞到隊列的尾部ViewTreeObserver
- 手動調用
View#measure
方法;
第四種方式需要根據
LayoutParams
分情況處理
//1. MATCH_PARENT: 無法測量
//2. 具體值 dp/px
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,EXACTLY);
//3. wrap_content
int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,AT_MOST);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,AT_MOST);//理論上能支持的最大值來構造
view.measure(widthMeasureSpec,heightMeasureSpec);
二. layout 過程
layout用於ViewGroup確定子元素的位置.
- layout方法確定view本身的位置,但onLayout確定所有子元素的位置.
- layout中首先通過setFrame來確定l,r,t,b四個位置,接着調用onLayout.
- View 的onLayout是空方法,ViewGroup的onLayout是一個抽象方法,不同ViewGroup的佈局特性不一致.
- 自定義ViewGroup中的onLayout中會遍歷調用子view的layout過程
- getMeasuredWidth和getWidth只是賦值時機不同(測量寬高的賦值時機要稍微早一些),值一般相等
//在自定義view中將r增大,則導致`最終寬高`和`測量寬高`不一致
@Override public void layout(int l, int t, int r, int b) {
super.layout(l, t, r+100, b);
}
三. draw 過程
background.draw (繪製背景)->
onDraw (繪製自己)->
dispatchDraw(draw child)(繪製children) ->
onDrawScrollBars.(繪製裝飾)
dispatchDraw
用於繪製傳遞onDraw
一般是一個空方法,不同的子View/子ViewGroup
繪製過程是不同的.- View的dispatchDraw是空方法,ViewGroup的dispatchDraw有代碼.
setWillNotDraw
:如果一個View不需要繪製任何內容,設置這個標記位true後,系統會進行優化.- 默認情況下View沒有開啓,ViewGroup開啓了setWillNotDraw.
- 當明確知道一個
ViewGroup
需要通過onDraw
來繪製內容時,需要顯示地關閉WILL_NOT_DRAW
標記.
自定義View
- 繼承
View
,實現onDraw
,需要支持wrap_content和padding
處理- 繼承
ViewGroup
,需要處理自己和子元素的測量和佈局過程
.- 繼承特定的
View(TextView)
,不需要處理wrap_content和padding
- 繼承特定
ViewGroup(LinearLayout)
,和2
差不多
需要注意
- 支持
wrap_content
,否則wrap_content
就和match_parent
效果相同.- 處理好
padding
,(需要在draw中處理padding)否則padding屬性是無法起作用的- 直接繼承自
ViewGroup
的控件需要在onMeasure
和onLayout
中處理padding
和margin
.- 不需要使用
handler
,因爲View有post方法- 線程和動畫及時停止
View#onDetachedFromWindow
,否則會導致內存泄漏- 嵌套滑動,需要
衝突處理
,參考第3章