您好,登錄后才能下訂單哦!
View繪制的三大流程,指的是measure(測量)、layout(布局)、draw(繪制)
measure負責確定View的測量寬/高,也就是該View需要占用屏幕的大小,確定完View需要占用的屏幕大小后,就會通過layout確定View的最終寬/高和四個頂點在手機界面上的位置,等通過measure和layout過程確定了View的寬高和要顯示的位置后,就會執行draw繪制View的內容到手機屏幕上。
在詳細介紹這三大流程之前,需要簡單了解一下ViewRootImpl,View繪制的三大步驟都是通過ViewRootImpl實現的,ViewRootImpl是連接WindowManager窗口管理和DecorView頂層視圖的紐帶。View的繪制流程從ViewRootImpl的performTraversals方法開始,順序執行measure、layout、draw這三個流程,最終完成對View的繪制工作,在performTraversals方法中,會調用measure、layout、draw這三個方法,這三個方法內部也會調用其對應的onMeasure、onLayout、onDraw方法,通常我們在自定義View時,也就是重寫的這三個方法來實現View的具體繪制邏輯
下面詳細了解下各個步驟經歷的主要方法(這里貼的源碼版本為API 23)
一、measure
在performTraversals方法中,第一個需要進行的就是measure過程,獲取到必要信息后,performTraversals方法中首先會調用measureHierarchy方法,接著measureHierarchy方法里再去調用performMeasure方法,在performMeasure方法中最終就會去調用View的measure方法,從而開始進行測量過程
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure"); try { mView.measure(childWidthMeasureSpec, childHeightMeasureSpec); } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } }
mView其實指的就是DecorView頂層視圖,從源碼可以看出,measure的遞歸過程就是從DecorView開始的
View和ViewGroup的測量方法有一定區別,View通過measure方法就可以完成自身的測量過程,而ViewGroup不僅需要調用measure方法測量自己,還需要去遍歷其子元素的measure方法,其子元素如果是ViewGroup,則該子元素需使用同樣的方法再次遞歸下去。
View
來看看View是如何測量自己的寬高的
先在View源碼中找到measure方法
public final void measure(int widthMeasureSpec, int heightMeasureSpec) { // ...... if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT || widthMeasureSpec != mOldWidthMeasureSpec || heightMeasureSpec != mOldHeightMeasureSpec) { // ...... if (cacheIndex < 0 || sIgnoreMeasureCache) { // measure ourselves, this should set the measured dimension flag back onMeasure(widthMeasureSpec, heightMeasureSpec); mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } // ..... }
View的measure過程就是通過measure方法來完成,View中的measure方法是由ViewGroup的measureChild方法調用的,ViewGroup在調用該子View的measure方法的同時還傳入了子View的widthMeasureSpec和heightMeasureSpec值。該方法被定義為final類型,也就是說其measure過程是固定的,在measure中調用了onMeasure方法,如果想要自定義測量過程的話,需要重寫onMeasure方法。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
Google在介紹該方法的時候也說了
Measure the view and its content to determine the measured width and the measured height. This method is invoked by {@link #measure(int, int)} and should be overridden by subclasses to provide accurate and efficient measurement of their contents.
該方法需要被子類覆蓋,讓子類提供精準、有效的測量數據,所以我們一般在進行自定義View開發時,需要自定義測量過程就需要復寫此方法。
setMeasuredDimension方法的作用就是設置View的測量寬高,其實我們在使用getMeasuredWidth/getMeasuredHeight 方法獲取的寬高值就是此處設置的值。
如果不復寫此onMeasure方法,則默認使用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; }
可以發現,傳入的measureSpec數值被MeasureSpec解析成了對應的數據,這里簡單介紹下MeasureSpec,它的作用就是告訴View應該以哪一種模式測量這個View,SpecMode有三種模式:
• UNSPECIFIED:表示父容器不對View有任何限制,這種模式主要用于系統內部多次Measure的情況,不需要過多關注
• AT_MOST:父容器已經指定了大小,View的大小不能大于這個值,相當于布局中使用的wrap_content模式
• EXACTLY:表示View已經定義了精確的大小,使用這個指定的精確大小specSize作為該View的大小,相當于布局中我們指定了66dp這種精確數值或者match_parent模式
傳入的measureSpec值經過MeasureSpec.getMode方法獲取它的測量模式,MeasureSpec.getSize方法獲取對應模式下的規格大小,從而確定了其最終的測量大小。
ViewGroup
ViewGroup是一個繼承至View的抽象類,ViewGroup沒有實現測量自己的具體過程,因為其過程是需要各個子類根據自己的需要再具體實現,比如LinearLayout、RelativeLayout等布局的特性都是不同的,不能統一的去管理,所以就交給其子類自己去實現
ViewGroup在measure時,除了實現自身的測量,還需要對它的每個子元素進行measure,在ViewGroup內部提供了一個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); } } }
其中,mChilderenCount指的是該ViewGroup所擁有的子元素的個數,通過一個for循環調用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); }
該方法先通過child.getLayoutParams方法取得子元素的LayoutParams,然后調用getChildMeasureSpec方法計算出該子元素正確的MeasureSpec,再使用child.measure方法把這個MeasureSpec傳遞給View進行測量。
通過這一系列過程,就能讓各個子元素依次進入measure了
二、layout
通過之前的measure過程,View已經測量出了自己需要的寬高大小,performTraversals方法接下來就會執行layout過程
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
layout的過程主要是用來確定View的四個頂點所在屏幕上的位置
layout過程首先從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; }
layout(int l, int t, int r, int b)方法里的四個參數分別指的是左、上、右、下的位置,這四個值是通過ViewRootImpl類里的performTraversals方法傳入的
layout方法用來確定View自身的位置,mLeft、mTop、mBottom、mRight的值最終會由setOpticalFrame和setFrame方法確定,其實setOpticalFrame內部最后也是通過調用setFrame方法設置的
private boolean setOpticalFrame(int left, int top, int right, int bottom) { Insets parentInsets = mParent instanceof View ? ((View) mParent).getOpticalInsets() : Insets.NONE; Insets childInsets = getOpticalInsets(); return setFrame( left + parentInsets.left - childInsets.left, top + parentInsets.top - childInsets.top, right + parentInsets.left + childInsets.right, bottom + parentInsets.top + childInsets.bottom); }
確定完View的四個頂點位置后,就相當于View在父容器中的位置被確定了,接下來會調用onLayout方法,這個方法是沒有具體實現的
protected void onLayout(boolean changed, int left, int top, int right, int bottom) { }
和ViewGroup的onMeasure類似,onLayout方法的具體實現也是需要根據各個View或ViewGroup的特性來決定的,所以源碼中是個空方法,有興趣的可以去看看LinearLayout、RelativeLayout等實現了onLayout方法的ViewGroup子類
之前的measure過程,得到的是測量寬高,而通過onLayout方法,進一步確定了View的最終寬高,一般情況下,measure過程的測量寬高和layout過程確定的最終寬高是一樣的
三、draw
經過以上步驟,View已經確定好了大小和屏幕中顯示的位置,接著就可以繪制自身需要顯示的內容了
在performTraversals方法中,會調用performDraw方法,performDraw方法中調用draw方法,draw方法中接著調用drawSoftware方法
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, boolean scalingRequired, Rect dirty) { // Draw with software renderer. final Canvas canvas; try { ...... canvas = mSurface.lockCanvas(dirty); } try { ...... try { mView.draw(canvas); } } }
首先會通過lockCanvas方法取得一個Canvas畫布對象,接著由mView(DecorView)頂層視圖去調用View的draw方法,并傳入一個Canvas畫布對象
其實Google的工程師已經把draw的繪制過程注釋的非常詳細了
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)
1. 繪制View的背景
如果該View設置了背景,則繪制背景。此背景指的是我們在布局文件中通過android:background屬性,或代碼中使用setBackgroundResource、setBackgroundColor等方法設置的背景圖片或背景顏色
if (!dirtyOpaque) { drawBackground(canvas); }
dirtyOpaque屬性用來判斷該View是否是透明的,如果是透明的則不執行某些步驟,比如繪制背景,繪制內容等
2. 如果有必要的話,保存這個canvas畫布,為該層邊緣的fading效果作準備
第2步和第5步是配套的,我們一般不用管2和5,源碼中的注釋也說了,其中的2和5方法在通常情況下是直接跳過的(skip step 2 & 5 if possible (common case)),其主要作用是實現一些如同View滑動到邊緣時產生的陰影效果,可以不用過多關注
3. 繪制View的內容
該步驟調用了onDraw方法,這個方法是一個空實現
/** * Implement this to do your drawing. * * @param canvas the canvas on which the background will be drawn */ protected void onDraw(Canvas canvas) { }
每個子View需要展示的內容肯定是不相同的,所以onDraw的詳細過程需要子類自己去實現
4. 繪制子View
和第3步一樣,此方法也是一個空實現
/** * Called by draw to draw the child views. This may be overridden * by derived classes to gain control just before its children are drawn * (but after its own view has been drawn). * @param canvas the canvas on which to draw the view */ protected void dispatchDraw(Canvas canvas) { }
對于單純的View來說,它是沒有子View的,所以不需要實現該方法,該方法主要是被ViewGroup重寫了,找到ViewGroup中重寫的dispatchDraw方法
@Override protected void dispatchDraw(Canvas canvas) { ...... for (int i = 0; i < childrenCount; i++) { while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) { final View transientChild = mTransientViews.get(transientIndex); if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE || transientChild.getAnimation() != null) { more |= drawChild(canvas, transientChild, drawingTime); } transientIndex++; if (transientIndex >= transientCount) { transientIndex = -1; } } int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i; final View child = (preorderedList == null) ? children[childIndex] : preorderedList.get(childIndex); if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { more |= drawChild(canvas, child, drawingTime); } } ...... }
在ViewGroup的dispatchDraw方法中通過for循環調用drawChild方法
protected boolean drawChild(Canvas canvas, View child, long drawingTime) { return child.draw(canvas, this, drawingTime); }
drawChild方法里調用子視圖的draw方法,從而讓其子視圖進入draw過程
5. 繪制View邊緣的漸變褪色效果,類似于陰影效果
當第2個步驟保存了canvas畫布后,就可以為這個畫布實現陰影效果
6. 繪制View的裝飾物
View的裝飾物,指的是View除了背景、內容、子View的其它部分,比如滾動條這些
四、常見問題
1.在Activity中獲取View的寬高,得到的值為0
通過上面的measure分析可以知道,View的measure過程和Activity的生命周期方法不是同步的,所以無法保證Activity的某個生命周期執行后View就一定能獲取到值,當我們在View還沒有完成measure過程就去獲取它的寬高,當然獲取不到了,解決這問題的方法有很多,這里推薦使用以下方法
(1)在View的post方法中獲取:
這個方法簡單快捷,推薦使用
mView.post(new Runnable() { @Override public void run() { width = mView.getMeasuredWidth(); height = mView.getMeasuredHeight(); } });
post方法中傳入的Runnable對象將會在View的measure、layout過程后觸發,因為UI的事件隊列是按順序執行的,所以任何post到隊列中的請求都會在Layout發生變化后執行。
(2)使用View的觀察者ViewTreeObserver
ViewTreeObserver是視圖樹的觀察者,其中OnGlobalLayoutListener監聽的是一個視圖樹中布局發生改變或某個視圖的可視狀態發生改變時,就會觸發此類監聽事件,其中onGlobalLayout回調方法會在View完成layout過程后調用,此時是獲取View寬高的好時機
ViewTreeObserver observer = mView.getViewTreeObserver(); observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { mView.getViewTreeObserver().removeGlobalOnLayoutListener(this); width = mScanIv.getMeasuredWidth(); height = mScanIv.getMeasuredHeight(); } });
使用這個方法需要注意,隨著View樹的狀態改變,onGlobalLayout方法會被回調多次,所以在進入onGlobalLayout回調方法時,就移除這個觀察者,保證onGlobalLayout方法只被執行一次就好了
(3)在onWindowFocusChanged回調中獲取
此方法是在View已經初始化完成,measure和layout過程已經執行完成,UI視圖已經渲染完成時被回調,此時View的寬高肯定也已經被確定了,這個時候就可以去獲取View的寬高了
@Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if (hasFocus) { width = mView.getMeasuredWidth(); height = mView.getMeasuredHeight(); } }
這個方法在Activity界面發生變化時也會被多次回調,如果只需要獲取一次寬高的話,建議加上標記加以限制
除了以上方法,還有其它的方法也能獲取到寬高,比如在onClick方法中獲取,手動調用measure方法,使用postDelayed等,了解了View繪制原理后,這些都是很容易就能理解的。
以上這篇淺談Android View繪制三大流程探索及常見問題就是小編分享給大家的全部內容了,希望能給大家一個參考,也希望大家多多支持億速云。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。