#### 4.3.1 measure過程
measure過程要分情況來看,如果只是一個原始的View,那么通過measure方法就完成了其測量過程,如果是一個ViewGroup,除了完成自己的測量過程外,還會遍歷去調用所有子元素的measure方法,各個子元素再遞歸去執行這個流程,下面針對這兩種情況分別討論。
**1.View的measure過程**
View的measure過程由其measure方法來完成,measure方法是一個final類型的方法,這意味著子類不能重寫此方法,在View的measure方法中會去調用View的onMeasure方法,因此只需要看onMeasure的實現即可,View的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這個方法的邏輯很簡單,對于我們來說,我們只需要看AT_MOST和.EXACTLY這兩種情況。簡單地理解,其實getDefaultSize返回的大小就是measureSpec中的specSize,而這個specSize就是View測量后的大小,這里多次提到測量后的大小,是因為View最終的大小是在layout階段確定的,所以這里必須要加以區分,但是幾乎所有情況下View的測量大小和最終大小是相等的。
至于UNSPECIFIED這種情況,一般用于系統內部的測量過程,在這種情況下,View的大小為getDefaultSize的第一個參數size,即寬/高分別為getSuggestedMinimumWidth和getSuggestedMinimumHeight這兩個方法的返回值,看一下它們的源碼:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.
getMinimumWidth());
}
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.
getMinimumHeight());
}
這里只分析getSuggestedMinimumWidth方法的實現,getSuggestedMinimumHeight和它的實現原理是一樣的。從getSuggestedMinimumWidth的代碼可以看出,如果View沒有設置背景,那么View的寬度為mMinWidth,而mMinWidth對應于android:minWidth這個屬性所指定的值,因此View的寬度即為android:minWidth屬性所指定的值。這個屬性如果不指定,那么mMinWidth則默認為0;如果View指定了背景,則View的寬度為max(mMinWidth, mBackground.getMinimumWidth())。mMinWidth的含義我們已經知道了,那么mBackground. getMinimumWidth()是什么呢?我們看一下Drawable的getMinimumWidth方法,如下所示。
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
可以看出,getMinimumWidth返回的就是Drawable的原始寬度,前提是這個Drawable有原始寬度,否則就返回0。那么Drawable在什么情況下有原始寬度呢?這里先舉個例子說明一下,ShapeDrawable無原始寬/高,而BitmapDrawable有原始寬/高(圖片的尺寸),詳細內容會在第6章進行介紹。
這里再總結一下getSuggestedMinimumWidth的邏輯:如果View沒有設置背景,那么返回android:minWidth這個屬性所指定的值,這個值可以為0;如果View設置了背景,則返回android:minWidth和背景的最小寬度這兩者中的最大值,getSuggestedMinimumWidth和getSuggestedMinimumHeight的返回值就是View在UNSPECIFIED情況下的測量寬/高。
從getDefaultSize方法的實現來看,View的寬/高由specSize決定,所以我們可以得出如下結論:直接繼承View的自定義控件需要重寫onMeasure方法并設置wrap_content時的自身大小,否則在布局中使用wrap_content就相當于使用match_parent。為什么呢?這個原因需要結合上述代碼和表4-1才能更好地理解。從上述代碼中我們知道,如果View在布局中使用wrap_content,那么它的specMode是AT_MOST模式,在這種模式下,它的寬/高等于specSize;查表4-1可知,這種情況下View的specSize是parentSize,而parentSize是父容器中目前可以使用的大小,也就是父容器當前剩余的空間大小。很顯然,View的寬/高就等于父容器當前剩余的空間大小,這種效果和在布局中使用match_parent完全一致。如何解決這個問題呢?也很簡單,代碼如下所示。
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指定一個默認的內部寬/高(mWidth和mHeight),并在wrap_content時設置此寬/高即可。對于非wrap_content情形,我們沿用系統的測量值即可,至于這個默認的內部寬/高的大小如何指定,這個沒有固定的依據,根據需要靈活指定即可。如果查看TextView、ImageView等的源碼就可以知道,針對wrap_content情形,它們的onMeasure方法均做了特殊處理,讀者可以自行查看它們的源碼。
**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(parentWidth-
MeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeight-
MeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
很顯然,measureChild的思想就是取出子元素的LayoutParams,然后再通過getChildMeasureSpec來創建子元素的MeasureSpec,接著將MeasureSpec直接傳遞給View的measure方法來進行測量。getChildMeasureSpec的工作過程已經在上面進行了詳細分析,通過表4-1可以更清楚地了解它的邏輯。
我們知道,ViewGroup并沒有定義其測量的具體過程,這是因為ViewGroup是一個抽象類,其測量過程的onMeasure方法需要各個子類去具體實現,比如LinearLayout、RelativeLayout等,為什么ViewGroup不像View一樣對其onMeasure方法做統一的實現呢?那是因為不同的ViewGroup子類有不同的布局特性,這導致它們的測量細節各不相同,比如LinearLayout和RelativeLayout這兩者的布局特性顯然不同,因此ViewGroup無法做統一實現。下面就通過LinearLayout的onMeasure方法來分析ViewGroup的measure過程,其他Layout類型讀者可以自行分析。
首先來看LinearLayout的onMeasure方法,如下所示。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
上述代碼很簡單,我們選擇一個來看一下,比如選擇查看豎直布局的LinearLayout的測量過程,即measureVertical方法,measureVertical的源碼比較長,下面只描述其大概邏輯,首先看一段代碼:
// See how tall everyone is. Also remember max width.
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
...
// Determine how big this child would like to be. If this or
// previous children have given a weight, then we allow it to
// use all available space (and we will shrink things later
// if needed).
measureChildBeforeLayout(
child, i, widthMeasureSpec, 0, heightMeasureSpec,
totalWeight == 0 ? mTotalLength : 0);
if (oldHeight ! = Integer.MIN_VALUE) {
lp.height = oldHeight;
}
final int childHeight = child.getMeasuredHeight();
final int totalLength = mTotalLength;
mTotalLength=Math.max(totalLength, totalLength+childHeight+lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
}
從上面這段代碼可以看出,系統會遍歷子元素并對每個子元素執行measureChild-BeforeLayout方法,這個方法內部會調用子元素的measure方法,這樣各個子元素就開始依次進入measure過程,并且系統會通過mTotalLength這個變量來存儲LinearLayout在豎直方向的初步高度。每測量一個子元素,mTotalLength就會增加,增加的部分主要包括了子元素的高度以及子元素在豎直方向上的margin等。當子元素測量完畢后,LinearLayout會測量自己的大小,源碼如下所示。
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
// Check against our minimum height
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
// Reconcile our calculated size with the heightMeasureSpec
int heightSizeAndState=resolveSizeAndState(heightSize, heightMeasureSpec,
0);
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
...
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec,
childState),
heightSizeAndState);
這里對上述代碼進行說明,當子元素測量完畢后,LinearLayout會根據子元素的情況來測量自己的大小。針對豎直的LinearLayout而言,它在水平方向的測量過程遵循View的測量過程,在豎直方向的測量過程則和View有所不同。具體來說是指,如果它的布局中高度采用的是match_parent或者具體數值,那么它的測量過程和View一致,即高度為specSize;如果它的布局中高度采用的是wrap_content,那么它的高度是所有子元素所占用的高度總和,但是仍然不能超過它的父容器的剩余空間,當然它的最終高度還需要考慮其在豎直方向的padding,這個過程可以進一步參看如下源碼:
public static int resolveSizeAndState(int size, int measureSpec, int
childMeasuredState) {
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:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result | (childMeasuredState&MEASURED_STATE_MASK);
}
View的measure過程是三大流程中最復雜的一個,measure完成以后,通過getMeasured-Width/Height方法就可以正確地獲取到View的測量寬/高。需要注意的是,在某些極端情況下,系統可能需要多次measure才能確定最終的測量寬/高,在這種情形下,在onMeasure方法中拿到的測量寬/高很可能是不準確的。一個比較好的習慣是在onLayout方法中去獲取View的測量寬/高或者最終寬/高。
上面已經對View的measure過程進行了詳細的分析,現在考慮一種情況,比如我們想在Activity已啟動的時候就做一件任務,但是這一件任務需要獲取某個View的寬/高。讀者可能會說,這很簡單啊,在onCreate或者onResume里面去獲取這個View的寬/高不就行了?讀者可以自行試一下,實際上在onCreate、onStart、onResume中均無法正確得到某個View的寬/高信息,這是因為View的measure過程和Activity的生命周期方法不是同步執行的,因此無法保證Activity執行了onCreate、onStart、onResume時某個View已經測量完畢了,如果View還沒有測量完畢,那么獲得的寬/高就是0。有沒有什么方法能解決這個問題呢?答案是有的,這里給出四種方法來解決這個問題:
(1)Activity/View#onWindowFocusChanged。
onWindowFocusChanged這個方法的含義是:View已經初始化完畢了,寬/高已經準備好了,這個時候去獲取寬/高是沒問題的。需要注意的是,onWindowFocusChanged會被調用多次,當Activity的窗口得到焦點和失去焦點時均會被調用一次。具體來說,當Activity繼續執行和暫停執行時,onWindowFocusChanged均會被調用,如果頻繁地進行onResume和onPause,那么onWindowFocusChanged也會被頻繁地調用。典型代碼如下:
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
}
(2)view.post(runnable)。
通過post可以將一個runnable投遞到消息隊列的尾部,然后等待Looper調用此runnable的時候,View也已經初始化好了。典型代碼如下:
protected void onStart() {
super.onStart();
view.post(new Runnable() {
@Override
public void run() {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
}
(3)ViewTreeObserver。
使用ViewTreeObserver的眾多回調可以完成這個功能,比如使用OnGlobalLayoutListener這個接口,當View樹的狀態發生改變或者View樹內部的View的可見性發現改變時,onGlobalLayout方法將被回調,因此這是獲取View的寬/高一個很好的時機。需要注意的是,伴隨著View樹的狀態改變等,onGlobalLayout會被調用多次。典型代碼如下:
protected void onStart() {
super.onStart();
ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
@SuppressWarnings("deprecation")
@Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
}
(4)view.measure(int widthMeasureSpec, int heightMeasureSpec)。
通過手動對View進行measure來得到View的寬/高。這種方法比較復雜,這里要分情況處理,根據View的LayoutParams來分:
**match_parent**
直接放棄,無法measure出具體的寬/高。原因很簡單,根據View的measure過程,如表4-1所示,構造此種MeasureSpec需要知道parentSize,即父容器的剩余空間,而這個時候我們無法知道parentSize的大小,所以理論上不可能測量出View的大小。
**具體的數值(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)
- 前言
- 第1章 Activity的生命周期和啟動模式
- 1.1 Activity的生命周期全面分析
- 1.1.1 典型情況下的生命周期分析
- 1.1.2 異常情況下的生命周期分析
- 1.2 Activity的啟動模式
- 1.2.1 Activity的LaunchMode
- 1.2.2 Activity的Flags
- 1.3 IntentFilter的匹配規則
- 第2章 IPC機制
- 2.1 Android IPC簡介
- 2.2 Android中的多進程模式
- 2.2.1 開啟多進程模式
- 2.2.2 多進程模式的運行機制
- 2.3 IPC基礎概念介紹
- 2.3.1 Serializable接口
- 2.3.2 Parcelable接口
- 2.3.3 Binder
- 2.4 Android中的IPC方式
- 2.4.1 使用Bundle
- 2.4.2 使用文件共享
- 2.4.3 使用Messenger
- 2.4.4 使用AIDL
- 2.4.5 使用ContentProvider
- 2.4.6 使用Socket
- 2.5 Binder連接池
- 2.6 選用合適的IPC方式
- 第3章 View的事件體系
- 3.1 View基礎知識
- 3.1.1 什么是View
- 3.1.2 View的位置參數
- 3.1.3 MotionEvent和TouchSlop
- 3.1.4 VelocityTracker、GestureDetector和Scroller
- 3.2 View的滑動
- 3.2.1 使用scrollTo/scrollBy
- 3.2.2 使用動畫
- 3.2.3 改變布局參數
- 3.2.4 各種滑動方式的對比
- 3.3 彈性滑動
- 3.3.1 使用Scroller7
- 3.3.2 通過動畫
- 3.3.3 使用延時策略
- 3.4 View的事件分發機制
- 3.4.1 點擊事件的傳遞規則
- 3.4.2 事件分發的源碼解析
- 3.5 View的滑動沖突
- 3.5.1 常見的滑動沖突場景
- 3.5.2 滑動沖突的處理規則
- 3.5.3 滑動沖突的解決方式
- 第4章 View的工作原理
- 4.1 初識ViewRoot和DecorView
- 4.2 理解MeasureSpec
- 4.2.1 MeasureSpec
- 4.2.2 MeasureSpec和LayoutParams的對應關系
- 4.3 View的工作流程
- 4.3.1 measure過程
- 4.3.2 layout過程
- 4.3.3 draw過程
- 4.4 自定義View
- 4.4.1 自定義View的分類
- 4.4.2 自定義View須知
- 4.4.3 自定義View示例
- 4.4.4 自定義View的思想
- 第5章 理解RemoteViews
- 5.1 RemoteViews的應用
- 5.1.1 RemoteViews在通知欄上的應用
- 5.1.2 RemoteViews在桌面小部件上的應用
- 5.1.3 PendingIntent概述
- 5.2 RemoteViews的內部機制
- 5.3 RemoteViews的意義
- 第6章 Android的Drawable
- 6.1 Drawable簡介
- 6.2 Drawable的分類
- 6.2.1 BitmapDrawable2
- 6.2.2 ShapeDrawable
- 6.2.3 LayerDrawable
- 6.2.4 StateListDrawable
- 6.2.5 LevelListDrawable
- 6.2.6 TransitionDrawable
- 6.2.7 InsetDrawable
- 6.2.8 ScaleDrawable
- 6.2.9 ClipDrawable
- 6.3 自定義Drawable
- 第7章 Android動畫深入分析
- 7.1 View動畫
- 7.1.1 View動畫的種類
- 7.1.2 自定義View動畫
- 7.1.3 幀動畫
- 7.2 View動畫的特殊使用場景
- 7.2.1 LayoutAnimation
- 7.2.2 Activity的切換效果
- 7.3 屬性動畫
- 7.3.1 使用屬性動畫
- 7.3.2 理解插值器和估值器 /
- 7.3.3 屬性動畫的監聽器
- 7.3.4 對任意屬性做動畫
- 7.3.5 屬性動畫的工作原理
- 7.4 使用動畫的注意事項
- 第8章 理解Window和WindowManager
- 8.1 Window和WindowManager
- 8.2 Window的內部機制
- 8.2.1 Window的添加過程
- 8.2.2 Window的刪除過程
- 8.2.3 Window的更新過程
- 8.3 Window的創建過程
- 8.3.1 Activity的Window創建過程
- 8.3.2 Dialog的Window創建過程
- 8.3.3 Toast的Window創建過程
- 第9章 四大組件的工作過程
- 9.1 四大組件的運行狀態
- 9.2 Activity的工作過程
- 9.3 Service的工作過程
- 9.3.1 Service的啟動過程
- 9.3.2 Service的綁定過程
- 9.4 BroadcastReceiver的工作過程
- 9.4.1 廣播的注冊過程
- 9.4.2 廣播的發送和接收過程
- 9.5 ContentProvider的工作過程
- 第10章 Android的消息機制
- 10.1 Android的消息機制概述
- 10.2 Android的消息機制分析
- 10.2.1 ThreadLocal的工作原理
- 10.2.2 消息隊列的工作原理
- 10.2.3 Looper的工作原理
- 10.2.4 Handler的工作原理
- 10.3 主線程的消息循環
- 第11章 Android的線程和線程池
- 11.1 主線程和子線程
- 11.2 Android中的線程形態
- 11.2.1 AsyncTask
- 11.2.2 AsyncTask的工作原理
- 11.2.3 HandlerThread
- 11.2.4 IntentService
- 11.3 Android中的線程池
- 11.3.1 ThreadPoolExecutor
- 11.3.2 線程池的分類
- 第12章 Bitmap的加載和Cache
- 12.1 Bitmap的高效加載
- 12.2 Android中的緩存策略
- 12.2.1 LruCache
- 12.2.2 DiskLruCache
- 12.2.3 ImageLoader的實現446
- 12.3 ImageLoader的使用
- 12.3.1 照片墻效果
- 12.3.2 優化列表的卡頓現象
- 第13章 綜合技術
- 13.1 使用CrashHandler來獲取應用的crash信息
- 13.2 使用multidex來解決方法數越界
- 13.3 Android的動態加載技術
- 13.4 反編譯初步
- 13.4.1 使用dex2jar和jd-gui反編譯apk
- 13.4.2 使用apktool對apk進行二次打包
- 第14章 JNI和NDK編程
- 14.1 JNI的開發流程
- 14.2 NDK的開發流程
- 14.3 JNI的數據類型和類型簽名
- 14.4 JNI調用Java方法的流程
- 第15章 Android性能優化
- 15.1 Android的性能優化方法
- 15.1.1 布局優化
- 15.1.2 繪制優化
- 15.1.3 內存泄露優化
- 15.1.4 響應速度優化和ANR日志分析
- 15.1.5 ListView和Bitmap優化
- 15.1.6 線程優化
- 15.1.7 一些性能優化建議
- 15.2 內存泄露分析之MAT工具
- 15.3 提高程序的可維護性