RecyclerView Item 佈局寬高無效問題探究

轉載地址:http://www.jianshu.com/p/9a6db88b8ad3

前言

這個問題很早之前就碰到過,後來通過google找到了解決辦法,也就沒有去管它了,直到最近有朋友問到這個問題,感覺很熟悉卻又說不出具體原因,因此,就想通過源碼分析一下。順便做個總結,避免以後出現類似的問題。
前言

問題復現

爲什麼發現了這個問題呢?是當時要寫一個列表,列表本來很簡單,一行顯示一個文本,實現起來也很容易,一個RecyclerView就搞定。

Activity以及Adapter代碼如下:

private void initView() {
        mRecyclerView = (RecyclerView) findViewById(R.id.rv_inflate_test);
        RVAdapter adapter = new RVAdapter();
        adapter.setData(mockData());
        LinearLayoutManager manager = new LinearLayoutManager(this);
        manager.setOrientation(LinearLayoutManager.VERTICAL);
        mRecyclerView.addItemDecoration(new DividerItemDecoration(this,DividerItemDecoration.VERTICAL));
        mRecyclerView.setLayoutManager(manager);
        mRecyclerView.setAdapter(adapter);
        adapter.notifyDataSetChanged();
    }


    private List<String> mockData(){
        List<String> datas = new ArrayList<>();
        for(int i=0;i<100;i++){
            datas.add("這是第"+i+ "個item ");
        }

        return datas;
    }


    public static class RVAdapter extends RecyclerView.Adapter{
        private List<String> mData;

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

        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            return new InflateViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.inflate_test_item,null));
        }

        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
             InflateViewHolder viewHolder = (InflateViewHolder) holder;
             ((InflateViewHolder) holder).mTextView.setText(mData.get(position));
        }

        @Override
        public int getItemCount() {
            return mData == null ? 0:mData.size();
        }

        public static class InflateViewHolder extends RecyclerView.ViewHolder{
            private TextView mTextView;
            public InflateViewHolder(View itemView) {
                super(itemView);
                mTextView = (TextView) itemView.findViewById(R.id.text_item);
            }
        }
    }

然後RecyclerView的item佈局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="wrap_content">
   <TextView
       android:id="@+id/text_item"
       android:layout_width="match_parent"
       android:layout_height="50dp"
       android:textSize="18sp"
       android:textColor="@android:color/white"
       android:background="#AA47BC"
       android:gravity="center"
       />
</LinearLayout>

代碼很簡單,就是一個RecyclerView 顯示一個簡單的列表,一行顯示一個文本。寫完代碼運行看一下效果:

這裏寫圖片描述

運行效果一看,這是什麼鬼?右邊空出來這麼大一塊?一看就覺得是item的佈局寫錯了,難道item的寬寫成wrap_content? 那就去改一下嘛。進入item佈局一看:

這裏寫圖片描述

不對啊,明明佈局的寬寫的是match_parent,爲什麼運行的結果就是包裹內容的呢?然後就想着既然LinearLayout作爲根佈局寬失效了,那就換其他幾種佈局方式試一下呢?

根佈局換爲FrameLayout,其他不變:

這裏寫圖片描述

運行效果如下:
這裏寫圖片描述

效果和LinearLayout一樣,還是不行,那再換成RelativeLayout試一下:
這裏寫圖片描述

看一下運行效果:

這裏寫圖片描述

換成RelativeLayout後,運行的效果,好像就是我們想要的了,曾經一度以後只要將跟佈局換成RelativeLayout,就沒有寬高失效的問題了。爲了驗證這個問題,我改變了高度再來測試,如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="200dp"
              android:layout_height="200dp"
              android:background="@android:color/holo_red_light"
    >
   <TextView
       android:id="@+id/text_item"
       android:layout_width="match_parent"
       android:layout_height="50dp"
       android:textSize="18sp"
       android:textColor="@android:color/white"
       android:background="#AA47BC"
       android:gravity="center"
       />

將佈局的寬和高固定一個確定的值200dp,然後再來看一下運行效果。

這裏寫圖片描述

如上,並沒有什麼卵用,寬和高都失效了。然後又在固定寬高的情況下將佈局換爲原來的LinearLayout和FrameLayout,效果和前面一樣,包裹內容。

因此,不管用什麼佈局作爲根佈局都會出現寬高失效的問題,那就得另找原因。到底是什麼原因呢?想到以前寫了這麼多的列表,也沒有出現寬高失效的問題啊?於是就去找以前的代碼來對比一下:

通過對比,發現寬高失效與不失效的區別在與Adapter中創建ViewHolder是加載佈局的方式不同:
LayoutInflater.from(parent.getContext()).inflate(R.layout.inflate_test_item,null)

以上這種加載方式Item寬高失效。

LayoutInflater.from(parent.getContext()).inflate(R.layout.inflate_test_item,parent,false)

以上這種方式加載佈局item不會出現寬高失效。,效果如下(寬和高都爲200dp):
這裏寫圖片描述

問題我們算是定位到了,就是加載佈局的方式不一樣,那麼這兩種加載佈局的寫法到底有什麼區別呢?這個我們就需要去深入瞭解inflate這個方法了

inflate 加載佈局幾種寫法的區別

上面我們定位到了RecyclerView item 佈局寬高失效的原因在於使用inflate 加載佈局時的問題,那麼我們就看一下inflate這個方法:
這裏寫圖片描述

從上圖可以看到 inflate 方法有四個重載方法,有兩個方法第一個參數接收的是一個佈局文件id,另外兩個接收的是XmlPullParse,看源碼就知道,接收佈局文件的inflate方法裏面調用的是接收XmlPullParse的方法。

這裏寫圖片描述

因此,我們一般只調用接收佈局文件ID的inflate方法。兩個重載方法的區別在於有無第三個參數attachToRoot, 而從源碼裏裏面可以看到,兩個參數的方法最終調用的是三個參數的inflate方法:
這裏寫圖片描述

第三個參數的值是根據第二個參數的值來判斷的。

因此我們只需要分析一下三個參數的inflate方法,看一下這個方法的定義:

/**
     * Inflate a new view hierarchy from the specified xml resource. Throws
     * {@link InflateException} if there is an error.
     * 
     * @param resource ID for an XML layout resource to load (e.g.,
     *        <code>R.layout.main_page</code>)
     * @param root Optional view to be the parent of the generated hierarchy (if
     *        <em>attachToRoot</em> is true), or else simply an object that
     *        provides a set of LayoutParams values for root of the returned
     *        hierarchy (if <em>attachToRoot</em> is false.)
     * @param attachToRoot Whether the inflated hierarchy should be attached to
     *        the root parameter? If false, root is only used to create the
     *        correct subclass of LayoutParams for the root view in the XML.
     * @return The root View of the inflated hierarchy. If root was supplied and
     *         attachToRoot is true, this is root; otherwise it is the root of
     *         the inflated XML file.
     */
    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)

解釋:從指定的xml資源文件加載一個新的View,如果發生錯誤會拋出InflateException異常。
參數解釋:
resource:加載的佈局文件資源id,如:R.layout.main_page。
root:如果attachToRoot(也就是第三個參數)爲true, 那麼root就是爲新加載的View指定的父View。否則,root只是一個爲返回View層級的根佈局提供LayoutParams值的簡單對象。
attachToRoot: 新加載的佈局是否添加到root,如果爲false,root參數僅僅用於爲xml根佈局創建正確的LayoutParams子類(列如:根佈局爲LinearLayout,則用LinearLayout.LayoutParam)。

瞭解了這幾個參數的意義後,我們來看一下前面提到的兩種寫法

第一種:root 爲null

View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.inflate_test_item,null)

這可能是我們用得比較多的一種方式,直接提供一個佈局,返回一個View,根據上面的幾個參數解釋就知道,這種方式,沒有指定新加載的View添加到哪個父容器,也沒有root提供LayoutParams佈局信息。這個時候,如果調用view.getLayoutParams() 返回的值爲null。通過上面的測試,我們知道這種方式會導致RecyclerView Item 佈局寬高失效。具體原因稍後再分析。

第二種:root不爲null,attachToRoot爲false

View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.inflate_test_item,parent,false)

這種方式加載,root不爲null,但是attachToRoot 爲 false,因此,加載的View不會添加到root,但是會用root生成的LayoutParams信息。這種方式就是上面我們說的 RecyclerView Item 寬高不會失效的加載方式。

那麼爲什麼第一種加載方式RecyclerView Item 佈局寬高會失效?而第二種加載方式寬高不會失效呢?我們接下來從原來來分析一下。

源碼分析寬高失效原因

1,首先我們來分析一下inflate 方法的源碼:

....
          //前面省略
          //result是最終返回的View
          View result = root;

            try {
               ...
               // 省略部分代碼
               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, inflaterContext, attrs, false);
                } else {

                   // 重點就在這個else代碼塊裏了
                    //解釋1:首先創建了xml佈局文件的根View,temp View 

                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                    ViewGroup.LayoutParams params = null;
                   // 解釋2:判斷root是否爲null,不爲null,就通過root生成LayoutParams
                    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);
                        // 解釋3:如果在root不爲null, 並且attachToRoot爲false,就爲temp View(也就是通過inflate加載的根View)設置LayoutParams.
                        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");
                    }
                     //解釋4:加載根佈局temp View 下面的子View

                    rInflateChildren(parser, temp, attrs, true);

                    if (DEBUG) {
                        System.out.println("-----> done inflating children");
                    }

                    //解釋5: 注意這一步,root不爲null ,並且attachToRoot 爲true時,纔將從xml加載的View添加到root.
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }

                    // 解釋6:最後,如果root爲null,或者attachToRoot爲false,那麼最終inflate返回的值就是從xml加載的View(temp),否則,返回的就是root(temp已添加到root)
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }

            }

            ...
            //省略部分代碼
            return result;
        }

從上面這段代碼就能很清楚的說明前面提到的兩種加載方式的區別了。

第一種加載方式 root爲 null :源碼中的代碼在 解釋1 和 解釋6 直接返回的就是從xml加載的temp View。

第二種加載方式 root不爲null ,attachToRoot 爲false: 源碼中在 解釋3 和解釋5 ,爲temp 設置了通過root生成的LayoutParams信息,但是沒有add 添加到root 。

2,RecyclerView 部分源碼分析

分析了inflate的源碼,那麼接下來我們就要看一下RecyclerView 的源碼了,看一下是怎麼加載item 到 RecyclerView 的。由於RecyclerView的代碼比較多,我們就通過關鍵字來找,主要找holer.itemView ,加載的佈局就是ViewHolder中的itemView.

通過源碼我們找到了一個方法tryGetViewHolderForPositionByDeadline,其中有一段代碼如下:

//1,重點就在這裏了,獲取itemView 的LayoutParams
            final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
            final LayoutParams rvLayoutParams;
            if (lp == null) {
                // 2,如果itemView獲取到的LayoutParams爲null,就生成默認的LayoutParams
                rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
                holder.itemView.setLayoutParams(rvLayoutParams);
            } else if (!checkLayoutParams(lp)) {
                rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
                holder.itemView.setLayoutParams(rvLayoutParams);
            } else {
                rvLayoutParams = (LayoutParams) lp;
            }

            rvLayoutParams.mViewHolder = holder;
            rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
            return holder;

其實重點就在這個方法裏面了,看一下我註釋的兩個地方,先獲取itemView的LayoutParams,如果獲取到的LayoutPrams爲null 的話,那麼就生成默認的LayoutParams。我們看一下生成默認LayoutParams的方法generateDefaultLayoutParams

@Override
    protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
        if (mLayout == null) {
            throw new IllegalStateException("RecyclerView has no LayoutManager");
        }
        return mLayout.generateDefaultLayoutParams();
    }

注意,裏面又調用了mLayoutgenerateDefaultLayoutParams方法,這個mLayout其實就是RecyclerView 的佈局管理器LayoutManager.

這裏寫圖片描述
這裏寫圖片描述

可以看到generateDefaultLayoutParams是一個抽象方法,具體的實現由對應的LayoutManager實現,我們用的是LinearLayoutManager,因此我們看一下LinearLayoutManager 的實現。

/**
     * {@inheritDoc}
     */
    @Override
    public LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT);
    }

臥槽,看到這兒大概就明白了item佈局的寬高爲什麼會失效了,如果使用了默認生成LayoutParams這個方法,寬高都是WRAP_CONTENT。也就是說不管外面你的item根佈局 寬高寫的多少最終都是包裹內容。

那麼前面說的兩種方式哪一種用了這個方法呢?其實按照前面的分析和前面的結果來看,我們推測第一種加載方式(root爲null)使用了這個方法,而第二種加載方式(root不爲null,attachToRoot爲false)則沒有使用這個方法。因此我們斷點調試看一下:

第一種加載方式:
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.inflate_test_item,null)

這裏寫圖片描述

通過斷點調試如上圖,從itemView 中獲取的layoutParams爲null,因此會調用generateDefaultLayoutParams方法。因此會生成一個寬高都是wrap_content的LayoutParams,最後導致不管外面的item根佈局設置的寬高是多少都會失效。

第二種加載方式:
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.inflate_test_item,parent,false)

斷點調試如下圖:
這裏寫圖片描述

從上圖可以看出,這種加載方式從itemView是可以獲取LayoutParams的,爲RecyclerView的LayoutParams,因此就不會生成默認的LayoutParams,佈局設置的寬高也就不會失效。

總結

本文了解了infalte 加載佈局的幾種寫法,也解釋了每個參數的意義。最後通過源碼解釋了兩種加載佈局的方式在RecyclerView 中爲什麼一種寬高會失效,而另一種則不會失效。因此在使用RecyclerView寫列表的時候,我們應該使用item佈局不會失效的這種方式:

View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.inflate_test_item,parent,false)

可能有的同學會問,如果加載佈局時第三個參數設置爲true呢?結果會一樣嗎?你會發現,一運行就會崩潰

這裏寫圖片描述

爲什麼呢?因爲相當於 addView 了兩次.RecyclerView中不應該這樣使用。

好了,以上就是全部內容,如有問題,歡迎指正。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章