美文网首页
ListView: 我偷偷给直播间公聊埋个坑

ListView: 我偷偷给直播间公聊埋个坑

作者: 负了时光不负卿 | 来源:发表于2017-11-21 20:10 被阅读0次

    1. 前提说明

    虽然说Google官方提倡使用RecyclerView,但不代表说ListView就完全被RecyclerView取代。在某些业务上,使用ListView可能更加容易达到某种效果,比如说当请求网络数据为空时,需要展示一张默认图。ListView本身就有方法setEmptyView(View),而RecyclerView只能通过判断获取到的数据是否为空,调用EmptyView.setVisiable()来实现。

    空数据占位图.jpg

    2. 业务背景

    平常我们使用ListView的item都需要宽度铺满整个ListView,而公聊消息不一样,每一个item的长度根据内容确定的,。我们第一想法肯定是把Item的xml根布局的layout_width改成wrap_content,于是神奇的一幕就出现了...

    现实.PNG

    我们期望的却是酱紫!

    理想.PNG

    3. 代码概览

    我们先过一下编写好的Demo源码,代码量很少,和我们平时的书写习惯一致。

    MainActivity

    public class MainActivity extends Activity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            ListView listView = findViewById(R.id.listview);
            listView.setAdapter(new ListViewAdapter(this));
            listView.setDividerHeight(20);
        }
    }
    
    <?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">
    
        <ListView
            android:id="@+id/listview"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    
    </LinearLayout>
    

    Adapter与Item.xml

    public class ListViewAdapter extends BaseAdapter {
    
        private LayoutInflater layoutInflater;
        private String[] mDatas = {"Hello", "我是一条纯洁的公聊消息", "看看这个文字内容能有多长?"};
    
        public ListViewAdapter(Context context) {
            layoutInflater = LayoutInflater.from(context);
        }
    
        @Override
        public int getCount() {return Short.MAX_VALUE;}
    
        @Override
        public Object getItem(int i) {return mDatas[i % mDatas.length];}
    
        @Override
        public long getItemId(int i) {return i;}
    
        @Override
        public View getView(int position, View convertView, ViewGroup viewGroup) {
            ViewHolder holder = null;
            if (convertView == null) {
                convertView = layoutInflater.inflate(R.layout.item_view,null);           //别找了,重点在这
                holder = new ViewHolder(convertView);
                convertView.setTag(holder);
            } else {
                holder = (ViewHolder) convertView.getTag();
            }
            holder.contentView.setText(mDatas[position % mDatas.length]);
            return convertView;
        }
    
        class ViewHolder {
            public TextView contentView;
            public ViewHolder(View itemView) {
                contentView = itemView.findViewById(R.id.item_txt);
            }
        }
    }
    
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="wrap_content"                                    //别找了,重点在这
        android:layout_height="40dp"
        android:background="#666666"
        android:orientation="vertical"
        android:paddingBottom="10dp"
        android:paddingTop="10dp">
    
        <TextView
            android:id="@+id/item_txt"
            android:layout_width="wrap_content"
            android:layout_height="match_parent" />
    
    </LinearLayout>
    

    4. 异常点

    明明item的宽度为wrap_content,但展示出来却是铺满屏幕match_parent。

    5. 解决思路

    既然itemView宽度展示出现问题,那itemView.getLayoutParams()就是有问题的,而itemView添加到ListView上面是通过Adapter.getView()的方式,那我们只需要查看Adapter.getView()在哪个位置调用的,于是我们在ListView及父类中搜索,定位到AbsListView的obtainView()方法

    View obtainView(int position, boolean[] outMetadata) {
             ... 
            final View transientView = mRecycler.getTransientStateView(position);
            if (transientView != null) {
                final LayoutParams params = (LayoutParams) transientView.getLayoutParams();
    
                if (params.viewType == mAdapter.getItemViewType(position)) {
                    final View updatedView = mAdapter.getView(position, transientView, this);
                    if (updatedView != transientView) {
                        setItemViewLayoutParams(updatedView, position);
                        mRecycler.addScrapView(updatedView, position);
                    }
                }
           ....
    

    既然我们的ItemView通过obtainView()方法获取到了View对象,我们就可以往上面跟踪,看看这个方法的调用位置:

    • AbsListView.getHeightForPosition(int )
      官方解释: (Returns the height of the view for the specified position)返回这个位置View的高度,排除
    • onMeasure()方面中被调用。我们知道view在onlayout中才排布view,排除
    • measureHeightOfChildren(int, int, int, int, int)和第一个相似,排除
    • addViewAbove() addViewBelow(),makeAndAddView() 看名字与addView()方法相似,基本上也就断定的这三个方法了。

    接下来我们看一下这三个方法:

        private View addViewAbove(View theView, int position) {
            int abovePosition = position - 1;
            View view = obtainView(abovePosition, mIsScrap);
            int edgeOfNewChild = theView.getTop() - mDividerHeight;
            setupChild(view, abovePosition, edgeOfNewChild, false, mListPadding.left,
                    false, mIsScrap[0]);
            return view;
        }
    
     private View addViewBelow(View theView, int position) {
            int belowPosition = position + 1;
            View view = obtainView(belowPosition, mIsScrap);
            int edgeOfNewChild = theView.getBottom() + mDividerHeight;
            setupChild(view, belowPosition, edgeOfNewChild, true, mListPadding.left,
                    false, mIsScrap[0]);
            return view;
        }
    
        private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
                boolean selected) {
            if (!mDataChanged) {
                final View activeView = mRecycler.getActiveView(position);
                if (activeView != null) {
                    setupChild(activeView, position, y, flow, childrenLeft, selected, true);
                    return activeView;
                }
            }
            final View child = obtainView(position, mIsScrap);
            setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
            return child;
        }
    

    这三个方法,基本就是通过obtainView()获取到itemView,然后调用setupChild()方法去配置itemView,点进setupChild()查看一切就清楚了。

    private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
                boolean selected, boolean isAttachedToWindow) {
            ...
            AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
            if (p == null) {
                p = (AbsListView.LayoutParams) generateDefaultLayoutParams();
            }
           ...
    }
    
        protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
            return new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.WRAP_CONTENT, 0);
        }
    

    所以如果我们在obtainView()中通过mAdapter.getView()得到的View没有设置过LayoutParam,那么就给这个View设置了一个宽度match_parent的默认LayoutParams,那么前面的现象就说的过去了,
    推理归推理,我们还是需要找到证据,回到Adapter.getView()的方法体中。

     public View getView(int position, View convertView, ViewGroup viewGroup) {
            ViewHolder holder = null;
            if (convertView == null) {
                convertView = layoutInflater.inflate(R.layout.item_view, null);
                holder = new ViewHolder(convertView);
                convertView.setTag(holder);
            } else {
                holder = (ViewHolder) convertView.getTag();
            }
            holder.contentView.setText(mDatas[position % mDatas.length]);
            return convertView;
        }
    

    我们通过查看layoutInflater.inflate(R.layout.item_view, null)源码跟踪View的初始化过程,于是有了如下代码。

    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
            synchronized (mConstructorArgs) {
                ...
                View result = root;
                try {
                    final String name = parser.getName();
                    if (TAG_MERGE.equals(name)) {
                       ...
                        rInflate(parser, root, inflaterContext, attrs, false);
                    } else {
                        final View temp = createViewFromTag(root, name, inflaterContext, attrs);      //我才是重点行
                        ViewGroup.LayoutParams params = null;                                         ...
                        if (root != null) {                                                           ...
                            params = root.generateLayoutParams(attrs);                                ...
                            if (!attachToRoot) {                                                      ...
                                temp.setLayoutParams(params);                                         //我才是重点行
                            }
                        }
                        if (root != null && attachToRoot) {
                            root.addView(temp, params);
                        }
                        if (root == null || !attachToRoot) {
                            result = temp;
                        }
                    }
                } catch (XmlPullParserException e) { } 
                finally {}
                return result;
            }
        }
    

    之前调用layoutInflater.inflate(R.layout.item_view, null)方法,将root设置为空,所以itemView并没有设置LayoutParams。

    6.解决办法:

      1. layoutInflater.inflate(R.layout.item_view, viewGroup, false);
      1. getView()方法中手动为View设置LayoutParms
        itemView.setLayoutParams(new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
        ViewGroup.LayoutParams.WRAP_CONTENT, 0));

    相关文章

      网友评论

          本文标题:ListView: 我偷偷给直播间公聊埋个坑

          本文链接:https://www.haomeiwen.com/subject/uhewvxtx.html