View的繪制流程主要分為散步,measure,layout以及draw,接下來,我們就從源碼角度分析這個步驟。
測量(measure)
測量是繪制的第一步,用來決定View或者ViewGroup的測量寬高,這里講View和ViewGroup分別討論下
1.View的測量過程
概述:View的測量過程相對簡單,直接可以通過onMeasure方法完成測量工作。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
首先通過getSuggestedMinimumHeight獲取View的最小高度(寬度),如果沒設置背景則返回minHeight,否則返回背景的原始高度和minHeight的較大值
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ?
mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
其次,計算出View的測量寬度
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;
}
2.ViewGroup的測量過程
概述:ViewGroup的測量工作分為兩步,完成自己的測量工作,遍歷子View并完成他們的測量工作,因此對應的方法有兩個onMeasure
和measureChildren
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];
//View的狀態不能是GONE
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
每一個子View調用自己的measure
方法
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);
}
onMeasure
測量自己的過程需要根據不同的ViewGroup自己實現,相對簡單的ViewGroup有FrameLayout和LinearLayout,但是FrameLayout的重疊屬性導致分析起來不太清晰,這里拿LinearLayout舉個例子。
區分LinearLayout的布局類型
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) { //垂直布局
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else { //水平布局
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
拿垂直方向距離,計算高度和寬度
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
final int count = getVirtualChildCount();
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
//View為null則直接跳過
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
//View為GONE也直接跳過
if (child.getVisibility() == View.GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
//將divider的高度算到最終高度里
if (hasDividerBeforeChildAt(i)) {
mTotalLength += mDividerHeight;
}
//如果通過權重來分配空間,則根據權重計算mTotalLength
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
totalWeight += lp.weight;
if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
...
} else {
measureChildBeforeLayout(child, i, widthMeasureSpec, 0, heightMeasureSpec, usedHeight);
final int childHeight = child.getMeasuredHeight();
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight +
lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
}
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
// Check against our minimum height
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
//計算LinearLayout的寬度
...
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);
}
measure過程完成后,我們就可以獲取到View的測量寬高,但是,這還不是View的最終寬高,最終寬高是在下面layout
中完成的。
布局(layout)
布局用來確定View或者ViewGroup的位置,如果是ViewGroup的話,與measure
一樣會遍歷所有的子元素并調用layout方法。
1.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) {
//調用View的onLayout方法將View放置到指定位置。
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
//執行onLayoutChangeListener
if (li != null && li.mOnLayoutChangeListeners != null) {
...
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
}
獲取四個頂點的位置
protected boolean setFrame(int left, int top, int right, int bottom) {
...
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
...
//計算新的寬高
int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
//View的大小是否變化
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
//刷新
invalidate(sizeChanged);
//設置新的四個頂點的位置
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
...
}
2.ViewGroup的layout過程
這里依然以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);
}
}
計算自身的位置以及遍歷子View并計算他們的位置
void layoutVertical(int left, int top, int right, int bottom) {
final int paddingLeft = mPaddingLeft;
int childTop;
int childLeft;
// 計算View右側的結束位置
final int width = right - left;
int childRight = width - mPaddingRight;
// 計算留給子View的剩余空間
int childSpace = width - paddingLeft - mPaddingRight;
//計算子View個數
final int count = getVirtualChildCount();
//根據Gravity計算childTop
...
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
//計算子View的測量寬高
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
//計算childLeft
...
//如果有divider,那么childTop要加上它。
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}
childTop += lp.topMargin;
//調用子View的layout方法。
setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
}
}
繪制(draw)
繪制過程比較簡單,主要6步
public void draw(Canvas canvas) {
...
//畫背景
if (!dirtyOpaque) {
drawBackground(canvas);
}
//繪制自己的內容
if (!dirtyOpaque) onDraw(canvas);
//繪制子View(如果有的話。)
dispatchDraw(canvas);
...
//繪制前景和滾動條
onDrawForeground(canvas);
}
獲取View寬高的方法
1.onWindowFocusChanged
該方法的注釋:該方法在得到或者失去焦點時調用,這是確定activity是否對用戶可見的指示器。不過該方法隨著focus的得到和失去會多次調用。
2.ViewTreeObserver
可以通過ViewTreeObserver的接口回調完成View寬高的獲取
3.View.post
通過post將一個runnable扔到消息隊列的末尾,然后調用
測量寬高和真實寬高的區別
測量寬高和真實寬高基本一樣,只不過測量寬高是在measure
時期確定的,而真實寬高是在layout
時期決定的,兩者的確定時期不同。日常開發過程中,我們可以認為是測量寬高就是真實寬高。但不絕對,我們可以在View的Layout方法中手動修改left,top,right,bottom的值導致他們不一樣。