在開發中自定義控件的使用是比較頻繁的,而自定義控件的基礎之一就是View的測量以及繪製。這篇文章從源碼的角度簡要分析一下View的測量繪製。
在瞭解View繪製流程之前,必須先要了解一個類,MeasureSpec,它是View的內部類,專門來進行對測量的數據和類型進行打包和解包,看一下源碼就會清楚不少:
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) {
return size + mode;
}
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
public static String toString(int measureSpec) {
int mode = getMode(measureSpec);
int size = getSize(measureSpec);
StringBuilder sb = new StringBuilder("MeasureSpec: ");
if (mode == UNSPECIFIED)
sb.append("UNSPECIFIED ");
else if (mode == EXACTLY)
sb.append("EXACTLY ");
else if (mode == AT_MOST)
sb.append("AT_MOST ");
else
sb.append(mode).append(" ");
sb.append(size);
return sb.toString();
}
}
類還是比較簡單,使用getMode和getSize方法對一個32位的int值進行解包,解釋出前2位和後30位分別作爲測量模式和大小的標註。使用makeMeasureSpec來進行打包。這裏暫時只需要知道可以用一個int值來存儲這兩種類型的數據就可以了,後面還會接觸到。
ViewGroup中存放了許多的View,那麼對View的測量一定會經過ViewGroup,在ViewGroup中的measureChildren方法完成了對子View的測量,來看一下:
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方法,進入看一下:
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);
}
可以看到方法中生成了子View的MeasureSpec,然後簡單的調用了子View的measure方法。來看一下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);
}
先解釋一下參數,spec是當前ViewGroup的MeasureSpec,padding是當前ViewGroup的padding,childDimension是子View的大小(可以爲MATCH_PARENT和WRAP_CONTENT)。
當ViewGroup的MeasureSpec是EXACTLY時:
①子View的大小是大於等於0的,那麼給子View設置的大小就是childDimension,MeasureSpec爲EXACTLY。
②子View的大小是MATCH_PARENT,給子View設置的大小是當前ViewGroup大小-padding大小,MeasureSpec爲EXACTLY。
③子View的大小是WRAP_CONTENT,給子View設置的大小是當前ViewGroup的大小-padding大小,MeasureSpec爲AT_MOST。
當ViewGroup的MeasureSpec是AT_MOST時:
①子View的大小是大於等於0的,那麼給子View設置的大小就是childDimension,MeasureSpec爲EXACTLY。
②子View的大小是MATCH_PARENT,給子View設置的大小是當前ViewGroup大小-padding大小,MeasureSpec爲AT_MOST。
③子View的大小是WRAP_CONTENT,給子View設置的大小是當前ViewGroup的大小-padding大小,MeasureSpec爲AT_MOST。
剩下的一種情況是ViewGroup的MeasureSpec爲UNSPECIFIED,這種情況基本不會出現,在這裏就不深入考慮了。瞭解了這些後ViewGroup對子View的測量就完成了,可以看到,子View寬高的測量值是由父ViewGroup和其自身的LayoutParams共同決定的。看完了ViewGroup對子View的分發測量再來看一下View的measure方法:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
widthMeasureSpec != mOldWidthMeasureSpec ||
heightMeasureSpec != mOldHeightMeasureSpec) {
// first clears the measured dimension flag
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
resolveRtlPropertiesIfNeeded();
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
// flag not set, setMeasuredDimension() was not invoked, we raise
// an exception to warn the developer
if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
throw new IllegalStateException("onMeasure() did not set the"
+ " measured dimension by calling"
+ " setMeasuredDimension()");
}
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
}
在這裏參數就是ViewGroup中創建的,因爲這個方法是final的,我們並不能修改,所以我們只看一下能修改的onMeasure方法:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
在方法中直接調用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;
}
由前面的分析我們知道,基本上不管子View中的LayoutParams設置的大小爲什麼,它的MeasureSpec都會是AT_MOST和EXACTLY中的一個,那麼它最後的測量大小必定等於specSize。並且還由剛纔的分析可以知道,當我們把子View指定爲WRAP_CONTENT時它的默認大小就是覆蓋了整個ViewGroup。大家可以簡單的做個實驗,在這裏就不貼實驗的代碼了。
如果想要讓WRAP_CONTENT起作用,我們需要在onMeasure中加入邏輯的判斷以確定其大小。
在onMeasure方法結束後,調用了setMeasuredDimension方法爲mMeasuredWidth和mMeasuredHeight賦值:
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
我們可以調用getMeasuredWidth對這個測量結果進行獲取(但是必須在onMeasure調用結束後),getMeasuredHeight同理。
既然已經測量過了,現在就來看一下layout方法:
ViewGroup的layout方法也是簡單的調用了父類View的layout方法,所以這裏只看一下View的方法。
public void layout(int l, int t, int r, int b) {
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = 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;
}
在layout中首先判斷了一下View的大小位置是否發生了改變,並且在setFram方法時將傳入的參數賦給本地變量進行保存,而後調用onLayout方法去處理子View,因爲onLayout方法和具體佈局相關,所以在View和ViewGroup中都沒有對其給出實現。
layout基本看完了,下面就是和繪畫相關的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) {
final Drawable background = mBackground;
if (background != null) {
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if (mBackgroundSizeChanged) {
background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
mBackgroundSizeChanged = false;
}
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
}
// 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);
// we're done...
return;
}
}
註釋寫的也比較清楚,繪製背景->繪製自己->繪製子View->繪製ScrollBar,值得一提的是在View中dispatchDraw方法是空的,在ViewGroup中給出了實現。