Android 列表控件的Item設(shè)置布局寬高無效問題探究

本文從源碼分析來解釋一個(gè)開發(fā)中困擾了我許久的問題:給ListView的Item設(shè)置固定的高度無效,其他列表控件GridView和RecyclerView也有同樣的問題。

我們通過代碼復(fù)現(xiàn)一下問題,部分代碼如下:

  • Activity代碼:
private void initView() {
      mListView = (ListView) findViewById(R.id.list_view);
        mList = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            mList.add("這是第" + i + "個(gè)item");
        }
        mAdapter = new MyListAdapter(this,mList);
        mListView.setAdapter(mAdapter);
    } 
  • Adapter代碼
public class MyListAdapter extends BaseAdapter {
    private Context mContext;
    private List<String> mList;

    public MyListAdapter(Context context, List<String> list) {
        this.mContext = context;
        this.mList = list;
    }

    @Override
    public int getCount() {
        return mList.size();
    }

    @Override
    public Object getItem(int position) {
        return null;
    }

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

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder viewHolder;
        if (convertView == null) {
            viewHolder = new ViewHolder();
            convertView = LayoutInflater.from(mContext).inflate(R.layout.list_item, null);
            viewHolder.tv = convertView.findViewById(R.id.tv);
            convertView.setTag(viewHolder);
        } else {
            viewHolder = (ViewHolder) convertView.getTag();
        }
        viewHolder.tv.setText(mList.get(position));
        return convertView;
    }

    static class ViewHolder {
        TextView tv;
    }
}
  • item布局文件如下:
<?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="100dp"
    android:gravity="center_vertical"
    android:background="#ffffff"
    android:orientation="vertical">

    <TextView
        android:id="@+id/tv"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:background="#ff00ff"
        android:gravity="center"/>
</LinearLayout>

運(yùn)行的結(jié)果如下:


image

上面的情況我們加載布局的方式是:

 convertView = LayoutInflater.from(mContext).inflate(R.layout.list_item, null);

我們可以看到雖然我們給item的根布局設(shè)置了高度為100dp,但是并沒有用item的高度還是TextView設(shè)置的50dp。

我們修改加載布局的方式:

 convertView = LayoutInflater.from(mContext).inflate(R.layout.list_item, parent,false);

我們再次運(yùn)行代碼結(jié)果如下圖:


image

可以看到這個(gè)時(shí)候根布局設(shè)置的100dp生效了,textView為50dp,豎直居中顯示。

所以加載布局的方式不同,會導(dǎo)致item設(shè)置寬高是否生效。那么這兩種加載布局的寫法到底有什么區(qū)別呢?我們通過源碼分析來看一下inflate這個(gè)方法。

inflate 加載布局的方法

我們知道加載布局方式的不同會決定item設(shè)置的寬高是否有用,我們通過查看方法參數(shù)可以看到inflate有四種加載布局的方式。如下圖:


image

從上圖可以看到 inflate 方法有四個(gè)重載方法,有兩個(gè)方法第一個(gè)參數(shù)接收的是一個(gè)布局文件,另外兩個(gè)接收的是XmlPullParse。

我們來看一下inflate接受布局文件ID的源碼:

image

看源碼就知道,接收布局文件的inflate方法里面調(diào)用的是接收XmlPullParse的方法。因此,我們一般只調(diào)用接收布局文件ID的inflate方法。兩個(gè)重載方法的區(qū)別在于有無第三個(gè)參數(shù)attachToRoot, 而從源碼里里面可以看到,兩個(gè)參數(shù)的方法最終調(diào)用的是三個(gè)參數(shù)的inflate方法:

image

源碼分析寬高失效原因

看過了inflate的幾種方法,我們需要了解的就是三個(gè)參數(shù)的inflate方法,所以我們先去看一下三個(gè)參數(shù)的inflate方法參數(shù)是什么意思:


image
  • parser:加載的布局文件資源id,如:R.layout.list_item。
  • root:如果attachToRoot(也就是第三個(gè)參數(shù))為true, 那么root就是為新加載的View指定的父View。否則,root只是一個(gè)為返回View層級的根布局提供LayoutParams值的簡單對象。
  • attachToRoot: 新加載的布局是否添加到root,如果為false,root參數(shù)僅僅用于為xml根布局創(chuàng)建正確的LayoutParams子類(列如:根布局為LinearLayout,則用LinearLayout.LayoutParam)。

接下來我們來分析一下三個(gè)參數(shù)的inflate 方法的源碼,源碼如下:

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");

            final Context inflaterContext = mContext;
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            Context lastContext = (Context) mConstructorArgs[0];
            mConstructorArgs[0] = inflaterContext;
            //把傳入的ViewGroup最終返回去了
            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, inflaterContext, attrs, false);
                } else { // 這里是重點(diǎn)
                    // Temp is the root view that was found in the xml
                   //首先創(chuàng)建了xml布局文件的根View——temp 
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                    ViewGroup.LayoutParams params = null;
                    //如果傳入的root不為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);
                       //如果在root不為null, 并且attachToRoot為false,就為temp View(也就是通過inflate加載的根View)設(shè)置LayoutParams(root生成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");
                    }

                    // Inflate all children under temp against its context. 
                   //加載根布局View——temp和下面的子View(把tempView 添加到root)
                    rInflateChildren(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.
                  //如果root不為null ,并且attachToRoot 為true時(shí),將從xml加載的View添加到root.(因?yàn)橹耙呀?jīng)add了,這就是第二次add,會crash)
                    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.
                  // 最后,如果root為null,或者attachToRoot為false,那么最終inflate返回的值就是從xml加載的View(temp,沒有設(shè)置LayoutParams),否則,返回的就是root(temp已添加到root)
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }

            } catch (XmlPullParserException e) {
                final InflateException ie = new InflateException(e.getMessage(), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            } catch (Exception e) {
                final InflateException ie = new InflateException(parser.getPositionDescription()
                        + ": " + e.getMessage(), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            } finally {
                // Don't retain static reference on context.
                mConstructorArgs[0] = lastContext;
                mConstructorArgs[1] = null;

                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }

            return result;
        }
    }

這就是inflate的重要代碼,從上面我們可以看出

  • 如果我們是
 convertView = LayoutInflater.from(mContext).inflate(R.layout.list_item, null);

這種加載布局的方法,我們沒有指定新加載的View添加到哪個(gè)父容器,root為null,也沒有root提供LayoutParams布局信息。這個(gè)時(shí)候直接返回的就是從xml加載的temp View。

      if (root == null || !attachToRoot) {
                        result = temp;
      }
  • 如果我們是
 convertView = LayoutInflater.from(mContext).inflate(R.layout.list_item, parent, false);

這種加載布局的方式, root不為null ,attachToRoot 為false, 就為temp View(也就是通過inflate加載的根View)設(shè)置LayoutParams,并且通過rInflateChildren(parser, temp, attrs, true)加載了temp下面的子view。當(dāng)然,如果加載布局時(shí)第三個(gè)參數(shù)設(shè)置為true時(shí),一運(yùn)行就會崩潰,因?yàn)橄喈?dāng)于 addView 了兩次,會crash。

那么inflate加載了布局之后ListView又是怎么把item布局加載進(jìn)去的呢?

我們找到ListView里面的setupChild方法,這個(gè)方法的注釋是將View添加到ViewGroup中,對子視圖定位。

我們看一下核心的方法實(shí)現(xiàn):

private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
            boolean selected, boolean isAttachedToWindow) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "setupListItem");
       ... 省略部分代碼
        //重點(diǎn)就是這里,獲取子View的LayoutParams
        AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
       //如果itemView獲取到的LayoutParams為null,就生成默認(rèn)的LayoutParams
        if (p == null) { 
            p = (AbsListView.LayoutParams) generateDefaultLayoutParams();
        }
        p.viewType = mAdapter.getItemViewType(position);
        p.isEnabled = mAdapter.isEnabled(position);
    }

通過上面可以看出,我們先獲取itemView的LayoutParams,如果得到的LayoutParams為null,就使用默認(rèn)的LayoutParams,而默認(rèn)的LayoutParams,寬度是MATCH_PARENT,高度是WRAP_CONTENT。


image

到這里就知道為什么不同的布局加載方式會導(dǎo)致item設(shè)置寬高無效了。

第一種加載方式:

 convertView = LayoutInflater.from(mContext).inflate(R.layout.list_item, null);

inflate的時(shí)候直接返回的就是從xml加載的temp View。沒有設(shè)置LayoutParams信息,在添加到ListView時(shí),得到的LayoutParams信息為null,所以設(shè)置了默認(rèn)的LayoutParams信息,就是高度為WRAP_CONTENT,所以給item設(shè)置的固定高度沒有用。

而第二種加載方式

 convertView = LayoutInflater.from(mContext).inflate(R.layout.list_item, parent, false);

inflate加載布局的時(shí)候返回的root View(添加了temp View的)。root View是有LayoutParams信息的。在添加到ListView時(shí),得到的LayoutParams信息為root的LayoutParams信息,也就是item布局設(shè)置的寬高信息,所以給item設(shè)置的固定高度有用。

總結(jié)

通過查看源碼我們了解了infalte 加載布局的幾種寫法,查看了ListView添加布局的方法,解釋了兩種加載布局的方式在ListView 中為什么一種寬高會失效,而另一種則不會失效。因此在使用列表控件寫列表的時(shí)候,如果要設(shè)置item寬高有效,我們應(yīng)該使用item布局不會失效的這種方式:

 convertView = LayoutInflater.from(mContext).inflate(R.layout.list_item, parent, false);

其他列表控件GridView、RecyclerView也類似,如果感興趣可以去看一下GridView和RecyclerView添加Item的方法,雖然有不同但是最終都是判斷子View得到的LayoutParams信息是否為空,但是RecyclerView返回的默認(rèn)LayoutParams信息是寬高都是WRAP_CONTENT。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容