編輯:關於Android編程
本篇文章接著上篇文章的內容來繼續討論View的繪制機制,上篇文章中我們主要講解了View的measure過程,今天我們就來學習ViewGroup的measure過程,由於ViewGroup只是一個抽象類,所以我們需要以一個具體的布局來分析measure過程,正如我上篇文章說的,我打算使用LinearLayout為例講解measure過程,如果你還沒有讀過上篇文章,那麼建議你先浏覽一下上篇文章吧:Android中View的繪制機制源碼分析 一
在進行今天的主題之前,我來給大家分享一下我最近看到並且非常喜歡的兩句話吧:
1、把生命浪費在美好的事物上
2、應該有一份不以此為生的職業
喜歡第一句話的原因是因為裡面包含了一種樂觀的生活態度,只要一件事情你在進行的過程中能夠給你帶來快樂,那麼我們就值得花時間做,喜歡第二句話的原因是作為程序員這個職業以後轉型的問題也是值得我們考慮的,相信大家也都聽說過程序員是吃青春飯的職業,當你到35-40歲已經年老色衰的時候,你不得不考慮轉型了,有部分轉型為管理人才,有些人完全轉型,干著和IT毫無關系的職業,所以我們是不是現在就要想想我們有沒有一份不以此為生的職業呢?好吧 扯淡就扯到這裡吧,下面我們步入正題。
我們來分析今天的第一個問題:你對layout_weight屬性知多少?
相信大多數同學會說這個屬性就是標明一個View在父View中所占的權重(比例),這個解釋對嗎?我們暫且不做評論,我們使用兩個例子來驗證一下:
example 1:
example 2:
效果圖如下:
在第一張圖片中,上面的TextView的weidht是2,下面的TextView的weight是4,所以上面的TextView的高度是下面TextView高度的一半,注意此時兩個TextView的layout_height都是0dip,再看下面的一張圖,同樣上面的TextView和下面TextView的weight分別是2和4,唯一不同的是它們的layout_height變為了match_parent,此時上面的高度確實下面的兩倍
所以從第一張圖片看來,layout_weight好像是代表比例的,但是從第二張圖片看,剛好是相反的,我們今天就帶著這個疑問開始分析LinearLayout的measure源碼吧
LinearLayout的measuer調用的是View中的measure方法,從上篇文章中我們知道measure會調用onMeasure方法,所以直接從LinearLayout的onMeasure開始分析:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mOrientation == VERTICAL) { measureVertical(widthMeasureSpec, heightMeasureSpec); } else { measureHorizontal(widthMeasureSpec, heightMeasureSpec); } }
Section one:
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) { //用來存儲所有的子View使用的高度 mTotalLength = 0; int maxWidth = 0; int alternativeMaxWidth = 0; int weightedMaxWidth = 0; boolean allFillParent = true; //所有View的weight的和 float totalWeight = 0; //獲得子View的個數 final int count = getVirtualChildCount(); //widthMeasureSpec和heightMeasureSpec就是父View傳遞進來的,這裡拿到父View的mode final int widthMode = MeasureSpec.getMode(widthMeasureSpec); final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
這裡定義了幾個重要的變量,mTotalLength,用來存儲所有子View的高度,count存在子View的個數,widthMode和heightMode用來存儲父View的mode(如果對於mode不熟,我以看我前面的一篇文章)。
Section Two:
//遍歷所有的子View,獲取所有子View的總高度,並對每個子View進行measure操作 for (int i = 0; i < count; ++i) { final View child = getVirtualChildAt(i); if (child == null) { //如果child 是Null,則mTotalLength加0 mTotalLength += measureNullChild(i); continue; } if (child.getVisibility() == View.GONE) { //如果child不可見,則跳過 i += getChildrenSkipCount(child, i); continue; } //拿到child的LayoutParams LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); //將weight的值加到totalWeight,weight的值就是xml文件中的layout_weight屬性的值 totalWeight += lp.weight; if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) { /** 如果父View的mode是EXACTLY,並且height==0 並且lp.weight>0(就是我們上面的例子中的第一張圖的情況) 那麼就先不measure這個child,直接把topMargin和bottoMargin等屬性加到totaoLength中 */ final int totalLength = mTotalLength; mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin); } else { int oldHeight = Integer.MIN_VALUE; //如果父View不是EXACLTY,那麼將子View的height變為WRAP_CONTENT if (lp.height == 0 && lp.weight > 0) { // heightMode is either UNSPECIFIED or AT_MOST, and this // child wanted to stretch to fill available space. // Translate that to WRAP_CONTENT so that it does not end up // with a height of 0 oldHeight = 0; lp.height = LayoutParams.WRAP_CONTENT; } // Determine how big this child would like to be. If this or // previous children have given a weight, then we allow it to // use all available space (and we will shrink things later // if needed). measureChildBeforeLayout( child, i, widthMeasureSpec, 0, heightMeasureSpec, totalWeight == 0 ? mTotalLength : 0); if (oldHeight != Integer.MIN_VALUE) { lp.height = oldHeight; } final int childHeight = child.getMeasuredHeight(); final int totalLength = mTotalLength; mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin + lp.bottomMargin + getNextLocationOffset(child)); if (useLargestChild) { largestChildHeight = Math.max(childHeight, largestChildHeight); } }
我們發現在measureVertical中調用了一個measureChildBeforeLayout方法,我們先看看它傳入的幾個參數,我們發現最後一個參數聽奇怪的,totalWeight==0?mTotalLength:0,也就是說對於一個View,如果這個View之前的View沒有設置過layout_weight屬性,那麼這個參數等於mTotalLength,如果有設置過,那麼傳0,我們先進入measureChildBeforeLayout方法看看:
void measureChildBeforeLayout(View child, int childIndex, int widthMeasureSpec, int totalWidth, int heightMeasureSpec, int totalHeight) { measureChildWithMargins(child, widthMeasureSpec, totalWidth, heightMeasureSpec, totalHeight); }
其實就是調用父類ViewGroup的measureChildWidthMargins方法,這個方法我們在前篇文章已經分析過了,這裡我們就不分析了,它就是對子View進行measure方法,只不過我們這裡需要注意,如果前面有View設置了layout_weight屬性,那麼這裡的totalHeight就是0,在執行完了measureChildBeforeLayout方法後,child的高度就知道了,就將child的高度累加到mTotalHeight中。
Section Three:
//將所有View的高度賦值給heightSize; int heightSize = mTotalLength; heightSize = Math.max(heightSize, getSuggestedMinimumHeight()); //這裡對heightSize再次賦值,不過如果LinearLayout是xml文件的根標簽,並且設置到Activity的話 //此時heightSize的大小就是屏幕的高度,我們暫時就考慮等於屏幕高度的情況,其他情況類似 heightSize = resolveSize(heightSize, heightMeasureSpec); //屏幕的高度還剩下delta,如果對於我們上面第一張圖,delta>0,對於第二張圖則<0 int delta = heightSize - mTotalLength; if (delta != 0 && totalWeight > 0.0f) { //如果設置了weightsum屬性,這weightSum等於weightsum的屬性,否則等於totalWeight float weightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight; mTotalLength = 0; //重新遍歷所有的子View for (int i = 0; i < count; ++i) { final View child = getVirtualChildAt(i); //如果子View不可見,直接跳過 if (child.getVisibility() == View.GONE) { continue; } LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); float childExtra = lp.weight; //如果設置了weight屬性 if (childExtra > 0) { // Child said it could absorb extra space -- give him his share //從delta中分到(weight/weightSum)*delta,注意這裡delta可能<0 int share = (int) (childExtra * delta / weightSum); weightSum -= childExtra; delta -= share; final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin, lp.width); // TODO: Use a field like lp.isMeasured to figure out if this // child has been previously measured if ((lp.height != 0) || (heightMode != MeasureSpec.EXACTLY)) { /** 記得heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0嗎 這個是Section Two的一個判斷條件,也就是說如果走到這裡,說明這個View前面已經measure過 現在要將share的值加入到高度上,所以要重新measure */ int childHeight = child.getMeasuredHeight() + share; if (childHeight < 0) { childHeight = 0; } child.measure(childWidthMeasureSpec, MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY)); } else { /** 由於走到Section Two中走到heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0時,是直接跳過的 所以沒有測量過,所以在這裡對View進行測量 */ child.measure(childWidthMeasureSpec, MeasureSpec.makeMeasureSpec(share > 0 ? share : 0, MeasureSpec.EXACTLY)); } } final int margin = lp.leftMargin + lp.rightMargin; final int measuredWidth = child.getMeasuredWidth() + margin; maxWidth = Math.max(maxWidth, measuredWidth); boolean matchWidthLocally = widthMode != MeasureSpec.EXACTLY && lp.width == LayoutParams.MATCH_PARENT; alternativeMaxWidth = Math.max(alternativeMaxWidth, matchWidthLocally ? margin : measuredWidth); allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT; final int totalLength = mTotalLength; mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin + getNextLocationOffset(child)); } // Add in our padding mTotalLength += mPaddingTop + mPaddingBottom; // TODO: Should we recompute the heightSpec based on the new total length? } else { alternativeMaxWidth = Math.max(alternativeMaxWidth, weightedMaxWidth); } if (!allFillParent && widthMode != MeasureSpec.EXACTLY) { maxWidth = alternativeMaxWidth; } maxWidth += mPaddingLeft + mPaddingRight; // Check against our minimum width maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); //所有的孩子View測量完畢,為自己設置大小 setMeasuredDimension(resolveSize(maxWidth, widthMeasureSpec), heightSize);
第一個例子:兩個TextView的高度都是0dip,layout_weight分別是2 和 4,LinearLayout的mode=EXACTLY
從Section Two開始,條件滿足heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0 所以在SectionTwo執行完後兩個TextView是沒有執行measure的,所以mTotalLenght等於0。
進入Section Three,此時heightSize等於屏幕的高度,所以delta=heightSize-mTotalLenght=屏幕高度。weightSum=2+4=6.在遍歷子View的時候,通過計算第一個TextView的高度是:屏幕高度*(2/6),並且delta=delta-屏幕高度*(2/6).weightSum=6-2=4.
由於第一個TextView不滿足條件(lp.height != 0) || (heightMode != MeasureSpec.EXACTLY),所以執行else裡面的邏輯:
child.measure(childWidthMeasureSpec, MeasureSpec.makeMeasureSpec(share > 0 ? share : 0, MeasureSpec.EXACTLY));所以第一個TextView的高度就是屏幕的1/3.
遍歷完第一個TextView之後,遍歷第二個TextView,同樣的道理第二個 TextView的高度等於delta*(4/4),也就是等於delta的值,其實也就是 屏幕高度*(4/6)。
第二個例子:兩個TextView的高度都是match_parent,layout_weight分別是2和4 ,LinearLayout的mode=EXACTLY
從Section Two開始,條件不滿足heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0 ,所以執行到了else裡面的邏輯
measureChildBeforeLayout( child, i, widthMeasureSpec, 0, heightMeasureSpec, totalWeight == 0 ? mTotalLength : 0);
int childHeight = child.getMeasuredHeight() + share; if (childHeight < 0) { childHeight = 0; } child.measure(childWidthMeasureSpec, MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));
對於layout_weight屬性的理解應該是這樣的:在SectionTwo中測量完所有的View後,將delta的值按照weight的比例給相應的 View,如果delta>0,那麼那麼就是在原來大小上加上相應的值,否則就是減去相應的值。
最後調用setMeasuredDimension(resolveSize(maxWidth, widthMeasureSpec), heightSize) 設置自身的大小。
相信到這裡你應該已經對LinearLayout的測量過程有了很深刻的理解了吧,如果還有覺得描述不清楚的地方,歡迎留言討論...
寫在前面的話:接觸Android的時間也不短了,聽了視頻、看了書、敲了代碼,寫了博客,做了demo。。。但是想做出一款優秀的APP(哪怕是封裝一個不錯的功能)還有很長的路
Activity在inflate layout時,通過DataBindingUtil來生成綁定,從代碼看,是遍歷contentView得到View數組對象,然後通過數據綁
如何為不同的list item呈現不同的菜單,本文實例就為大家介紹了Android仿微信或QQ滑動彈出編輯、刪除菜單效果、增加下拉刷新等功能的實現,分享給大家供大家參考,
更多動態視圖MoreNewsView經常看朋友圈的動態,有的動態內容較多就只展示前面一段,如果用戶想看完整的再點擊展開,這樣整個頁面的動態列表比較均衡,不會出現個別動態占