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

2017-11-21  本文已影响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));
上一篇下一篇

猜你喜欢

热点阅读