前段開始學(xué)習(xí)View的工作原理,前兩篇博客的草稿都已經(jīng)寫好了,本想一鼓作氣寫完所有的相關(guān)文章,然后經(jīng)歷了一段連續(xù)加班,結(jié)果今天準(zhǔn)備繼續(xù)寫文章時,把之前寫好的東西都忘記了,又重新梳理了一遍,所以說那怕就是已經(jīng)掌握的知識,也要記得溫故而知新。
言歸正傳,之前我們討論過了measure過程,measure過程完成之后,我們就可以通過 getMeasuredWidth
或getMeasuredHeight
來得到View的寬高尺寸了。而知道了寬高尺寸之后,剩下的就是布局(layout)過程了。說直白點,怎么把具一個寬高已定的View擺放在屏幕上。
我們先來看一個demo。
/** *@author www.yaoxiaowen.com */<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/linearLayout1" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:background="@color/google_yellow" > <LinearLayout android:id="@+id/linearLayout2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" android:background="@color/white" > <TextView android:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="textView1" android:background="@color/green" android:paddingLeft="20dp" android:paddingRight="40dp" android:paddingTop="10dp" /> <Button android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="button1" android:layout_marginLeft="20dp" android:background="@color/blue" /> </LinearLayout> <TextView android:id="@+id/text2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="text2" android:layout_marginLeft="40dp" android:background="@color/red" /></LinearLayout>
具體的實現(xiàn)效果,是這個樣子。
我們暫且不討論代碼當(dāng)中的實現(xiàn)原理之類的,僅僅就想想一種場景,給你一些尺寸固定但不同的盒子,擺放在一個大房間的地面上,我們應(yīng)該怎么擺放呢。
之所以要根據(jù)這個demo來設(shè)想這種場景,是因為我覺的,不管代碼怎么寫,但是基本的道理都不復(fù)雜,正所謂大道至簡,(畢竟我們不是做那些高大上的算法的)。自己根據(jù)場景去設(shè)想怎么擺放,同樣的也有助于理解代碼。
假設(shè)我們按照 LinearLayout
豎直方式的規(guī)則來擺放,那么開始擺放第一個盒子肯定從房間的左上角開始計算位置,根據(jù)父容器padding,本身的margin等來決定具體擺放的坐標(biāo)。然后再在豎直方向上 向前推進擺放第二個盒子。
我并不知道上面那段文字有沒有能清楚的表達出我的意思, 但是這是我前段時間關(guān)于這個layout的思考過程。思考過程本身就很難表達,但是我想這樣思考是有助于理解這個過程的。
和measure過程一樣,layout過程也是一個遞歸過程,并且ViewGroup類本身不 應(yīng)該具體實現(xiàn)onLayout
,具體實現(xiàn)過程應(yīng)該放在Framelayout
,LinearLayout
,RelativieLayout
等容器類中。
當(dāng)layout過程完成之后,我們就可以得到View的左上角和右下角的坐標(biāo)了。(就是left,top,right,bottom)。值得注意的是這個坐標(biāo)是相對坐標(biāo),就是相對于View父容器的坐標(biāo)。所以通過遞歸來計算是最自然最方便的方式。
這幾天看了關(guān)于編程語言歷史方面的文章, 在早期的編程語言中,是沒有遞歸這個概念的,是后來有人想出了這個概念,才逐漸的在各種編程語言中實現(xiàn)。所以說現(xiàn)在我們來看稀奇平常很自然的概念,但是當(dāng)初第一個想出這個概念的人,那就是天才了。
下面我們就從源碼角度來進行分析。performTraversals
調(diào)用了PerformLayout
方法。
//ViewRootImpl.java www.yaoxiaowen.com private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) { //... final View host = mView; //... host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); //...}
host
其實就是DecorView
了,在執(zhí)行layout
時,注意傳遞的頭兩個參數(shù)都是0,這說明 整個layout過程是從屏幕坐標(biāo)系的 左上角開始執(zhí)行的。
下一步還是走到了View#Layout()
, View#Layout
方法如下:
//View.java www.yaoxiaowen.com public void layout(int l, int t, int r, int b) { //... int oldL = mLeft; int oldT = mTop; int oldB = mBottom; int oldR = mRight; //setOpticalFrame方法內(nèi)部還是調(diào)用了 setFrame()方法,所以無論如何,最終都會執(zhí)行setFrame()方法。 // setFrame()方法會將View新的left,top,right,bottom存儲到View的成員變量中,并且返回一個boolean值, //返回true,則表示View的位置或尺寸發(fā)生了變化;否則就是未發(fā)生變化。 boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { //如果View布局發(fā)生了變化,或者存在 PFLAG_LAYOUT_REQUIRED 標(biāo)記,則執(zhí)行以下代碼 //首先 觸發(fā)onLayout 方法,View中默認(rèn)的onLayout是個空方法。 //(因為擺放位置是父容器負(fù)責(zé)的,View中存在該方法是為了遍歷循環(huán)的需要)。 //但是 容器類的ViewGroup則需要實現(xiàn)onLayout方法,從而在 onLayout()方法中依次循環(huán)子View, //并調(diào)用他們的Layout方法。 onLayout(changed, l, t, r, b); //... //我們可以通過 View的addOnLayoutChangeListener(View.onLayoutChangeListener)方法 //向View 中添加多個 Layout 發(fā)生變化的事件監(jiān)聽器。 //這些監(jiān)聽器都存儲在 mListenerInfo.mOnLayoutChangeListeners 這個List當(dāng)中 ListenerInfo li = mListenerInfo; if (li != null && li.mOnLayoutChangeListeners != null) { // 首先對 mOnLayoutChangeListeners 中的事件監(jiān)聽器 進行拷貝。 ArrayList<OnLayoutChangeListener> listenersCopy = (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone(); int numListeners = listenersCopy.size(); for (int i = 0; i < numListeners; ++i) { // 遍歷注冊的事件監(jiān)聽器,依次調(diào)用其 onLayoutChange 方法,這樣 Layout事件監(jiān)聽器就得到了相應(yīng)。 listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB); } } } //.... }
setFrame()
方法的作用也不復(fù)雜。
//View.java www.yaoxiaowen.com// setFrame()方法會將View新的left,top,right,bottom存儲到View的成員變量中,并且返回一個boolean值,//返回true,則表示View的位置或尺寸發(fā)生了變化;否則就是未發(fā)生變化。protected boolean setFrame(int left, int top, int right, int bottom) { //... mLeft = left; mTop = top; mRight = right; mBottom = bottom; mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom); //...}
通過分析以上代碼可以知道,在layout()
方法當(dāng)中,主要做的就是將left,top,right,bottom方法保存起來,而View自身怎么被布局(其實我覺的用擺放這個詞更合適)。則是父容器需要完成的工作。
因為總所周知的原因(其實就是各個容器類ViewGroup布局子元素的方式差異很大)。ViewGroup并沒有實現(xiàn)具體的onLayout
方法,各個具體的ViewGroup,比如LinearLayout
,RelativeLayout
,FrameLayout
等,它們都有它們具體的 onLayout
實現(xiàn)方式。
我們以LinearLayout
為例來進行分析。
@Override 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); } }
和measure過程是一樣的套路,也是分為兩種不同的情況,我們就以 豎直方向為例。(源碼比較多,并且實際也要考慮使用layout_weight
的情況,不過我們僅僅只抽取一部分分析基本原理)。
//LinearLayout.java www.yaoxiaowen.com void layoutVertical(int left, int top, int right, int bottom) { final int paddingLeft = mPaddingLeft; int childTop; int childLeft; //... //遍歷子元素 for (int i = 0; i < count; i++) { final View child = getVirtualChildAt(i); if (child == null) { childTop += measureNullChild(i); } else if (child.getVisibility() != GONE) { //經(jīng)過了measure過程,我們已經(jīng)知道了 View的寬高 final int childWidth = child.getMeasuredWidth(); final int childHeight = child.getMeasuredHeight(); //布局文件中,可能設(shè)置了 margin等 final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); //... //隨著遍歷過程,childTop是逐步增加的。所以說在豎直方向上,后續(xù)的View都被擺放在靠下的位置。 childTop += lp.topMargin; //在設(shè)置子View時,傳遞的是 子View 左上角的坐標(biāo)和寬高尺寸 setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight); //childTop逐步的向下增加,并且在增加的過程中,也考慮了 子View的margin,以及偏移量。 childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child); i += getChildrenSkipCount(child, i); } } }
在該方法中,通過調(diào)用setChildFrame
來為子元素指定相應(yīng)的位置。
而setChildFrame
只是很簡單的一句話。
private void setChildFrame(View child, int left, int top, int width, int height) { child.layout(left, top, left + width, top + height); }
而child.layout
其實不就是繼續(xù)調(diào)用 onLayout
之類的嘛。所以這樣就形成了完美的遞歸。直到把整個View樹都完成了layout過程。
自此,我們就分析完了layout過程,相比與measure過程,簡單了很多。并且理解起來也更容易,就想想在一個大房間的地面上,我們怎么使用遞歸的方式來擺放很多的盒子。
最后再來看看幾個方法。
public final int getWidth() { return mRight - mLeft; } public final int getHeight() { return mBottom - mTop; } public final int getMeasuredHeight() { return mMeasuredHeight & MEASURED_SIZE_MASK; } public final int getMeasuredWidth() { return mMeasuredWidth & MEASURED_SIZE_MASK; }
同樣都是得到寬高的方式,這兩組方法有什么區(qū)別呢?現(xiàn)在我們了解了Measure和layout過程,那么我們就知道了,getMeasuredWidth()
方法是在measure過程完成之后可以調(diào)用的,而getWidth()
方法則是在 layout過程之后可以調(diào)用的。并且?guī)缀踉谒星闆r下,兩者的返回值都是相等的。
http://www.cnblogs.com/yaoxiaowen/p/7141855.html