標籤:android 源碼 布局 listview
在自訂ListView中,需要將下拉重新整理的View在初始化的時候設定padding隱藏起來,這時就要在初始化的時候獲得要載入的布局View的高度。
private View headView;headView = inflater.inflate(R.layout.header, null);
如果接下來調用:
headView.getHeight();headView.getMeasuredHeight();
我們知道都會返回0,原因是getMeasuredHeight要在measure後才有值,getHeight要在layout後才有值。要在這裡取得載入的布局的寬高,需要程式員調用measure測量出該布局的寬高。以前在寫下拉ListView的時候有這樣一段代碼,一直不太明白,今天研究了一下:
private void measureView(View child) {ViewGroup.LayoutParams lp = child.getLayoutParams();if(lp == null){lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);}//headerView的寬度資訊int childMeasureWidth = ViewGroup.getChildMeasureSpec(0, 0, lp.width);int childMeasureHeight;if(lp.height > 0){childMeasureHeight = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);//最後一個參數表示:適合、匹配} else {childMeasureHeight = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);//未指定}//System.out.println("childViewWidth"+childMeasureWidth);//System.out.println("childViewHeight"+childMeasureHeight);//將寬和高設定給childchild.measure(childMeasureWidth, childMeasureHeight);}
在inflate完布局後,調用measureView(headView),然後再調用getMeasureHeight函數就可以獲得實際高度了。
接下來從原理上分析一下這段代碼:
通過inflater.inflate(R.layout.header, null);將XML轉化為View的時候,是沒有設定View的LayoutParams的,就會進入到lp ==null條件中new一個LayoutParams。接下來設定View的childMeasureWidth和childMeasureHeight這點比較費解。
childMeasureWidth:
調用ViewGroup的getChildMeasureSpec傳遞的第一個參數(父類的spec)竟然是0,傳進0表示父類的spec是未指定的MeasureSpec.UNSPECIFIED模式,size也為0。那麼該函數返回的值就是一個UNSPECIFIED類型size為0的值。
childMeasureHeight:
和上面類似,如果lp.height > 0就表示設定的是一個精確值,接下來會封裝一個EXACTLY和lp.height的值給childMeasureHeight,然後這裡lp.height是wrap_content,他是個負值(-2),所以會封裝一個未指定UNSPECIFIED和size為0的值給childMeasureHeight。
即childMeasureWidth和childMeasureHeight兩個參數均為0。
關於傳遞給child的widthMeasureSpec和heightMeasureSpec可以通過列印出的結果看出來確實是這樣的。
然後將這兩個值作為參數傳遞給子View的measure函數。
上次在張鴻洋大神的部落格看到在划動刪除代碼裡這樣寫,擷取PopupWindow的寬高之前調用了mPopupWindow.getContentView().measure(0, 0);擷取他的寬高。
View view = mInflater.inflate(R.layout.delete_btn, null); mDelBtn = (Button) view.findViewById(R.id.id_item_btn); mPopupWindow = new PopupWindow(view, LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); /** * 先調用下measure,否則拿不到寬和高 */ mPopupWindow.getContentView().measure(0, 0); mPopupWindowHeight = mPopupWindow.getContentView().getMeasuredHeight(); mPopupWindowWidth = mPopupWindow.getContentView().getMeasuredWidth();
這樣看來,其實和一開始給的代碼是一樣的。因為分析完開始的代碼,我們發現最後執行child.measure(childMeasureWidth, childMeasureHeight);的時候兩個參數就是0。
我最初很難理解父布局傳遞給子布局一個UNSPECIFIED和0的值。因為預設情況下對於View來說會直接設定:
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
裡面的函數前面都學習過,很明顯會將傳過來的0設定給該View。但是調用這段代碼後卻是得到了真事的尺寸不是0。
帶著問題,我將測量的布局僅僅寫一個View,命名為view_layout.xml:
<View xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="50dp" android:layout_height="50dp" android:background="#ffff0000" />
還有一個View被LinearLayout包裹著,viewgroup_layout.xml:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" ><View android:layout_width="50dp" android:layout_height="50dp" /></LinearLayout>
還有一個textview_layout.xml:
<TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="50dp" android:layout_height="50dp" android:text="Test" />
activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:id="@+id/mainLayout" ></LinearLayout>
然後在onCerate的時候執行下面代碼:
inflater = LayoutInflater.from(this); mainLayout = (LinearLayout) findViewById(R.id.mainLayout); view = inflater.inflate(R.layout.view_layout, null); textview = inflater.inflate(R.layout.textview_layout, null); viewgroup = inflater.inflate(R.layout.viewgroup_layout, null); measureView(view); measureView(textview); measureView(viewgroup); Log.i(TAG, "view的width和height分別是:" + view.getMeasuredWidth()+ " " + view.getMeasuredHeight()); Log.i(TAG, "textview的width和height分別是:" + textview.getMeasuredWidth()+ " " + textview.getMeasuredHeight()); Log.i(TAG, "viewgroup的width和height分別是:" + viewgroup.getMeasuredWidth()+ " " + viewgroup.getMeasuredHeight());
得到的結果是:
果然和最初解釋的一樣。對於View來說預設情況下會直接設定。
對於繼承自View的TextView和ViewGroup而言卻獲得了其測量值。
原因在哪裡?因為TextView和ViewGroup都重寫了onMeasure。在ViewGroup 和TextView裡面對AT_MOST和UNSPECIFIED的處理方式一樣。
一直忽視了UNSPECIFIED很少使用,所以沒能發現問題。不能慣性的認為onMeasure裡面的操作就是調用setMeasuredDimension進行儲存寬高的值。在這之前如果是ViewGroup還會具體的根據裡面子View的情況進行處理。
既然談到了View的尺寸,那麼在將他們添加到父布局,再擷取以下測量寬高,這時的寬高測量要在onWindowFocusChanged裡面。
mainLayout.addView(view);mainLayout.addView(textview);mainLayout.addView(viewgroup); @Override public void onWindowFocusChanged(boolean hasFocus) { Log.i(TAG, "獲得焦點後");// Log.i(TAG, "view的width和height分別是:" + view.getMeasuredWidth()+ " " + view.getHeight()); Log.i(TAG, "textview的width和height分別是:" + textview.getMeasuredWidth()+ " " + textview.getMeasuredHeight()); Log.i(TAG, "viewgroup的width和height分別是:" + viewgroup.getMeasuredWidth()+ " " + viewgroup.getMeasuredHeight()); super.onWindowFocusChanged(hasFocus);}
列印出來的Log如下:
View的寬高竟然有值了!原因是什嗎?在調用addView的時候會使試圖重新繪製!至於寬和高為什麼是這樣的,那是因為調用generateDefaultLayoutParams返回的寬為match_parent高為wrap_content。最終傳測量該View是的onMeasure的參數是AT_MOST和ScreenSize。因為是個View,直接調用setMeasuredDimension設定。
後面兩個出現0的原因是View將螢幕佔滿了。如果不調用mainLayout.addView(view);可以看到:
改成"horizontal"後,列印資訊如下:
至於高度,textview為15,由於是最外層,最終為包裹內容。Viewgroup的最終寬高為50dp,由於My Phone像素密度為0.75,因此換算為像素為38,結果一致。
這就告訴我,自己調用onMeasure只是暫時拿到了View的寬高測量值,這個值與傳入的父布局的widthMeasureSpec, heightMeasureSpec有關。實際上添加到布局的真實寬高可能和這個值不一樣。
ListView中也有類似的方法,和開始的方法基本一樣。
private void measureItem(View child) { ViewGroup.LayoutParams p = child.getLayoutParams(); if (p == null) { p = new ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); } int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, mListPadding.left + mListPadding.right, p.width); int lpHeight = p.height; int childHeightSpec; if (lpHeight > 0) { childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY); } else { childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); } child.measure(childWidthSpec, childHeightSpec); }
Android如何在初始化的時候擷取載入的布局的寬高