Android LayoutInflater深度解析

                           LayoutInflater深度解析

1.概述

對於Inflate的三個參數(int resource, ViewGroup root, boolean attachToRoot),不少人可能有這樣的誤解:node

  • 若是inflate(layoutId, null )則layoutId的最外層的控件的寬高是沒有效果的
  • 若是inflate(layoutId, root, false ) 則認爲和上面效果是同樣的
  • 若是inflate(layoutId, root, true ) 則認爲這樣的話layoutId的最外層控件的寬高才能正常顯示

是不是真的像這樣呢?今天咱們就從源碼的角度去探究:android

2.案例 

咱們就採用一個ListView,item爲一個按鈕app

Activity的佈局文件:ide

<?xml version="1.0" encoding="utf-8"?>
<ListView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/mListView"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" >
</ListView>

item的佈局文件:佈局

<?xml version="1.0" encoding="utf-8"?>
<Button xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/btn_test"
    android:layout_width="120dp"
    android:layout_height="120dp" >
</Button>

ListView的適配器:測試

/**
 * Created by quguangel on 2016/12/1.
 */

public class MyAdapter extends BaseAdapter {
    private String[] listData;
    private LayoutInflater mInflater;
    public MyAdapter(String[] listData,Context mContext){
        this.listData = listData;
        this.mInflater = LayoutInflater.from(mContext);
    }
    @Override
    public int getCount() {
        return listData.length;
    }

    @Override
    public Object getItem(int i) {
        return listData[i];
    }

    @Override
    public long getItemId(int i) {
        return i;
    }

    @Override
    public View getView(int i, View view, ViewGroup viewGroup) {
        if(view == null){
            view = mInflater.inflate(R.layout.listview_item_layout,null);
            holder = new ViewHolder();
            holder.button = (Button) view.findViewById(R.id.btn_test);
            view.setTag(holder);
        }else{
            holder = (ViewHolder) view.getTag();
        }
        holder.button.setText(listData[i]);
        return view;
    }
    ViewHolder holder;
    static class ViewHolder{
        public Button button;
    }
}

主Activity:this

package qu.com.handlerthread;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.ListView;

public class ListViewActivity extends AppCompatActivity {
    private static final String[] datas = {"張三","李四","王五"};
    private ListView mListView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_list_view);
        mListView = (ListView) findViewById(R.id.mListView);
        mListView.setAdapter(new MyAdapter(datas,this));
    }
}

咱們主要關注getView裏面的inflate那行代碼:下面我依次把getView裏的寫成:
一、convertView = mInflater.inflate(R.layout.item, null);
二、convertView = mInflater.inflate(R.layout.item, parent ,false);
三、convertView = mInflater.inflate(R.layout.item, parent ,true);spa

分別看效果圖:code

效果圖1:convertView = mInflater.inflate(R.layout.listview_item_layout, null);xml

效果圖2:convertView = mInflater.inflate(R.layout.listview_item_layout, parent ,false);

咱們如今只經過運行1,2效果圖就能夠看出,這兩種方式加載的佈局明顯不一樣,這與咱們上面的所說的不同。真的是這樣嗎?等下揭曉。

效果圖3:convertView = mInflater.inflate(R.layout.listview_item_layout, parent ,true);

看到第三種狀況,你是否是感到特別的驚訝,沒錯,就是報錯了。

從這三幅效果圖能夠看出,這三種方式加載的佈局現實後效果徹底不同。

inflate(R.layout.listview_item_layout, null)的確不能正確處理寬高,可是並不是和

inflate(R.layout.listview_item_layout, parent ,false)顯示的效果相同,而它能夠完美的顯示正確的寬高,

對於inflate(R.layout.listview_item_layout, parent ,true)卻報錯了,等下咱們源碼來分析爲啥會報錯。

由此可知:咱們前面所說的3個結論明顯有錯誤。

3.源碼解析

下面咱們經過源碼來解釋這三種加載方式的差別

這三個方法,最終都會執行下面的代碼:

public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {  
       synchronized (mConstructorArgs) {  
           final AttributeSet attrs = Xml.asAttributeSet(parser);  
           Context lastContext = (Context)mConstructorArgs[0];  
           mConstructorArgs[0] = mContext;  
           View result = root;  
  
           try {  
               // Look for the root node.  
               int type;  
               while ((type = parser.next()) != XmlPullParser.START_TAG &&  
                       type != XmlPullParser.END_DOCUMENT) {  
                   // Empty  
               }  
  
               if (type != XmlPullParser.START_TAG) {  
                   throw new InflateException(parser.getPositionDescription()  
                           + ": No start tag found!");  
               }  
  
               final String name = parser.getName();  
                 
               if (DEBUG) {  
                   System.out.println("**************************");  
                   System.out.println("Creating root view: "  
                           + name);  
                   System.out.println("**************************");  
               }  
  
               if (TAG_MERGE.equals(name)) {  
                   if (root == null || !attachToRoot) {  
                       throw new InflateException("<merge /> can be used only with a valid "  
                               + "ViewGroup root and attachToRoot=true");  
                   }  
  
                   rInflate(parser, root, attrs, false);  
               } else {  
                   // Temp is the root view that was found in the xml  
                   View temp;  
                   if (TAG_1995.equals(name)) {  
                       temp = new BlinkLayout(mContext, attrs);  
                   } else {  
                       temp = createViewFromTag(root, name, attrs);  
                   }  
  
                   ViewGroup.LayoutParams params = null;  
  
                   if (root != null) {  
                       if (DEBUG) {  
                           System.out.println("Creating params from root: " +  
                                   root);  
                       }  
                       // Create layout params that match root, if supplied  
                       params = root.generateLayoutParams(attrs);  
                       if (!attachToRoot) {  
                           // Set the layout params for temp if we are not  
                           // attaching. (If we are, we use addView, below)  
                           temp.setLayoutParams(params);  
                       }  
                   }  
  
                   if (DEBUG) {  
                       System.out.println("-----> start inflating children");  
                   }  
                   // Inflate all children under temp  
                   rInflate(parser, temp, attrs, true);  
                   if (DEBUG) {  
                       System.out.println("-----> done inflating children");  
                   }  
  
                   // We are supposed to attach all the views we found (int temp)  
                   // to root. Do that now.  
                   if (root != null && attachToRoot) {  
                       root.addView(temp, params);  
                   }  
  
                   // Decide whether to return the root that was passed in or the  
                   // top view found in xml.  
                   if (root == null || !attachToRoot) {  
                       result = temp;  
                   }  
               }  
  
           } catch (XmlPullParserException e) {  
               InflateException ex = new InflateException(e.getMessage());  
               ex.initCause(e);  
               throw ex;  
           } catch (IOException e) {  
               InflateException ex = new InflateException(  
                       parser.getPositionDescription()  
                       + ": " + e.getMessage());  
               ex.initCause(e);  
               throw ex;  
           } finally {  
               // Don't retain static reference on context.  
               mConstructorArgs[0] = lastContext;  
               mConstructorArgs[1] = null;  
           }  
  
           return result;  
       }  
   }

1.首先拿到咱們加載XML文件的屬性值

2.聲明View result = root; 最終值返回result。

View temp;  
                   if (TAG_1995.equals(name)) {  
                       temp = new BlinkLayout(mContext, attrs);  
                   } else {  
                       temp = createViewFromTag(root, name, attrs);  
                   }

在這裏咱們建立了View;

if(root!=null)  
{  
 params = root.generateLayoutParams(attrs);  
        if (!attachToRoot)  
 {  
   temp.setLayoutParams(params);  
 }  
}

能夠看到,當root不爲null,attachToRoot爲false時,爲temp設置了LayoutParams.

繼續往下看:

if (root != null && attachToRoot)  
{  
root.addView(temp, params);  
}

能夠看到,當root不爲null,attachToRoot爲true時,將temp按照params添加到root中

if (root == null || !attachToRoot) {   
result = temp;   
}

能夠看到,當root爲null,直接將temp賦值給result;最後返回result。

從上面的分析已經能夠看出:

Inflate(resId , null ) 只建立temp ,返回temp

Inflate(resId , parent, false )建立temp,而後執行temp.setLayoutParams(params);返回temp

Inflate(resId , parent, true ) 建立temp,而後執行root.addView(temp, params);最後返回root

由上面已經可以解釋:

Inflate(resId , null )不能正確處理寬和高是由於:layout_width,layout_height是相對了父級設置的,必須與父級的LayoutParams一致。而此temp的getLayoutParams爲null

Inflate(resId , parent,false ) 能夠正確處理,由於temp.setLayoutParams(params);這個params正是root.generateLayoutParams(attrs);獲得的。

Inflate(resId , parent,true )不只可以正確的處理,並且已經把resId這個view加入到了parent,而且返回的是parent,和以上二者返回值有絕對的區別,還記得文章前面的例子上,MyAdapter裏面的getView報的錯誤: 

這是由於源碼中調用了root.addView(temp, params);而此時的root是咱們的ListView,ListView爲AdapterView的子類:
直接看AdapterView的源碼:

@Override  
  public void addView(View child) {  
        throw new UnsupportedOperationException("addView(View) is not supported in AdapterView");  
  }

看到這裏就能解決我以前出現的異常呢。

4.進一步探究

主Activity的佈局文件:

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/mTextView"
    android:layout_width="120dp"
    android:layout_height="120dp"
    android:text="測試"
    android:textSize="30sp"
    android:gravity="center"
   >
</TextView>

主Activity:

public class ListViewActivity extends AppCompatActivity {
    private LayoutInflater mInflater;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_list_view);
        mInflater = LayoutInflater.from(this);

        View testView1 = mInflater.inflate(R.layout.activity_list_view,null);

        View testView2 = mInflater.inflate(R.layout.activity_list_view,
                (ViewGroup)findViewById(android.R.id.content),false);

        View testView3 = mInflater.inflate(R.layout.activity_list_view,
                (ViewGroup)findViewById(android.R.id.content),true);

        Log.e("le","testView1="+testView1 +"testView1.layoutParams="+testView1.getLayoutParams());
        Log.e("le","testView2="+testView1 +"testView2.layoutParams="+testView2.getLayoutParams());
        Log.e("le","testView3="+testView1 +"testView3.layoutParams="+testView3.getLayoutParams());
    }
}

能夠看到咱們的主Activity並無執行setContentView,僅僅執行了LayoutInflater的3個方法。
注:parent咱們用的是Activity的內容區域:即android.R.id.content,是一個FrameLayout,咱們在setContentView(resId)時,其實系統會自動爲了包上一層FrameLayout(id=content)。

按照咱們上面的說法:

view1的layoutParams 應該爲null
view2的layoutParams 應該不爲null,且爲FrameLayout.LayoutParams
view3爲FrameLayout,且將這個button添加到Activity的內容區域了(由於R.id.content表明Actvity內容區域)

下面看一下輸出結果,和Activity的展現:

可見,雖然咱們沒有執行setContentView,可是依然能夠看到繪製的控件,是由於

View testView3 = mInflater.inflate(R.layout.activity_list_view,
                (ViewGroup)findViewById(android.R.id.content),true)

這個方法內部已經執行了root.addView(temp , params); 上面已經解析過了。

也能夠看出:和咱們的推測徹底一致,到此已經徹底說明了inflate3個重載的方法的區別。相信你們之後在使用時也能選擇出最好的方式。不過下面準備從ViewGroup和View的角度來講一下,爲啥layoutParams爲null,就不能這確的處理。

5.從ViewGroup和View的角度來解析

若是你們對自定義ViewGroup和自定義View有必定的掌握,確定不會對onMeasure方法陌生:
ViewGroup的onMeasure方法所作的是:

爲childView設置測量模式和測量出來的值。

如何設置呢?就是根據LayoutParams。
若是childView的寬爲:LayoutParams. MATCH_PARENT,則設置模式爲MeasureSpec.EXACTLY,且爲childView計算寬度。
若是childView的寬爲:固定值(即大於0),則設置模式爲MeasureSpec.EXACTLY,且將lp.width直接做爲childView的寬度。
若是childView的寬爲:LayoutParams. WRAP_CONTENT,則設置模式爲:MeasureSpec.AT_MOST
高度與寬度相似。

View的onMeasure方法:
主要作的就是根據ViewGroup傳入的測量模式和測量值,計算本身應該的寬和高:
通常是這樣的流程:
若是寬的模式是AT_MOST:則本身計算寬的值。
若是寬的模式是EXACTLY:則直接使用MeasureSpec.getSize(widthMeasureSpec);

對於最後一塊,若是不清楚,沒關係,之後我會在自定義ViewGroup和自定義View時詳細講解的。

大概就是這樣的流程,真正的繪製過程確定比這個要複雜,就是爲了說明若是View的寬和高若是設置爲準確值,則必定依賴於LayoutParams,因此咱們的inflate(resId,null)纔沒能正確處理寬和高。

相關文章
相關標籤/搜索