View的繪製流程簡單理解
在 Android 中,我們會用到很多控制元件(如:TextView 等)、容器(如:LinearLayout 等);
其中控制元件都是繼承於 View ;而容器都是繼承於 ViewGroup ;那麼這些控制元件、容器是如何顯示到螢幕上的呢?
無論是控制元件還是容器都要經過三個最主要的過程,從而顯示到螢幕上的;即:onMeasure()、onLayout 和 onDraw;
1. onMeasure()
Measure 就是測量的意思,顧名思義就是測量控制元件大小的了。繪製流程是從哪裡來的,怎麼到 onMeasure() 這個方法的呢?
繪製流程的開始是從 ViewRootImpl 中的 performTraversals() 方法開始的,
下面看 View 中的 measure() 方法,下面是官網的原始碼:
17383 /** 17384 * <p> 17385 * This is called to find out how big a view should be. The parent 17386 * supplies constraint information in the width and height parameters. 17387 * </p> 17388 * 17389 * <p> 17390 * The actual measurement work of a view is performed in 17391 * {@link #onMeasure(int, int)}, called by this method. Therefore, only 17392 * {@link #onMeasure(int, int)} can and must be overridden by subclasses. 17393 * </p> 17394 * 17395 * 17396 * @param widthMeasureSpec Horizontal space requirements as imposed by the 17397 * parent 17398 * @param heightMeasureSpec Vertical space requirements as imposed by the 17399 * parent 17400 * 17401 * @see #onMeasure(int, int) 17402 */ 17403 public final void measure(int widthMeasureSpec, int heightMeasureSpec) { 17404 boolean optical = isLayoutModeOptical(this); 17405 if (optical != isLayoutModeOptical(mParent)) { 17406 Insets insets = getOpticalInsets(); 17407 int oWidth = insets.left + insets.right; 17408 int oHeight = insets.top + insets.bottom; 17409 widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth); 17410 heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight); 17411 } 17412 17413 // Suppress sign extension for the low bytes 17414 long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL; 17415 if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2); 17416 17417 if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT || 17418 widthMeasureSpec != mOldWidthMeasureSpec || 17419 heightMeasureSpec != mOldHeightMeasureSpec) { 17420 17421 // first clears the measured dimension flag 17422 mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET; 17423 17424 resolveRtlPropertiesIfNeeded(); 17425 17426 int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 : 17427 mMeasureCache.indexOfKey(key); 17428 if (cacheIndex < 0 || sIgnoreMeasureCache) { 17429 // measure ourselves, this should set the measured dimension flag back 17430 onMeasure(widthMeasureSpec, heightMeasureSpec); 17431 mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; 17432 } else { 17433 long value = mMeasureCache.valueAt(cacheIndex); 17434 // Casting a long to int drops the high 32 bits, no mask needed 17435 setMeasuredDimensionRaw((int) (value >> 32), (int) value); 17436 mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; 17437 } 17438 17439 // flag not set, setMeasuredDimension() was not invoked, we raise 17440 // an exception to warn the developer 17441 if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) { 17442 throw new IllegalStateException("onMeasure() did not set the" 17443 + " measured dimension by calling" 17444 + " setMeasuredDimension()"); 17445 } 17446 17447 mPrivateFlags |= PFLAG_LAYOUT_REQUIRED; 17448 } 17449 17450 mOldWidthMeasureSpec = widthMeasureSpec; 17451 mOldHeightMeasureSpec = heightMeasureSpec; 17452 17453 mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 | 17454 (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension 17455 } 17456 17457 /** 17458 * <p> 17459 * Measure the view and its content to determine the measured width and the 17460 * measured height. This method is invoked by {@link #measure(int, int)} and 17461 * should be overriden by subclasses to provide accurate and efficient 17462 * measurement of their contents. 17463 * </p> 17464 * 17465 * <p> 17466 * <strong>CONTRACT:</strong> When overriding this method, you 17467 * <em>must</em> call {@link #setMeasuredDimension(int, int)} to store the 17468 * measured width and height of this view. Failure to do so will trigger an 17469 * <code>IllegalStateException</code>, thrown by 17470 * {@link #measure(int, int)}. Calling the superclass' 17471 * {@link #onMeasure(int, int)} is a valid use. 17472 * </p> 17473 * 17474 * <p> 17475 * The base class implementation of measure defaults to the background size, 17476 * unless a larger size is allowed by the MeasureSpec. Subclasses should 17477 * override {@link #onMeasure(int, int)} to provide better measurements of 17478 * their content. 17479 * </p> 17480 * 17481 * <p> 17482 * If this method is overridden, it is the subclass's responsibility to make 17483 * sure the measured height and width are at least the view's minimum height 17484 * and width ({@link #getSuggestedMinimumHeight()} and 17485 * {@link #getSuggestedMinimumWidth()}). 17486 * </p> 17487 * 17488 * @param widthMeasureSpec horizontal space requirements as imposed by the parent. 17489 * The requirements are encoded with 17490 * {@link android.view.View.MeasureSpec}. 17491 * @param heightMeasureSpec vertical space requirements as imposed by the parent. 17492 * The requirements are encoded with 17493 * {@link android.view.View.MeasureSpec}. 17494 * 17495 * @see #getMeasuredWidth() 17496 * @see #getMeasuredHeight() 17497 * @see #setMeasuredDimension(int, int) 17498 * @see #getSuggestedMinimumHeight() 17499 * @see #getSuggestedMinimumWidth() 17500 * @see android.view.View.MeasureSpec#getMode(int) 17501 * @see android.view.View.MeasureSpec#getSize(int) 17502 */ 17503 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 17504 setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), 17505 getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); 17506 }
英語好一點的小夥伴看到註釋便能理解 measure() 方法是來做什麼用的了,下面我簡單的翻譯一下原始碼 17383 - 17402 行的註釋是什麼意思:
measure() 方法是為了找出這個檢視是多大,實際測量的工作在 onMeasure(int, int) 中執行;因為 measure() 方法是 final 修飾,所以我們重寫只需要重寫 onMeasure(int, int) 方法即可。widthMeasureSpec表示父節點施加的水平空間要求(即寬度測量),heightMeasureSpec 表示父節點施加的垂直空間要求(即高度測量)。
繼續往下看,在原始碼 17430 行呼叫 onMeasure(int, int) 方法,緊接著原始碼就給出了 onMeasure(int, int) 方法,再看一下原始碼 17457-17502 的註釋是什麼意思:
onMeasure(int, int) 方法主要測量高度和寬度,該方法由 measure() 方法呼叫,提供精確的測量。在重寫此方法時,必須呼叫 setMeasuredDimension(int, int) 來儲存這個檢視的寬度和高度。否則丟擲 IllegalStateException 異常。
在 onMeasure(int, int) 方法中呼叫了 setMeasuredDimension(int, int) 方法和 getDefaultSize() 方法,setMeasuredDimension(int, int) 是來儲存這個檢視的寬度和高度,那麼 getDefaultSize() 方法就是測量檢視的寬度和高度, getDefaultSize() 方法原始碼如下:
17607 /**
17608 * Utility to return a default size. Uses the supplied size if the
17609 * MeasureSpec imposed no constraints. Will get larger if allowed
17610 * by the MeasureSpec.
17611 *
17612 * @param size Default size for this view
17613 * @param measureSpec Constraints imposed by the parent
17614 * @return The size this view should be.
17615 */
17616 public static int getDefaultSize(int size, int measureSpec) {
17617 int result = size;
17618 int specMode = MeasureSpec.getMode(measureSpec);
17619 int specSize = MeasureSpec.getSize(measureSpec);
17620
17621 switch (specMode) {
17622 case MeasureSpec.UNSPECIFIED:
17623 result = size;
17624 break;
17625 case MeasureSpec.AT_MOST:
17626 case MeasureSpec.EXACTLY:
17627 result = specSize;
17628 break;
17629 }
17630 return result;
17631 }
來看一些對 getDefaultSize() 方法的解釋:
如果測量沒有被限制,那麼該方法將返回檢視的預設大小,如果限制,那麼可以根據需要檢視將變得更大。方法中兩個引數:size 是檢視的預設大小;measureSpec 是父類施加的約束。最後返回該檢視的大小。
在 getDefaultSize() 方法中 MeasureSpec 有三種類型,繼續來看一下官網原始碼:
19600 public static class MeasureSpec {
19601 private static final int MODE_SHIFT = 30;
19602 private static final int MODE_MASK = 0x3 << MODE_SHIFT;
19603
19604 /**
19605 * Measure specification mode: The parent has not imposed any constraint
19606 * on the child. It can be whatever size it wants.
19607 */
19608 public static final int UNSPECIFIED = 0 << MODE_SHIFT;
19609
19610 /**
19611 * Measure specification mode: The parent has determined an exact size
19612 * for the child. The child is going to be given those bounds regardless
19613 * of how big it wants to be.
19614 */
19615 public static final int EXACTLY = 1 << MODE_SHIFT;
19616
19617 /**
19618 * Measure specification mode: The child can be as large as it wants up
19619 * to the specified size.
19620 */
19621 public static final int AT_MOST = 2 << MODE_SHIFT;
19622 ...
19623 }
下面來看一下對這三種類型的解釋:
UNSPECIFIED:父節點沒有對子節點施加任何約束。它可以是任意大小。(一般不用)
EXACTLY:父節點為子節點確定了精確的大小。孩子會得到這些界限不管它想要多大。
AT_MOST:子元素可以是它想要的任意大小,直到指定的大小。
到這裡 View 的一次測量便結束了,還不算很複雜。
一般情況下,頁面都是比較複雜的,會包含多個子檢視,下面通過 ViewGroup 來看一下頁面是如何測量的,下面是原始碼:
5396 /**
5397 * Ask all of the children of this view to measure themselves, taking into
5398 * account both the MeasureSpec requirements for this view and its padding.
5399 * We skip children that are in the GONE state The heavy lifting is done in
5400 * getChildMeasureSpec.
5401 *
5402 * @param widthMeasureSpec The width requirements for this view
5403 * @param heightMeasureSpec The height requirements for this view
5404 */
5405 protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
5406 final int size = mChildrenCount;
5407 final View[] children = mChildren;
5408 for (int i = 0; i < size; ++i) {
5409 final View child = children[i];
5410 if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
5411 measureChild(child, widthMeasureSpec, heightMeasureSpec);
5412 }
5413 }
5414 }
5415
5416 /**
5417 * Ask one of the children of this view to measure itself, taking into
5418 * account both the MeasureSpec requirements for this view and its padding.
5419 * The heavy lifting is done in getChildMeasureSpec.
5420 *
5421 * @param child The child to measure
5422 * @param parentWidthMeasureSpec The width requirements for this view
5423 * @param parentHeightMeasureSpec The height requirements for this view
5424 */
5425 protected void measureChild(View child, int parentWidthMeasureSpec,
5426 int parentHeightMeasureSpec) {
5427 final LayoutParams lp = child.getLayoutParams();
5428
5429 final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
5430 mPaddingLeft + mPaddingRight, lp.width);
5431 final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
5432 mPaddingTop + mPaddingBottom, lp.height);
5433
5434 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
5435 }
先看 ViewGroup 中官網對 measureChildren() 方法的解釋:
所有子檢視自己去測量自己,同時考慮該檢視的基本要求及其填充,我們忽略了那些處於隱藏狀態的檢視,以便太繁瑣。
在原始碼5411行呼叫 measureChild() 方法進行測量自己,該方法呼叫 getChildMeasureSpec() 方法分別測量高度和寬度;後面的測量就和測量 View 一樣了。
到這裡,對 ViewGroup 的測量也有所瞭解;
下面來看我寫的一個 demo:
public class MyView extends View {
public MyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//在重寫此方法時,必須呼叫 setMeasuredDimension(int, int) 來儲存這個檢視的寬度和高度。
//否則丟擲 IllegalStateException 異常。
setMeasuredDimension(300,200);
}
...
}
很簡單,裡面只是返回了控制元件的固定值,需要記住的是在重寫控制元件的 onMeasure() 方法一定要重寫 setMeasuredDimension() 方法。
2. onLayout()
測量完畢後,下一步就開始進行佈局了,以 View 為例子看一下原始碼:
16600 /**
16601 * Assign a size and position to a view and all of its
16602 * descendants
16603 *
16604 * <p>This is the second phase of the layout mechanism.
16605 * (The first is measuring). In this phase, each parent calls
16606 * layout on all of its children to position them.
16607 * This is typically done using the child measurements
16608 * that were stored in the measure pass().</p>
16609 *
16610 * <p>Derived classes should not override this method.
16611 * Derived classes with children should override
16612 * onLayout. In that method, they should
16613 * call layout on each of their children.</p>
16614 *
16615 * @param l Left position, relative to parent
16616 * @param t Top position, relative to parent
16617 * @param r Right position, relative to parent
16618 * @param b Bottom position, relative to parent
16619 */
16620 @SuppressWarnings({"unchecked"})
16621 public void layout(int l, int t, int r, int b) {
16622 if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
16623 onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
16624 mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
16625 }
16626
16627 int oldL = mLeft;
16628 int oldT = mTop;
16629 int oldB = mBottom;
16630 int oldR = mRight;
16631
16632 boolean changed = isLayoutModeOptical(mParent) ?
16633 setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
16634
16635 if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
16636 onLayout(changed, l, t, r, b);
16637 mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
16638
16639 ListenerInfo li = mListenerInfo;
16640 if (li != null && li.mOnLayoutChangeListeners != null) {
16641 ArrayList<OnLayoutChangeListener> listenersCopy =
16642 (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
16643 int numListeners = listenersCopy.size();
16644 for (int i = 0; i < numListeners; ++i) {
16645 listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
16646 }
16647 }
16648 }
16649
16650 mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
16651 mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
16652 }
16653
16654 /**
16655 * Called from layout when this view should
16656 * assign a size and position to each of its children.
16657 *
16658 * Derived classes with children should override
16659 * this method and call layout on each of
16660 * their children.
16661 * @param changed This is a new size or position for this view
16662 * @param left Left position, relative to parent
16663 * @param top Top position, relative to parent
16664 * @param right Right position, relative to parent
16665 * @param bottom Bottom position, relative to parent
16666 */
16667 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
16668 }
先看一下官網對 layout() 方法的介紹:
為檢視及其所有子檢視分配大小和位置,這是佈局機制的第二階段(首先是測量)。派生類不應重寫此方法,帶有子類的派生類應該重寫 onLayout() 。四個引數分別為:左位置,頂部位置,右位置和底部位置。
在原始碼16636行,呼叫了 onLayout() 方法,可以發現這個方法是個空方法,來看一下原始碼對 onLayout() 方法的解釋:
從layout呼叫時,該檢視應該為其每個子檢視分配大小和位置。帶有子類的派生類應該重寫此方法,並對其每個子類呼叫佈局。
設定控制元件的位置應該是在裝有控制元件的容器中進行的,那麼在 View 中 onLayout() 裡面是空的也不足為奇了。既然是在容器中進行佈局的,那麼來看一下 ViewGroup 中的 onLayout() 方法:
4973 /**
4974 * {@inheritDoc}
4975 */
4976 @Override
4977 protected abstract void onLayout(boolean changed,
4978 int l, int t, int r, int b);
可以看到,在 ViewGroup 中,onLayout() 方法是一個空的抽象方法,這也就意味著,所有的容器都要重寫 onLayout() 方法進行佈局;那麼我們就來重寫 onLayout() 的方法,如下面程式碼:
public class MyViewGroup extends ViewGroup {
public MyViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (getChildCount() > 0) {
View childView = getChildAt(0);
//測量第一個子View大小
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
if (getChildCount() > 0) {
View childView = getChildAt(0);
//設定第一個子View左、上、右、下的座標
childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());
}
}
}
上面程式碼中,首先重寫 onMeasure() 方法,在裡面拿出第一個子 View 進行測量其大小;其次重寫了 onLayout() 方法,在裡面拿出第一個子 View 設定它的座標。寫完後,就去佈局檔案中試一下:
<com.example.qd.permission.MyViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_width="50dp"
android:layout_height="50dp"
android:src="@mipmap/ic_launcher_round"/>
</com.example.qd.permission.MyViewGroup>
佈局檔案中也很簡單,裡面只有一個 ImageView 控制元件,下面是執行結果:
可以看到正常顯示出來了,我們可以設定它的座標來改變位置。
3. onDraw()
看完前兩個階段,進入到最後一個階段:繪製過程,先看一下原始碼:
16143 /**
16144 * Manually render this view (and all of its children) to the given Canvas.
16145 * The view must have already done a full layout before this function is
16146 * called. When implementing a view, implement
16147 * {@link #onDraw(android.graphics.Canvas)} instead of overriding this method.
16148 * If you do need to override this method, call the superclass version.
16149 *
16150 * @param canvas The Canvas to which the View is rendered.
16151 */
16152 @CallSuper
16153 public void draw(Canvas canvas) {
16154 final int privateFlags = mPrivateFlags;
16155 final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
16156 (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
16157 mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
16158
16159 /*
16160 * Draw traversal performs several drawing steps which must be executed
16161 * in the appropriate order:
16162 *
16163 * 1. Draw the background
16164 * 2. If necessary, save the canvas' layers to prepare for fading
16165 * 3. Draw view's content
16166 * 4. Draw children
16167 * 5. If necessary, draw the fading edges and restore layers
16168 * 6. Draw decorations (scrollbars for instance)
16169 */
16170
16171 // Step 1, draw the background, if needed
16172 int saveCount;
16173
16174 if (!dirtyOpaque) {
16175 drawBackground(canvas);
16176 }
16177
16178 // skip step 2 & 5 if possible (common case)
16179 final int viewFlags = mViewFlags;
16180 boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
16181 boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
16182 if (!verticalEdges && !horizontalEdges) {
16183 // Step 3, draw the content
16184 if (!dirtyOpaque) onDraw(canvas);
16185
16186 // Step 4, draw the children
16187 dispatchDraw(canvas);
16188
16189 // Overlay is part of the content and draws beneath Foreground
16190 if (mOverlay != null && !mOverlay.isEmpty()) {
16191 mOverlay.getOverlayView().dispatchDraw(canvas);
16192 }
16193
16194 // Step 6, draw decorations (foreground, scrollbars)
16195 onDrawForeground(canvas);
16196
16197 // we're done...
16198 return;
16199 }
.... ...
16345 }
可以看出,官網給出瞭解釋和 onDraw() 中的6個步驟:
手動將此檢視(及其所有子檢視)呈現到給定畫布。在呼叫此函式之前,檢視必須已經完成了完整的佈局,在實現檢視時,需要重寫 onDraw() 方法,而不是去重寫這個方法;如果您確實需要重寫此方法,請呼叫超類版本。
再來看一下6個步驟:
繪製遍歷執行幾個繪製步驟,這些步驟必須以適當的順序執行:
1. 畫的背景
2. 如果需要,儲存畫布的圖層以備褪色
3.畫檢視的內容
4. 繪製子檢視
5. 如果需要,繪製衰落邊緣並恢復圖層
6. 繪製裝飾(例如滾動條)
也不再多說什麼了,官網已經解釋的很清楚了;下面來看一下具體的使用方法:
public class MyView extends View {
private Paint mPaint;
public MyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
}
@Override
protected void onDraw(Canvas canvas) {
mPaint.setColor(Color.RED);
//設定背景的座標位置
canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);
mPaint.setColor(Color.BLACK);
mPaint.setTextSize(30);
String text = "wu";
//設定文字座標位置
canvas.drawText(text, 0, getHeight() / 2, mPaint);
}
}
程式碼很簡單,在裡面新建了 Paint 物件,然後在畫布上畫出來我們想要的佈局;在佈局檔案中應用它:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<com.example.qd.permission.MyView
android:layout_width="80dp"
android:layout_height="50dp"/>
</LinearLayout>
來看一下效果圖:
可以看出,雖然辣眼睛,但是效果已經達到了。
到此為止,View 繪製的三個主要流程也就講完了