簡述
android的界面顯示其實分成兩塊窿锉,一塊了系統(tǒng)的DecorView(頂級view)
和普通view 膜蛔,而DecorView包含一個豎直的LinearLayout伞广,上部分是titleBar ,下部分是一個id為content的frameLayout
轩拨,不管是顯示過程
還是事件分發(fā)
扩氢,都是由它傳遞而來。另一塊就是我們自己設置填充到DecorView中framelayout的view ≌ū埃現(xiàn)在是不是有點體會了既鞠,為什么在給activity設置布局的時候方法是setContentView()
,因為我們的確是把view設置到id為content的FrameLayout當中的矾兜。
不管是DecorView或者是普通view 损趋,要顯示到界面上都要經(jīng)歷三個過程:measure(測量寬高)
、layout(布局位置)
椅寺、draw(繪制)
view的測量過程
View的測量過程決定了它的寬高浑槽,MeasureSpec
參與了view的測量,所以我們有必要詳細了解一下
MeasureSpec
MeasureSpec代表了一個32位的int值返帕,高2位是SpecMode(測量模式)
桐玻,低30位是測量SpecSize(測量尺寸)
,而SpecMode又分為三種荆萤,不同的模式下最終生成的尺寸是不一樣的
AT_MOST (最大模式):父容器指定一個可用的大小即SpecSize镊靴,子view的大小不可超過該尺寸,具體視圖子view的實現(xiàn)而定链韭,對應于
wrap_content
屬性EXACTLY (精準模式):父容器已經(jīng)計算出了確定的值偏竟,子view的最終大小就是SpecSize的值,對應于
match_parent和確定值
UNSPECIFIED (未指定):父容器不對子view做任何限制敞峭,需要多大就給多大踊谋,一般用于系統(tǒng)內(nèi)部,我們就不用太過關心了
現(xiàn)在我們大概了解了什么是MeasureSpec旋讹,那么它是怎樣生成的呢殖蚕?
對于DecorView轿衔,它的MeasureSpec由屏幕尺寸和自身的LayoutParamas
決定。對于普通view睦疫,它的measure過程由ViewGroup的measureChild()
調用
protected void measureChild(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec=getChildMeasureSpec(parentWidthMeasureSpec,mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width);
final int childHeightMeasureSpec=getChildMeasureSpec(parentHeightMeasureSpec+mPaddingTop + mPaddingBottom + lp.topMargin +lp.bottomMargin+heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);}
由此可見害驹,子view的measureSpec由父容器的MeasureSpec、padding/margin蛤育、自身layoutParamas
決定宛官,具體實現(xiàn)由于代碼稍多就不貼出來了,有興趣可以自己研究缨伊≌蹋總結起來就是:
子view的寬高是具體值进宝,SpecMode總是EXACTLY刻坊,
最后的寬高就是設置的值
子view的寬高為match_parent,則SpecMode(測量模式)由父容器決定党晋,
最后的寬高取決于父容器的測量尺寸
子view的寬高為wrap_content谭胚,則SpecMode(測量模式)始終是AT_MOST,
最后的寬高不能超過父容器的剩余空間
View的measure過程
view的measure()
方法是一個final方法未玻,里面調用的是onMeasure()
灾而,如下:
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
setMeasureDimension()
會把view的寬高測量設置進系統(tǒng),再來看看系統(tǒng)默認的處理測量的寬高的方法getDefaultSize()
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
//獲取測量模式和測量值
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
//這種模式主要用于系統(tǒng)的多次測量扳剿,我們不用關心
case MeasureSpec.UNSPECIFIED:
result = size;
break;
//在我們關心的這2中模式下旁趟,返回的其實就是測量值
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
這個方法的邏輯也很簡單,簡單來說就是直接返回了測量值(SpecSize)
庇绽,當然這是系統(tǒng)默認的處理锡搜,我們在自定義view的時候可以重寫onMeasure()
方法,根據(jù)自己的邏輯把測量值設置進去瞧掺。
額外補充一點耕餐,如果我們自定義view的時候默認系統(tǒng)設置測量值的方法,那么wrap_content
的作用效果跟match_parent
一樣辟狈,為什么肠缔?回看一下上面的總結,當view的寬高屬性值為wrap_content
時哼转,測量模式為AT_MOST
明未,測量尺寸為specSize
,就是父容器的剩余可用尺寸
壹蔓,這不就跟match_parent
效果一樣了么趟妥!
那有什么方法能處理么?很簡單庶溶,重寫onMeasure()
煮纵,當寬高屬性為wrap_content
的時候懂鸵,給設置一個默認的大小,ok搞定
@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//獲取寬高的測量模式
int withSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int withSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
//當寬/高為wrap_content即對用AT_MOST的時候行疏,設置一個默認值給系統(tǒng)
if (withSpecMode==MeasureSpec.AT_MOST && heightSpecMode==MeasureSpec.AT_MOST){
setMeasuredDimension(defWith,defHeight);
}else if (withSpecMode==MeasureSpec.AT_MOST){
setMeasuredDimension(defWith,heightSpecSize);
}else {
setMeasuredDimension(withSpecSize,defHeight);
}
}
ViewGroup的measure過程
對于ViewGroup
本身匆光,系統(tǒng)并沒有提供一個默認的onMeasure方法,因為不同特性的ViewGroup
(比如LinearLayout
和RelativeLayout
)他們的測量方式肯定是不一樣的酿联,所以需要子類自己去實現(xiàn)终息。而對于ViewGroup里面的view,則會通過measureChildren()
循環(huán)遍歷每一個view贞让,然后調用measureChild()
(這個方法的邏輯跟measureChildWithMargins()
一模一樣)周崭,如此從而完成測量。
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);
}
}
}
measureChild()
方法的核心是:取出子view的layoutParamas
和自身的MeasureSpec
生成MeasureSpec
傳遞給子view的measure()
方法喳张,完成對子view的測量
獲取View的測量寬/高
measure()
方法完成后续镇,可以通過getMeasureHeight/with
獲取測量的寬高,但是這時取出的值并不一定是最終的值销部,某些情況下系統(tǒng)會多次調用measure才能完成測量摸航。所以最準確的方式是在layout
中獲取測量的寬高值
如果我們想在activity或者fragment中獲取一個view的測量寬高怎么辦?
- 在activity中可在onWindowFocusChanged()方法里面獲取舅桩,缺點就是activity得到或者是去焦點時都會回調酱虎,可用導致頻繁的調用
@Overridepublic void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus){
etUsername.getMeasuredWidth();
}
}
- view.post(runnable)這種方法是極力推薦的,投遞一個runnable到消息隊列擂涛,當looper處理這條消息的時候读串,view也初始化好了
etUsername.post(new Runnable() {
@Override public void run() {
etUsername.getMeasuredWidth();
}
});