LayoutInflater原始碼分析與應用 | 掘金技術徵文

GitLqr發表於2017-04-23

*本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家釋出

一、簡述

LayoutInflater直譯為 佈局填充器,它是用來建立佈局檢視的,常用inflate()將一個xml佈局檔案轉換成一個View,下面先介紹下獲取LayoutInflater的三種方式 和 建立View的兩種方式。

1、獲取LayoutInflater的三種方式

  1. LayoutInflater inflater = getLayoutInflater(); //呼叫Activity的getLayoutInflater()
  2. LayoutInflater inflater =(LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
  3. LayoutInflater inflater = LayoutInflater.from(context);

其實不管是哪種方式,最後都是通過方式2獲取到LayoutInflater的,如:

LayoutInflater原始碼分析與應用 | 掘金技術徵文

2、建立View的兩種方式

  1. View.inflate();
  2. LayoutInflater.from(context).inflate();

二、原始碼分析

上面兩種建立View的方式都是開發中常用的,那兩者有什麼關係嗎?下面對View.inflate()進行方法呼叫分析:

1、View.inflate()最終呼叫方法探究

1)按住Ctrl+滑鼠左鍵檢視View.inflate()方法

LayoutInflater原始碼分析與應用 | 掘金技術徵文

可以看到View.inflate()就是呼叫了LayoutInflater.from(context).inflate()。

好,到這一步要明確,不管我們研究哪種方式,實際上都研究方式2,即LayoutInflater.from(context).inflate()。

2)按住Ctrl+滑鼠左鍵檢視LayoutInflater.from(context).inflate(resource, root)方法。

LayoutInflater原始碼分析與應用 | 掘金技術徵文

嗯?LayoutInflater.from(context).inflate(resource, root)再呼叫了自己的過載inflate(resource, root, root != null)。

3)按住Ctrl+滑鼠左鍵檢視LayoutInflater.from(context).inflate(resource, root).inflate(resource, root, root != null)方法。

LayoutInflater原始碼分析與應用 | 掘金技術徵文

嗯??LayoutInflater.from(context).inflate(resource, root).inflate(resource, root, root != null)再再呼叫了自己的過載inflate(parser, root, attachToRoot)。

4)按住Ctrl+滑鼠左鍵檢視LayoutInflater.from(context).inflate(resource, root).inflate(resource, root, root != null).inflate(parser, root, attachToRoot)方法。

這下總算是到頭了,不過程式碼太長,這裡就截了一半的圖(這不是重點)。

LayoutInflater原始碼分析與應用 | 掘金技術徵文

好,重點來了,到這步我們可以明白一點,View.inflate()整個方法呼叫鏈如下:

View.inflate() = 
    LayoutInflater.from(context)
        .inflate(resource, root)
        .inflate(resource, root, root != null)
        .inflate(parser, root, attachToRoot)複製程式碼

2、LayoutInflater的inflate(parser, root, attachToRoot)做了什麼?

由於程式碼太長,不方便截圖,下面貼出程式碼中的重點程式碼:

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {

                ...
                省略程式碼~
                ...

                final View temp = createViewFromTag(root, name, inflaterContext, 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);
                    }
                }

                ...
                省略程式碼~
                ...

                // 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;
                }

                ...
                省略程式碼~
                ...

        return result;
    }
}複製程式碼

該inflate方法中有以下四步操作:

  1. 通過使用XmlPullParser parser將xml佈局檔案轉換成檢視temp。
  2. 判斷ViewGroup root物件是否為null,來決定要不要給temp設定LayoutParams。
  3. 判斷boolean attachToRoot是否為true,來決定是否要把temp順便加到ViewGroup root中。
  4. 最後返回檢視temp。

到這裡就把建立檢視的流程分析完了,接下來是比較 View.inflate()和 LayoutInflater.from(context).inflate()的區別。

3、View.inflate()和 LayoutInflater.from(context).inflate()的區別

1)View.inflate()第三個引數的解析:

開發中常常會對第三個引數(ViewGroup root)傳入null吧,通過上面對最終inflate方法的分析,可以知道,如果ViewGroup root取值為null,則得到的檢視temp不會被設定LayoutParams。下面做個試驗:

View itemView = View.inflate(parent.getContext(), android.R.layout.simple_list_item_1, null);
ViewGroup.LayoutParams params = itemView.getLayoutParams();
Log.e("CSDN_LQR", "params == null : " + (params == null));複製程式碼

列印結果如下:

LayoutInflater原始碼分析與應用 | 掘金技術徵文

同理,將第三個引數傳入一個確實存在的ViewGroup時,結果就是檢視temp能獲取到LayoutParams,有興趣的可以自己試試。

2)LayoutInflater.from(context).inflate()的優勢:

*下面的場景分析將體現出LayoutInflater.from(context).inflate()的靈活性。

如果是在RecyclerView或ListView中使用View.inflate()建立佈局檢視,又想對建立出來的佈局檢視進行高度等引數設定時,會有什麼瓶頸呢?

下面貼出我之前寫過的一段用於瀑布流介面卡的程式碼:

public class MyStaggeredAdapter extends RecyclerView.Adapter<MyStaggeredAdapter.MyViewHolder> {

    private List<String> mData;
    private Random mRandom = new Random();

    public MyStaggeredAdapter(List<String> data) {
        mData = data;
    }

    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        //這裡使用的是安卓自帶的文字控制元件佈局
        View itemView = View.inflate(parent.getContext(), android.R.layout.simple_list_item_1, null);
        return new MyViewHolder(itemView);
    }

    @Override
    public void onBindViewHolder(MyViewHolder holder, int position) {
        //為實現瀑布流效果,需要對條目高度進行設定(讓各個條目的高度不同)
        ViewGroup.LayoutParams params = holder.mTv.getLayoutParams();
        params.height = mRandom.nextInt(200) + 200;
        holder.mTv.setLayoutParams(params);
        holder.mTv.setBackgroundColor(Color.argb(255, 180 + mRandom.nextInt(60) + 30, 180 + mRandom.nextInt(60) + 30, 180 + mRandom.nextInt(60) + 30));
        holder.mTv.setText(mData.get(position));
    }

    @Override
    public int getItemCount() {
        return mData.size();
    }

    class MyViewHolder extends RecyclerView.ViewHolder {

        TextView mTv;

        public MyViewHolder(View itemView) {
            super(itemView);
            mTv = (TextView) itemView.findViewById(android.R.id.text1);
        }
    }

}複製程式碼

經過上面對View.inflate()的第三個引數解析之後,這程式碼的問題一眼就能看出來了吧,沒錯,就是ViewGroup.LayoutParams params = holder.mTv.getLayoutParams();這行程式碼獲取到的LayoutParams為空,不信?走一個。

LayoutInflater原始碼分析與應用 | 掘金技術徵文

接下來理所當然的要讓得到的LayoutParams不為空啦,所以將onCreateViewHolder()的程式碼修改如下:

@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    //這裡使用的是安卓自帶的文字控制元件佈局
    View itemView = View.inflate(parent.getContext(), android.R.layout.simple_list_item_1, parent);
    return new MyViewHolder(itemView);
}複製程式碼

傳入的ViewGroup parent不為null,所以肯定獲取的LayoutParams不為空,但是又有一個問題,看報錯。

LayoutInflater原始碼分析與應用 | 掘金技術徵文

為什麼會報這樣的錯呢?回看最終inflate()的四個步驟:

  1. 通過使用XmlPullParser parser將xml佈局檔案轉換成檢視temp。
  2. 判斷ViewGroup root物件是否為null,來決定要不要給temp設定LayoutParams。
  3. 判斷boolean attachToRoot是否為true,來決定是否要把temp順便加到ViewGroup root中。
  4. 最後返回檢視temp。

步驟2讓條目獲取的LayoutParams不為空沒錯,但是步驟3出問題了,當使用View.inflate(parent.getContext(), android.R.layout.simple_list_item_1, parent)傳入parent後,boolean attachToRoot的取值就是為true,所以建立出來的條目會順便新增到ViewGroup中(這裡的ViewGroup就是RecyclerView),而RecyclerView本身就會自動將條目新增到自身,這樣就新增了兩次,故報錯。那為什麼attachToRoot的取值是true呢?再看View.inflate()的整個方法呼叫鏈:

View.inflate() = 
    LayoutInflater.from(context)
        .inflate(resource, root)
        .inflate(resource, root, root != null)
        .inflate(parser, root, attachToRoot)複製程式碼

boolean attachToRoot的取值取決於root(也就是parent)是否為空,這就是View.inflate()的瓶頸,它沒法靈活的指定boolean attachToRoot的取值。這裡我就是隻是想讓建立出來的檢視能得到LayoutParams,但不新增到ViewGroup中,這樣的要求可以通過LayoutInflater.from(context).inflate()來實現。所以下面將onCreateViewHolder()的程式碼修改如下:

@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    View itemView = LayoutInflater.from(parent.getContext()).inflate(android.R.layout.simple_list_item_1,parent,false);
    ViewGroup.LayoutParams params = itemView.getLayoutParams();
    Log.e("CSDN_LQR", "params == null : " + (params == null));
    return new MyViewHolder(itemView);
}複製程式碼

程式碼中LayoutInflater.from(parent.getContext()).inflate(android.R.layout.simple_list_item_1,parent,false)傳入了parent(即ViewGroup不為null),所以建立出來的檢視可以得到LayoutParams,同時又指定attachToRoot的取值為false,即不新增到ViewGroup中。到這裡,上面重覆新增子控制元件的問題就解決了,總結一下吧:

  • View.inflate()第三個引數若不為null,則建立出來的檢視一定能獲得LayoutParams,反之,不一定。(下面會解釋)
  • LayoutInflater.from(context).inflate()可以靈活的指定傳入的ViewGroup是否為空來決定建立出來的檢視能否獲得LayoutParams,同時又可以指定attachToRoot的取值來決定建立出來的檢視是否要新增到ViewGroup中。

三、小細節

*上面已經將LayoutInflater的原始碼分析完畢,現在還有一個小問題,其實跟本文主題沒多大關係,當作擴充來看吧。

前面說到,View.inflate()第三個引數若不為null,則建立出來的檢視一定能獲得LayoutParams,反之,不一定。這話怎麼理解?

也就是說,即使View.inflate()第三個引數為null,建立出來的檢視也有可能獲得LayoutParams咯?是的,說到底,這個LayoutParams的有無,實際取決於條目本身是否有父控制元件,且看上面用到的simple_list_item_1佈局:

LayoutInflater原始碼分析與應用 | 掘金技術徵文

發現了吧,就一個TextView,沒有父控制元件,那如果我給它加個父控制元件,同時使用最開始的方式也能順利得到LayoutParams呢?程式碼如下:

@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    View itemView = View.inflate(parent.getContext(), R.layout.item_layout, null);
    return new MyViewHolder(itemView);
}

@Override
public void onBindViewHolder(MyViewHolder holder, int position) {
    ViewGroup.LayoutParams params = holder.mTv.getLayoutParams();
    Log.e("CSDN_LQR", "params == null : " + (params == null));
    ...
    控制元件設定
    ...
}複製程式碼

item_layout的佈局程式碼如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical">

    <TextView
        android:id="@android:id/text1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_vertical"
        android:minHeight="?android:attr/listPreferredItemHeightSmall"
        android:textAppearance="?android:attr/textAppearanceListItemSmall"/>
</LinearLayout>複製程式碼

執行,果然可以獲得LayoutParams,列印結果如下:

LayoutInflater原始碼分析與應用 | 掘金技術徵文

四、最後

本人也是頭次寫類分析型的文章,如描述有誤,請不吝賜教,同時還請各位看客多擔待,指出後本人會盡快修改,謝謝。


這篇文章參加掘金技術徵文:gold.xitu.io/post/58522d…

相關文章