淺談Android View繪制三大流程探索及常見問題
View繪制的三大流程,指的是measure(測量)、layout(布局)、draw(繪制)
measure負(fù)責(zé)確定View的測量寬/高,也就是該View需要占用屏幕的大小,確定完View需要占用的屏幕大小后,就會(huì)通過layout確定View的最終寬/高和四個(gè)頂點(diǎn)在手機(jī)界面上的位置,等通過measure和layout過程確定了View的寬高和要顯示的位置后,就會(huì)執(zhí)行draw繪制View的內(nèi)容到手機(jī)屏幕上。
在詳細(xì)介紹這三大流程之前,需要簡單了解一下ViewRootImpl,View繪制的三大步驟都是通過ViewRootImpl實(shí)現(xiàn)的,ViewRootImpl是連接WindowManager窗口管理和DecorView頂層視圖的紐帶。View的繪制流程從ViewRootImpl的performTraversals方法開始,順序執(zhí)行measure、layout、draw這三個(gè)流程,最終完成對(duì)View的繪制工作,在performTraversals方法中,會(huì)調(diào)用measure、layout、draw這三個(gè)方法,這三個(gè)方法內(nèi)部也會(huì)調(diào)用其對(duì)應(yīng)的onMeasure、onLayout、onDraw方法,通常我們?cè)谧远xView時(shí),也就是重寫的這三個(gè)方法來實(shí)現(xiàn)View的具體繪制邏輯
下面詳細(xì)了解下各個(gè)步驟經(jīng)歷的主要方法(這里貼的源碼版本為API 23)
一、measure
在performTraversals方法中,第一個(gè)需要進(jìn)行的就是measure過程,獲取到必要信息后,performTraversals方法中首先會(huì)調(diào)用measureHierarchy方法,接著measureHierarchy方法里再去調(diào)用performMeasure方法,在performMeasure方法中最終就會(huì)去調(diào)用View的measure方法,從而開始進(jìn)行測量過程
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其實(shí)指的就是DecorView頂層視圖,從源碼可以看出,measure的遞歸過程就是從DecorView開始的
View和ViewGroup的測量方法有一定區(qū)別,View通過measure方法就可以完成自身的測量過程,而ViewGroup不僅需要調(diào)用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方法調(diào)用的,ViewGroup在調(diào)用該子View的measure方法的同時(shí)還傳入了子View的widthMeasureSpec和heightMeasureSpec值。該方法被定義為final類型,也就是說其measure過程是固定的,在measure中調(diào)用了onMeasure方法,如果想要自定義測量過程的話,需要重寫onMeasure方法。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
Google在介紹該方法的時(shí)候也說了
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.
該方法需要被子類覆蓋,讓子類提供精準(zhǔn)、有效的測量數(shù)據(jù),所以我們一般在進(jìn)行自定義View開發(fā)時(shí),需要自定義測量過程就需要復(fù)寫此方法。
setMeasuredDimension方法的作用就是設(shè)置View的測量寬高,其實(shí)我們?cè)谑褂胓etMeasuredWidth/getMeasuredHeight 方法獲取的寬高值就是此處設(shè)置的值。
如果不復(fù)寫此onMeasure方法,則默認(rèn)使用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; }
可以發(fā)現(xiàn),傳入的measureSpec數(shù)值被MeasureSpec解析成了對(duì)應(yīng)的數(shù)據(jù),這里簡單介紹下MeasureSpec,它的作用就是告訴View應(yīng)該以哪一種模式測量這個(gè)View,SpecMode有三種模式:
• UNSPECIFIED:表示父容器不對(duì)View有任何限制,這種模式主要用于系統(tǒng)內(nèi)部多次Measure的情況,不需要過多關(guān)注
• AT_MOST:父容器已經(jīng)指定了大小,View的大小不能大于這個(gè)值,相當(dāng)于布局中使用的wrap_content模式
• EXACTLY:表示View已經(jīng)定義了精確的大小,使用這個(gè)指定的精確大小specSize作為該View的大小,相當(dāng)于布局中我們指定了66dp這種精確數(shù)值或者match_parent模式
傳入的measureSpec值經(jīng)過MeasureSpec.getMode方法獲取它的測量模式,MeasureSpec.getSize方法獲取對(duì)應(yīng)模式下的規(guī)格大小,從而確定了其最終的測量大小。
ViewGroup
ViewGroup是一個(gè)繼承至View的抽象類,ViewGroup沒有實(shí)現(xiàn)測量自己的具體過程,因?yàn)槠溥^程是需要各個(gè)子類根據(jù)自己的需要再具體實(shí)現(xiàn),比如LinearLayout、RelativeLayout等布局的特性都是不同的,不能統(tǒng)一的去管理,所以就交給其子類自己去實(shí)現(xiàn)
ViewGroup在measure時(shí),除了實(shí)現(xiàn)自身的測量,還需要對(duì)它的每個(gè)子元素進(jìn)行measure,在ViewGroup內(nèi)部提供了一個(gè)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所擁有的子元素的個(gè)數(shù),通過一個(gè)for循環(huán)調(diào)用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,然后調(diào)用getChildMeasureSpec方法計(jì)算出該子元素正確的MeasureSpec,再使用child.measure方法把這個(gè)MeasureSpec傳遞給View進(jìn)行測量。
通過這一系列過程,就能讓各個(gè)子元素依次進(jìn)入measure了
二、layout
通過之前的measure過程,View已經(jīng)測量出了自己需要的寬高大小,performTraversals方法接下來就會(huì)執(zhí)行l(wèi)ayout過程
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
layout的過程主要是用來確定View的四個(gè)頂點(diǎn)所在屏幕上的位置
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)方法里的四個(gè)參數(shù)分別指的是左、上、右、下的位置,這四個(gè)值是通過ViewRootImpl類里的performTraversals方法傳入的
layout方法用來確定View自身的位置,mLeft、mTop、mBottom、mRight的值最終會(huì)由setOpticalFrame和setFrame方法確定,其實(shí)setOpticalFrame內(nèi)部最后也是通過調(diào)用setFrame方法設(shè)置的
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的四個(gè)頂點(diǎn)位置后,就相當(dāng)于View在父容器中的位置被確定了,接下來會(huì)調(diào)用onLayout方法,這個(gè)方法是沒有具體實(shí)現(xiàn)的
protected void onLayout(boolean changed, int left, int top, int right, int bottom) { }
和ViewGroup的onMeasure類似,onLayout方法的具體實(shí)現(xiàn)也是需要根據(jù)各個(gè)View或ViewGroup的特性來決定的,所以源碼中是個(gè)空方法,有興趣的可以去看看LinearLayout、RelativeLayout等實(shí)現(xiàn)了onLayout方法的ViewGroup子類
之前的measure過程,得到的是測量寬高,而通過onLayout方法,進(jìn)一步確定了View的最終寬高,一般情況下,measure過程的測量寬高和layout過程確定的最終寬高是一樣的
三、draw
經(jīng)過以上步驟,View已經(jīng)確定好了大小和屏幕中顯示的位置,接著就可以繪制自身需要顯示的內(nèi)容了
在performTraversals方法中,會(huì)調(diào)用performDraw方法,performDraw方法中調(diào)用draw方法,draw方法中接著調(diào)用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); } } }
首先會(huì)通過lockCanvas方法取得一個(gè)Canvas畫布對(duì)象,接著由mView(DecorView)頂層視圖去調(diào)用View的draw方法,并傳入一個(gè)Canvas畫布對(duì)象
其實(shí)Google的工程師已經(jīng)把draw的繪制過程注釋的非常詳細(xì)了
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設(shè)置了背景,則繪制背景。此背景指的是我們?cè)诓季治募型ㄟ^android:background屬性,或代碼中使用setBackgroundResource、setBackgroundColor等方法設(shè)置的背景圖片或背景顏色
if (!dirtyOpaque) { drawBackground(canvas); }
dirtyOpaque屬性用來判斷該View是否是透明的,如果是透明的則不執(zhí)行某些步驟,比如繪制背景,繪制內(nèi)容等
2. 如果有必要的話,保存這個(gè)canvas畫布,為該層邊緣的fading效果作準(zhǔn)備
第2步和第5步是配套的,我們一般不用管2和5,源碼中的注釋也說了,其中的2和5方法在通常情況下是直接跳過的(skip step 2 & 5 if possible (common case)),其主要作用是實(shí)現(xiàn)一些如同View滑動(dòng)到邊緣時(shí)產(chǎn)生的陰影效果,可以不用過多關(guān)注
3. 繪制View的內(nèi)容
該步驟調(diào)用了onDraw方法,這個(gè)方法是一個(gè)空實(shí)現(xiàn)
/** * Implement this to do your drawing. * * @param canvas the canvas on which the background will be drawn */ protected void onDraw(Canvas canvas) { }
每個(gè)子View需要展示的內(nèi)容肯定是不相同的,所以onDraw的詳細(xì)過程需要子類自己去實(shí)現(xiàn)
4. 繪制子View
和第3步一樣,此方法也是一個(gè)空實(shí)現(xiàn)
/** * 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) { }
對(duì)于單純的View來說,它是沒有子View的,所以不需要實(shí)現(xiàn)該方法,該方法主要是被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循環(huán)調(diào)用drawChild方法
protected boolean drawChild(Canvas canvas, View child, long drawingTime) { return child.draw(canvas, this, drawingTime); }
drawChild方法里調(diào)用子視圖的draw方法,從而讓其子視圖進(jìn)入draw過程
5. 繪制View邊緣的漸變褪色效果,類似于陰影效果
當(dāng)?shù)?個(gè)步驟保存了canvas畫布后,就可以為這個(gè)畫布實(shí)現(xiàn)陰影效果
6. 繪制View的裝飾物
View的裝飾物,指的是View除了背景、內(nèi)容、子View的其它部分,比如滾動(dòng)條這些
四、常見問題
1.在Activity中獲取View的寬高,得到的值為0
通過上面的measure分析可以知道,View的measure過程和Activity的生命周期方法不是同步的,所以無法保證Activity的某個(gè)生命周期執(zhí)行后View就一定能獲取到值,當(dāng)我們?cè)赩iew還沒有完成measure過程就去獲取它的寬高,當(dāng)然獲取不到了,解決這問題的方法有很多,這里推薦使用以下方法
(1)在View的post方法中獲取:
這個(gè)方法簡單快捷,推薦使用
mView.post(new Runnable() { @Override public void run() { width = mView.getMeasuredWidth(); height = mView.getMeasuredHeight(); } });
post方法中傳入的Runnable對(duì)象將會(huì)在View的measure、layout過程后觸發(fā),因?yàn)閁I的事件隊(duì)列是按順序執(zhí)行的,所以任何post到隊(duì)列中的請(qǐng)求都會(huì)在Layout發(fā)生變化后執(zhí)行。
(2)使用View的觀察者ViewTreeObserver
ViewTreeObserver是視圖樹的觀察者,其中OnGlobalLayoutListener監(jiān)聽的是一個(gè)視圖樹中布局發(fā)生改變或某個(gè)視圖的可視狀態(tài)發(fā)生改變時(shí),就會(huì)觸發(fā)此類監(jiān)聽事件,其中onGlobalLayout回調(diào)方法會(huì)在View完成layout過程后調(diào)用,此時(shí)是獲取View寬高的好時(shí)機(jī)
ViewTreeObserver observer = mView.getViewTreeObserver(); observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { mView.getViewTreeObserver().removeGlobalOnLayoutListener(this); width = mScanIv.getMeasuredWidth(); height = mScanIv.getMeasuredHeight(); } });
使用這個(gè)方法需要注意,隨著View樹的狀態(tài)改變,onGlobalLayout方法會(huì)被回調(diào)多次,所以在進(jìn)入onGlobalLayout回調(diào)方法時(shí),就移除這個(gè)觀察者,保證onGlobalLayout方法只被執(zhí)行一次就好了
(3)在onWindowFocusChanged回調(diào)中獲取
此方法是在View已經(jīng)初始化完成,measure和layout過程已經(jīng)執(zhí)行完成,UI視圖已經(jīng)渲染完成時(shí)被回調(diào),此時(shí)View的寬高肯定也已經(jīng)被確定了,這個(gè)時(shí)候就可以去獲取View的寬高了
@Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if (hasFocus) { width = mView.getMeasuredWidth(); height = mView.getMeasuredHeight(); } }
這個(gè)方法在Activity界面發(fā)生變化時(shí)也會(huì)被多次回調(diào),如果只需要獲取一次寬高的話,建議加上標(biāo)記加以限制
除了以上方法,還有其它的方法也能獲取到寬高,比如在onClick方法中獲取,手動(dòng)調(diào)用measure方法,使用postDelayed等,了解了View繪制原理后,這些都是很容易就能理解的。
以上這篇淺談Android View繪制三大流程探索及常見問題就是小編分享給大家的全部內(nèi)容了,希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Android開發(fā)之保存圖片到相冊(cè)的三種方法詳解
這篇文章主要介紹了Android開發(fā)實(shí)現(xiàn)的保存圖片到相冊(cè)功能的三種方法,文中的示例代碼講解詳細(xì),有一定的參考價(jià)值,感興趣的可以了解一下2022-04-04Android生存指南之:開發(fā)中的注意事項(xiàng)
本篇文章是對(duì)在Android開發(fā)中的一些注意事項(xiàng),需要的朋友可以參考下2013-05-05Android5.0以上實(shí)現(xiàn)全透明的狀態(tài)欄方法(仿網(wǎng)易云界面)
下面小編就為大家分享一篇Android5.0以上實(shí)現(xiàn)全透明的狀態(tài)欄方法(仿網(wǎng)易云界面),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-01-01Android實(shí)現(xiàn)點(diǎn)擊AlertDialog上按鈕時(shí)不關(guān)閉對(duì)話框的方法
這篇文章主要介紹了Android實(shí)現(xiàn)點(diǎn)擊AlertDialog上按鈕時(shí)不關(guān)閉對(duì)話框的方法,涉及設(shè)置監(jiān)聽的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-02-02Android 出現(xiàn)問題Installation error: INSTALL_FAILED_CONFLICTING_P
這篇文章主要介紹了Android 出現(xiàn)問題Installation error: INSTALL_FAILED_CONFLICTING_PROVIDER解決辦法的相關(guān)資料,需要的朋友可以參考下2016-12-12Android 利用廣播監(jiān)聽usb連接狀態(tài)(變化情況)
這篇文章主要介紹了Android 利用廣播監(jiān)聽usb連接狀態(tài),需要的朋友可以參考下2017-06-06Android使用xml文件資源定義菜單實(shí)現(xiàn)方法示例
這篇文章主要介紹了Android使用xml文件資源定義菜單實(shí)現(xiàn)方法,結(jié)合實(shí)例形式分析了Android資源文件管理及xml配置自定義菜單相關(guān)操作技巧,需要的朋友可以參考下2019-03-03Android實(shí)現(xiàn)不同apk間共享數(shù)據(jù)的方法(2種方法)
這篇文章主要介紹了Android實(shí)現(xiàn)不同apk間共享數(shù)據(jù)的方法,介紹了apk自定義借口實(shí)現(xiàn)數(shù)據(jù)共享與基于User id的數(shù)據(jù)共享,并重點(diǎn)介紹了基于User id的數(shù)據(jù)共享實(shí)現(xiàn)技巧,非常簡單實(shí)用,需要的朋友可以參考下2016-01-01