在開發過程中我們經常要進行view的自定義。如果熟練掌握自定義技巧的話就能做出很多控件出來。這篇博客來講講view繪制背后發生的那些事。
### 一, view的基礎知識
## view的繪制概括
首先先說說view繪制的整體過程。?
View繪制的源碼分析 ,它的三大流程都是在ViewRootImpl中完成的,從ViewRootImpl中的performTraversals開始,有三個方法performMeasure,performLayout,prformDraw分別對measure,layout,draw三個方法。在onMeasure對所有子元素進行measure過程 ,這時measure就從父容器傳遞到子元素。子元素重復父元素的過程。layout與draw類似,只是draw通過diapatchDraw來實現 。?
measure完成后可以通過getMeasureWidth,getMeasureHeight分別獲取View測量后的寬高。在實際情況下幾乎所有情況它都等于最終寬高。layout過程決定view的四個頂點的坐標和實際view的寬高,完成之后可以通過getTop,getBottom,getLeft,getRight來拿 到view的四個頂點位置。并通過getWidth()和getHeight()來拿到最終寬高。draw決定了view的顯示,只有完成才能顯示在屏幕上。
## MeasureSpec
在測量過程中系統會將View的LayoutParams根據容器所施加的規則轉換成對應的MeasureSpec,然后再根據這個測量出view。?
Measure是一個32位的int,高2位代表SpecMode,低30位代表SpecSize。SpecMode表示測量模式,SpecSize指在某種測量模式下規格的大小。其代碼如下:
~~~
public static class MeasureSpec {
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中源碼很值得我們學習。他用一個32位的int來表示模式和大小,節省了空間,也更直觀。MeasureSpec通過將specMode和specSize打包成一個int來避免過多的對象內存分配。以上是MeasureSpec的打包和解包過程。?
specMode有三種狀態:UNSPECIFIED,EXACTLY(相當于match_parent和精確值這兩種模式),AT_MOST(wrap_content)。
## LayoutParams
對于一般容器,它的MeasureSpec是由父容器的MeasureSpec和自身的LayoutParams共同決定的。上篇博客[LayoutInflater源碼解析](http://blog.csdn.net/u014486880/article/details/50707672)?我們己經介紹了android view的結構,PhoneWindow包了一層DecorView,DecorView里才是title和我們的content view。所以行分析DecorView。?
先來看下DecorView的產生源碼:
~~~
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
~~~
再看下getRootMeasureSpec方法:
~~~
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;
}
~~~
這里很清楚,分別分MatchPraent和wrap_content和自定義來計算寬高。再來看下普通的view,在ViewGroup的measureChildWIthMargins中:
~~~
protected void measureChildWithMargins (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);
}
~~~
再看下getChildMeasureSpec:
~~~
public static int getChildMeasureSpec(int spec, int padding , int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec. getSize(spec) ;
int size = Math. max( 0, specSize - padding) ;
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec. EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size ;
resultMode = MeasureSpec. EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size ;
resultMode = MeasureSpec. AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension ;
resultMode = MeasureSpec. EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size ;
resultMode = MeasureSpec. AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size ;
resultMode = MeasureSpec. AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension ;
resultMode = MeasureSpec. EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = 0;
resultMode = MeasureSpec. UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = 0;
resultMode = MeasureSpec. UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode) ;
}
~~~
以上表明,如果父是EXACTLY,parentSize,那么子如果是EXACTLY,?
1)具體的值size:那子的MeasureSpec就是EXACTLY,size;?
2)MATCH_PARENT:那子的MeasureSpec就是EXACTLY,parentSize;?
3)WRAP_CONTENT:那子的MeasureSpec就是AT_MOST,parentSize;
如果父是ATMOST,parentSize,那么子如果是EXACTLY,?
1)具體的值size:那子的MeasureSpec就是EXACTLY,size;?
2)MATCH_PARENT:那子的MeasureSpec就是AT_MOST,parentSize;?
3)WRAP_CONTENT:那子的MeasureSpec就是AT_MOST,parentSize;?
總結:對于普通View的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams來共同決定。
## 二,三大過程源碼分析
### OnMeasure
* measure。如果是view,measure繪制其自身。如果是VIewGroup,measure繪制自身外,還要繪制其子元素。先看View的measure方法,measure是一個final方法,不能重寫:
~~~
if (cacheIndex < 0 |if (cacheIndex < 0 || sIgnoreMeasureCache ) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec , heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}| sIgnoreMeasureCache ) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec , heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
~~~
調用了onMeasure(),來看下它的源碼:
~~~
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth() , widthMeasureSpec) ,
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)) ;
}
~~~
看下getSuggestedMinimumWidth(),它就是獲取背景大小和mMinWidth的較大值:
~~~
protected int getSuggestedMinimumWidth () {
return (mBackground == null) ? mMinWidth : max(mMinWidth , mBackground .getMinimumWidth());
}
~~~
那么mMinWidth是什么呢,mMinWidth就是設置的android:minWidth的屬性,沒設置就等于0。不信,看如下代碼:
~~~
case R.styleable.View_minWidth:
mMinWidth = a.getDimensionPixelSize(attr , 0) ;
break;
~~~
getMinimumWidth()表示的是獲取背景圖大小,它位于Drawable下:
~~~
public int getMinimumHeight() {
final int intrinsicHeight = getIntrinsicHeight() ;
return intrinsicHeight > 0 ? intrinsicHeight : 0 ;
}
~~~
看下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;
}
~~~
它返回了specSize,它是測量后的大小。由上面的分析可知,view的寬高由specSize決定,而如果直接繼承View的控件需要重寫onMeasure方法并設置wrap_content的自身大小。否則wrap_content就相當 于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(200, 200);
} else if (widthSpecMode == MeasureSpec. AT_MOST) {
setMeasuredDimension(200, heightSpecSize);
} else if (heightSpecMode == MeasureSpec. AT_MOST) {
setMeasuredDimension(widthSpecSize, 200);
}
}
~~~
上面的200是指定的一個默認寬高。
2.ViewGroup的measure過程,它沒有重寫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);
}
}
}
~~~
分別繪制child,進入measureChild:
~~~
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);
}
~~~
獲取LayoutParams,通過getChildMeasureSpec來創建子無素的MeasureSpec,調用child.measure,因為ViewGroup有不同的特性,所以無法實現統一的onMeasure。
## Layout的過程
viewGroup會遍歷所有子元素并調用 其layout方法,layout方法來確定子元素的位置。viewgroup如下:
~~~
protected abstract void onLayout(boolean changed,
int l , int t, int r, int b) ;
~~~
需要子類自己實現。看下view的layout:
~~~
public void layout(int l, int t , int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT ) != 0) {
onMeasure(mOldWidthMeasureSpec , mOldHeightMeasureSpec) ;
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
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);
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li. mOnLayoutChangeListeners .clone();
int numListeners = listenersCopy.size() ;
for ( int i = 0 ; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange( this, l, t, r , b, oldL, oldT , oldR, oldB);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
~~~
在setFrame中確定了view的四個頂點坐標。mleft等。onLayout view也沒有具體實現,要看具體的。以LinearLayout為例:
~~~
protected void onLayout(boolean changed, int l , int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r , b);
} else {
layoutHorizontal(l, t, r , b);
}
}
~~~
以layoutVertical為例:
~~~
void layoutVertical(int left, int top , int right, int bottom) {
final int paddingLeft = mPaddingLeft ;
int childTop ;
int childLeft ;
// Where right end of child should go
final int width = right - left;
int childRight = width - mPaddingRight ;
// Space available for child
int childSpace = width - paddingLeft - mPaddingRight ;
final int count = getVirtualChildCount() ;
final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
switch (majorGravity) {
case Gravity.BOTTOM:
// mTotalLength contains the padding already
childTop = mPaddingTop + bottom - top - mTotalLength;
break;
// mTotalLength contains the padding already
case Gravity.CENTER_VERTICAL:
childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
break;
case Gravity. TOP:
default :
childTop = mPaddingTop;
break;
}
for (int i = 0; i < count ; i++) {
final View child = getVirtualChildAt(i) ;
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
final int childWidth = child.getMeasuredWidth() ;
final int childHeight = child.getMeasuredHeight();
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams() ;
int gravity = lp. gravity;
if (gravity < 0) {
gravity = minorGravity;
}
final int layoutDirection = getLayoutDirection() ;
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection) ;
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity. CENTER_HORIZONTAL :
childLeft = paddingLeft + ((childSpace - childWidth) / 2)
+ lp. leftMargin - lp.rightMargin ;
break;
case Gravity.RIGHT:
childLeft = childRight - childWidth - lp. rightMargin;
break;
case Gravity.LEFT:
default:
childLeft = paddingLeft + lp. leftMargin;
break;
}
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}
childTop += lp.topMargin;
setChildFrame(child , childLeft, childTop + getLocationOffset(child) ,
childWidth, childHeight);
childTop += childHeight + lp. bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child , i);
}
}
}
~~~
主要看以下代碼:
~~~
final int childWidth = child.getMeasuredWidth() ;
final int childHeight = child.getMeasuredHeight() ;
setChildFrame(child , childLeft, childTop + getLocationOffset(child) ,
childWidth , childHeight);
childTop += childHeight + lp. bottomMargin + getNextLocationOffset(child);
~~~
top會逐漸增大,所以會往下排。setChildFrame僅僅是調用子元素的layout方法。
~~~
private void setChildFrame(View child, int left, int top , int width, int height) {
child.layout(left, top, left + width , top + height);
}
~~~
通過子元素的layout來確定自身。
## draw過程
它有以下幾步:
* 繪制背景,(canvas)
* 繪制自己。(onDraw)
* 繪制children(dispatchDraw)
* 繪制裝飾(onDrawScrollBars)?
看下view的draw源碼:
~~~
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK ) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo .mIgnoreDirtyState );
mPrivateFlags = (privateFlags & ~ PFLAG_DIRTY_MASK ) | PFLAG_DRAWN;
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1\. Draw the background
* 2\. If necessary, save the canvas' layers to prepare for fading
* 3\. Draw view's content
* 4\. Draw children
* 5\. If necessary, draw the fading edges and restore layers
* 6\. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL ) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL ) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas) ;
// Step 4, draw the children
dispatchDraw(canvas) ;
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas) ;
if ( mOverlay != null && !mOverlay.isEmpty()) {
mOverlay .getOverlayView().dispatchDraw(canvas) ;
}
// we're done...
return;
}
~~~
viewgroup中的dispatchDraw用于遍歷子view并調用子view的draw方法。這樣就一層層的傳下去。?
到此源碼分析就結束了。在繪制view的時候經常會在activity中獲得view的寬高,因為activity的生命周期和view不同步,在oncreate中無法獲取到view的寬高,接下來講講activity中如何獲取view。
## 三,view寬高確定
* onWindowFocusChanged:view己經初始化完畢,寬高己經準備好。當Activity得到焦點和失去焦點均會被調用,所以它會調用多次。
* 通過view.post,將一個runnable投弟到消息隊列尾部,等待looper調用時,view己經初始化好。
~~~
protected void onStart(){
super.onStart();
view.post(new Runnable(){
public void run(){
int width = view.getMeasuredWidth();
int height = new .getMeasuredHeight();
}
})
}
~~~
* ViewTreeObserver:?
使用ViewTreeObserver眾多回調接口來完成,如OnGlobalLayoutListener,當view樹狀態發生改變時或內部view可見性發生改變時會回調。
~~~
ViewObserver obserber = view.getViewObserver ();
obserber.addOnGlobalLayoutListener(new OnGlobalLayoutListener(){
public void onGlobalLayout(){
obserber.removeOnGlobalLayoutListener(this);
int width = view.getMeasuredWidth();
int height = new .getMeasuredHeight();
}
})
~~~
* 通過view進行measure來得到view的寬高。
~~~
int width = MeasureSpec.makeMeasureSpec(100,Measure.EXACTLY);//確定值
int height= MeasureSpec.makeMeasureSpec(100,Measure.EXACTLY);//確定值
view.measure(width,height);
對于wrap_content:
int width = MeasureSpec.makeMeasureSpec((1<<30)-1,Measure.AT_MOST);//wrap_content
int height= MeasureSpec.makeMeasureSpec((1<<30)-1,Measure.AT_MOST);//wrap_content
view.measure(width,height);
~~~
## 四,自定義view中注意事項
自定義View需要注意的事項:
* 如果是繼承view或者viewGroup,讓view支持wrap_content。
* 如果有必要,讓view支持padding。
* view中如果有動畫或者線程,要在onDetachedFromWindow中及時停止。當view的Activity退出或者當前view被remove時,調用它。
- 前言
- Android底部tab與標題欄相結合
- Android免費獲取短信驗證碼
- android中Handler的源碼分析
- 詳解Fragment的傳值問題
- 詳談gson解析
- android新控件之toolbar,floatingActionButton,SnackBar,CollapsingToolbarLayout
- android自定義控件
- 淺談android的線程池
- Android的消息處理機制,AsyncTask源碼解析
- IPC——android進程間通信
- CoCos2d_android入門所需知道的一切
- Cocos2d_android你所需要知道的一切(下)
- Activity你需要知道的一切
- Activity啟動過程源碼分析
- Data Binding Guide——google官方文檔翻譯(上)
- Data Binding Guide——google官方文檔翻譯(下)
- android TextView實現跑馬燈效果
- android中生成excel
- Volley源碼解析
- LayoutInflater源碼解析
- android發送郵件
- android測試工具MonkeyRunner--google官網翻譯
- android View繪制源碼分析
- Android中Window添加View的底層原理
- 仿美團商品選購下拉菜單實現