【Android】ListView RecyclerView ScrollView里嵌套ListView 相对优雅的解决

首先给大家推荐一下我老师大神的人工智能教学网站。教学不仅零基础,通俗易懂,而且非常风趣幽默,还时不时有内涵黄段子!点这里可以跳转到网站

一 背景概述:

ScrollView里嵌套ListView,一直是Android开发者(反正至少是我们组)最讨厌的设计之一,完美打破ListView(RecyclerView)的复用机制,成功的将Native页面变成一个又臭又长的H5网页效果,但由于这种设计需求在我司项目实在太多见,无奈之下,我还是决定封装一下,毕竟,一个项目里同样的代码写第二遍的程序员都不是好的圣斗士。但是我真的是拒绝的 !拒绝的!拒绝的!真的不喜欢这种界面:

这里写图片描述


还拿我前两天做的这个项目来说吧,如上图,技能认可是一个“ListView”,工作经历是一个“ListView”,每个”ListView”的Item里还会有评论,评论又是一个“ListView”,项目经历 教育经历与此类似。。世界上最恐怖的事,不是ListView套ListView,是ListView套的ListView,里面还要继续嵌套ListView。。
(题外话,这个页面头部是个巨幅Headerview,巨幅HeaderView里面嵌套最多两层ListView,然后底部还是一个分页的列表,不断加载更多……. 这个坑爹货也导致了我另一篇文章的产生: 让HeaderView也参与回收机制,自我感觉是优雅的为 RecyclerView 添加 HeaderView (FooterView)的解决方案http://blog.csdn.net/zxt0601/article/details/52267325


二 竞品分析:

对于以上情况, 由于需要在ScrollView中嵌套ListView ,或者ListView中嵌套ListView….总结就是要嵌套ListView在另外的可以滑动的ViewGroup中,这就有两个问题,
一,ListView和ViewGroup的滑动冲突。
二,ListView并不是全部展开的(View是复用的,ListView最多只有一屏的高度)。
市面上的解决方案,常见三种:
1、手动遍历子View,设置ListView高度(麻烦,且Item的根布局是RelativeLayout的时候无法测量,在android系统版本在17级以下(包含17的时候),RelativeLayout.measure(w,h)时,会出现空指针,只能外层再套一个其他Layout,这是硬伤)
2、通过重写ListView的onMeasure()方法:

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,
        MeasureSpec.AT_MOST);
        super.onMeasure(widthMeasureSpec, expandSpec);
    }

以前项目里常用这个,最容易百度出来的“最优”解,代码量最少,那时年少的我看到它是如获至宝的,因为那篇文章里口口声声告诉我,这样的话View还可以复用,真的很”优雅”。
但我经过实战发现Adapter的getView()会被重复调用多次,如果嵌套两层,getView()倍数调用,太伤性能,它根本不能复用View,不仅不复用,反而变本加厉
故弃用之。下一节中会提供证据,一定让你李菊福。
而且在某些极端情况下,例如每个Item的高度不一样,这个ListView的高度计算偶尔会不准确。
3、使用LinearLayout模拟ListView(写起来麻烦,inflate 的死去活来,但无明显缺点。
一开始我是拒绝这种方案的,太傻啦,自己inflate addView findViewById 多蠢,我有方法2 搭配CommonAdapter ViewHolder等工具类,要他何用。
但是在我知道方法2的真面目后,我只能选用本方法,它至少不会多次调用getView(),重复渲染视图,反正View的复用机制已经被打破,使用ListView不再有任何意义
So本文就是基于此种思路,封装一下固定代码,方便二次快速使用,且尽量的优化,一定程度上提高性能)

本文做了啥:

抽象封装往LinearLayout里inflate,addView的过程,暴漏出绑定数据的方法,并一定程度上考虑性能,缓存View。
在此基础上,利用ViewHolder 思想,尽量避免每次刷新都走findViewById这些耗性能的方法。

为啥要使用ViewHolder,为什么要封装这些缓存?

这种页面往往需要刷新,最无脑的办法就是removeAllViews(),简单粗暴,啥都不考虑,用户体验将会变成,刷新时闪一下,很差,因为View全部要inflate,addView,findViewById一遍。
所以我们在封装的NestFullListView里尽力避免刷新时 View的inflate addView,
在ViewHolder 尽力避免刷新时 findViewById();


三 李菊福: 如方法2重写onMeasure()后的getView()执行多少遍:

本节代码极其简单,没有营养,只为验证,不具有参考价值,故注释张 不再细细讲解阐述,请大家光速阅读。
布局如下:一个简单的ScrollView里面放一个重写onMeasure()方法的ListView,两个按钮用来添加删除数据源,

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

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <com.mcxtzhang.cstnorecyclelistview.other.ListViewForScrollView
                android:id="@+id/lv"
                android:layout_width="match_parent"
                android:layout_height="wrap_content" />
        </LinearLayout>
    </ScrollView>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:onClick="add"
        android:text="add" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:onClick="del"
        android:text="del" />
</RelativeLayout>

ListViewForScrollView.java 代码如下:

public class ListViewForScrollView extends ListView{
    public ListViewForScrollView(Context context) {
        super(context);
    }

    public ListViewForScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,
                MeasureSpec.AT_MOST);
        super.onMeasure(widthMeasureSpec, expandSpec);
    }
}

只是重写了onMeasure()方法 让其全部展开。

测试Activity代码:

/**
 * 本类用于验证重写onMeasure()方法的ListView,性能有多低。
 * getView会被重复调用多次
 */
public class ListViewActivity extends AppCompatActivity {
    private static final String TAG = "zxt/FullListView";
    private List<TestBean> mDatas;
    private ListViewForScrollView listViewForScrollView;
    private LvAdapter mAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_list_view);
        initDatas();
        listViewForScrollView = (ListViewForScrollView) findViewById(R.id.lv);
        listViewForScrollView.setAdapter(mAdapter = new LvAdapter(mDatas, this));
    }

    private void initDatas() {
        int i = 0;
        mDatas = new ArrayList<>();
        ArrayList<NestBean> nestBeen = new ArrayList<>();
        nestBeen.add(new NestBean("http://jiangsu.china.com.cn/uploadfile/2015/0827/1440653790186574.jpg"));
        mDatas.add(new TestBean((i++) + "", "http://inthecheesefactory.com/uploads/source/glidepicasso/cover.jpg", nestBeen));
        nestBeen = new ArrayList<>();
        nestBeen.add(new NestBean("http://imgs.ebrun.com/resources/2016_03/2016_03_24/201603244791458784582125_origin.jpg"));
        nestBeen.add(new NestBean("http://www.wccdaily.com.cn/hxdsb/20151204/6f443028313f1888b7a9fb19549d6ef6.jpg"));
        mDatas.add(new TestBean((i++) + "", "http://fudaoquan.com/wp-content/uploads/2016/04/wanghong.jpg", nestBeen));
        nestBeen = new ArrayList<>();
        nestBeen.add(new NestBean("http://img.mp.itc.cn/upload/20160427/316a154e56684a59b1e81df03a0860c4_th.png"));
        nestBeen.add(new NestBean("http://cdn.duitang.com/uploads/item/201509/17/20150917161810_exXGU.jpeg"));
        mDatas.add(new TestBean((i++) + "", "http://imgs.ebrun.com/resources/2016_03/2016_03_25/201603259771458878793312_origin.jpg", nestBeen));
        mDatas.add(new TestBean((i++) + "", "http://p14.go007.com/2014_11_02_05/a03541088cce31b8_1.jpg"));
        mDatas.add(new TestBean((i++) + "", "http://news.k618.cn/tech/201604/W020160407281077548026.jpg"));
        mDatas.add(new TestBean((i++) + "", "http://www.kejik.com/image/1460343965520.jpg"));
        mDatas.add(new TestBean((i++) + "", "http://cn.chinadaily.com.cn/img/attachement/jpg/site1/20160318/eca86bd77be61855f1b81c.jpg"));
        mDatas.add(new TestBean((i++) + "", "http://imgs.ebrun.com/resources/2016_04/2016_04_12/201604124411460430531500.jpg"));
        mDatas.add(new TestBean((i++) + "", "http://imgs.ebrun.com/resources/2016_04/2016_04_24/201604244971461460826484_origin.jpeg"));
        mDatas.add(new TestBean((i++) + "", "http://www.lnmoto.cn/bbs/data/attachment/forum/201408/12/074018gshshia3is1cw3sg.jpg"));
    }

    public void add(View view) {
        mDatas.add(new TestBean("add", "http://finance.gucheng.com/UploadFiles_7830/201603/2016032110220685.jpg"));
        mAdapter.notifyDataSetChanged();
    }

    public void del(View view) {
        mDatas.remove(mDatas.size() - 1);
        mAdapter.notifyDataSetChanged();
    }
}

基础工作准备就绪,先来看只嵌套一层ListView的getView()方法执行的次数
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="wrap_content"
    android:orientation="vertical">

    <TextView
        android:id="@+id/tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="哈哈" />

    <ImageView
        android:id="@+id/iv"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:scaleType="fitXY"
        android:src="@mipmap/ic_launcher" />

</LinearLayout>

Adapter是这样滴:

/**
 * 介绍:嵌套第一层Adapter
 * 本类用于验证重写onMeasure()方法的ListView,性能有多低。
 * getView会被重复调用多次
 * 作者:zhangxutong
 * 邮箱:zhangxutong@imcoming.com
 * 时间: 2016/9/10.
 */

public class LvAdapter extends BaseAdapter {
    ...
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        Log.d(TAG, "嵌套第1层的 getView() called with: position = [" + position + "], convertView = [" + convertView + "], parent = [" + parent + "]");
        LvViewHolder holder;
        if (null == convertView) {
            convertView = mInflater.inflate(R.layout.item_list_view, parent, false);
            holder = new LvViewHolder();
            holder.tv = (TextView) convertView.findViewById(R.id.tv);
            holder.iv = (ImageView) convertView.findViewById(R.id.iv);
            convertView.setTag(holder);
        } else {
            holder = (LvViewHolder) convertView.getTag();
        }
        TestBean testBean = mDatas.get(position);
        holder.tv.setText(testBean.getName());
        Glide.with(mContext)
                .load(testBean.getUrl())
                .into(holder.iv);

        return convertView;
    }

    private static class LvViewHolder {
        TextView tv;
        ImageView iv;
    }

}
这里写图片描述

UI美如画:

这里写图片描述

那么log里getView()执行了多少次呢?如图:

我们有九个Item,大概执行了9*7 = 63次吧,这个getView()执行次数好像和数据源的数量也有关系,但为什么会循环的走了N遍,我没有深究,我只知道!我被吓坏了。 而且 每当你点击add del 增删数据源,刷新整个ListView的时候,这些getView()又会疯狂的走几十遍,有兴趣的自己下载DEMO验证。。。

胆子小的已经不愿意再继续看这一节了,但是我满足胆子大的,别忘了 我开头放的那张图,评论可是ListViewForScrollView里在嵌套一个ListViewForScrollView,嗯,那么继续:
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="wrap_content"
    android:orientation="vertical">

    <TextView
        android:id="@+id/tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="哈哈" />

    <ImageView
        android:id="@+id/iv"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:scaleType="fitXY"
        android:src="@mipmap/ic_launcher" />

    <com.mcxtzhang.cstnorecyclelistview.other.ListViewForScrollView
        android:id="@+id/lv2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</LinearLayout>

Adapter变成这样:

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        Log.d(TAG, "嵌套第1层的 getView() called with: position = [" + position + "], convertView = [" + convertView + "], parent = [" + parent + "]");
        LvViewHolder holder;
        if (null == convertView) {
            convertView = mInflater.inflate(R.layout.item_list_view, parent, false);
            holder = new LvViewHolder();
            holder.tv = (TextView) convertView.findViewById(R.id.tv);
            holder.iv = (ImageView) convertView.findViewById(R.id.iv);
            holder.lv = (ListViewForScrollView) convertView.findViewById(R.id.lv2);
            convertView.setTag(holder);
        } else {
            holder = (LvViewHolder) convertView.getTag();
        }
        TestBean testBean = mDatas.get(position);
        holder.tv.setText(testBean.getName());
        Glide.with(mContext)
                .load(testBean.getUrl())
                .into(holder.iv);
        holder.lv.setAdapter(new NestAdapter(testBean.getNest(), mContext));

        return convertView;
    }

    private static class LvViewHolder {
        TextView tv;
        ImageView iv;
        ListViewForScrollView lv;
    }

新Adapter这样:

/**
 * 介绍:嵌套第二层Adapter
 * 本类用于验证重写onMeasure()方法的ListView,性能有多低。
 * getView会被重复调用多次
 * 作者:zhangxutong
 * 邮箱:zhangxutong@imcoming.com
 * 时间: 2016/9/10.
 */

public class NestAdapter extends BaseAdapter {
.....
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        Log.d(TAG, "嵌套第二层的 getView() called with: position = [" + position + "], convertView = [" + convertView + "], parent = [" + parent + "]");
        NestViewHolder holder;
        if (null == convertView) {
            convertView = mInflater.inflate(R.layout.item_nest_lv, parent, false);
            holder = new NestViewHolder();
            holder.nestIv = (ImageView) convertView.findViewById(R.id.nestIv);
            convertView.setTag(holder);
        } else {
            holder = (NestViewHolder) convertView.getTag();
        }
        NestBean nestBean = mDatas.get(position);
        Glide.with(mContext)
                .load(nestBean.getUrl())
                .into(holder.nestIv);
        return convertView;
    }

    private static class NestViewHolder {
        ImageView nestIv;
    }

}
这里写图片描述
这里写图片描述

UI美如画二:

getView()次数:

这里写图片描述


当我第一次见到getView()执行这么多次时,我是被吓坏的,有人管管这个ListView吗?他疯了吗?getView()不要钱吗?执行这么多次?
UI美如画,getView()次数是丑成渣啊!
有兴趣下载文末Demo自行验证,我只有一个请求,

别再用了方法二了


四 相对优雅的解决方法:

1 幼年期

那么当我一开始被吓坏的时候,我决定不这么做了,我就老老实实的用LinearLayout然后遍历数据源,往里addView().:
核心代码如下:

LinearLayout container= (LinearLayout)findViewById(R.id.xxxx);
container.removeAllViews();
for (TestBean bean: mDatas) {
LinearLayout item= (LinearLayout) mInflater.inflate(R.layout.xxxx, container, false);
TextView tvName = (TextView) skillContent.findViewById(R.id.tvName);
tvName.setText(skillInfoBean.getSkill_name());
llSkillContent.addView(skillContent);
        }

在布局里添加一个LinearLayout替代ListViewForScrollView,
然后遍历数据源,inflate出这些item,填充数据,
利用LinearLayout.addView(item),将item塞进去。
值得注意的是,每次遍历数据源塞item的时候,要注意container.removeAllViews();,
否则刷新界面的时候,view会重复增加在LinearLayout的尾部

2 成长期

这么做,性能是得到了一定程度的缓解,至少是不会重复执行getView()方法了。
可是这种写法,使用过的朋友肯定知道,其实代码量是比使用ListViewForScrollView多的,而且都是重复的没有意义的代码。尤其在需要嵌套两层ListView效果的时候,代码爆炸。
当项目里写多了这种代码的时候,我就开始厌倦它了。我要更简单的用!
于是它进化了:
进化后使用方法

nestFullListView = (NestFullListView) findViewById(R.id.cstFullShowListView);
        nestFullListView.setAdapter(new FullListViewAdapter<TestBean>(R.layout.item_lv, mDatas) {
            @Override
            void onBind(int pos, TestBean testBean, View v) {
                TextView tv = (TextView) v.findViewById(R.id.tv);
                tv.setText(testBean.getName());
            }
        }); 
//pos是位置,第二个参数是数据,第三个参数是ItemView,
void onBind(int pos, TestBean testBean, View v)

给Adapter传入item的layoutId,以及数据源后,我们只需要关注核心的数据绑定细节,其他完全不管。

增删数据时,如下调用:

    public void add(View view) {
        mDatas.add(new TestBean("add", "http://finance.gucheng.com/UploadFiles_7830/201603/2016032110220685.jpg"));
        nestFullListView.updateUI();
    }

    public void del(View view) {
        mDatas.remove(mDatas.size() - 1);
        nestFullListView.updateUI();
    }

只要调用NestFullListView的updateUI()即可。

NestFullListView如下:

/**
 * 介绍:完全伸展开的ListView(LinearLayout)
 * 作者:zhangxutong
 * 邮箱:zhangxutong@imcoming.com
 * 时间: 2016/9/9.
 */
public class NestFullListView extends LinearLayout {
    private LayoutInflater mInflater;

    public NestFullListView(Context context) {
        this(context, null);
    }

    public NestFullListView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public NestFullListView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    private void init(Context context) {
        mInflater = LayoutInflater.from(context);
        setOrientation(VERTICAL);
    }

    private FullListViewAdapter mAdapter;

    /**
     * 外部调用  同时刷新视图
     *
     * @param mAdapter
     */
    public void setAdapter(FullListViewAdapter mAdapter) {
        this.mAdapter = mAdapter;
        updateUI();
    }

    public void updateUI() {
        removeAllViews();
        if (null != mAdapter) {
            if (null != mAdapter.getDatas() && !mAdapter.getDatas().isEmpty()) {
                for (int i = 0; i < mAdapter.getDatas().size(); i++) {
                    View v = mInflater.inflate(mAdapter.getItemLayoutId(), this, false);
                    mAdapter.onBind(i, v);
                    addView(v);

                }
            }
        }
    }
}

代码也很简单,
1 初始化时设置LinearLayout的布局方向为竖直,
2 对外暴漏一个setAdapter()方法,
3 每次设置完Adapter后,自动调用updateUI() 方法进行视图渲染,
4 updateUI() 时,先removeAllViews(),
5 然后从Adapter里拿到数据源,和itemLayoutId, inflate出这个Item。
6 回调Adapter的onBind()方法。
7 add刚刚inflate的这个View进LinearLayout里。

Adapter如下:

/**
 * 介绍:完全伸展开的ListView的适配器
 * 作者:zhangxutong
 * 邮箱:mcxtzhang@163.com
 * CSDN:http://blog.csdn.net/zxt0601
 * 时间: 16/09/09.
 */

public abstract class FullListViewAdapter<T> {
    private int mItemLayoutId;//看名字
    private List<T> mDatas;//数据源

    public FullListViewAdapter(int mItemLayoutId, List<T> mDatas) {
        this.mItemLayoutId = mItemLayoutId;
        this.mDatas = mDatas;
    }

    /**
     * 被FullListView调用
     *
     * @param i
     * @param v
     */
    public void onBind(int i, View v) {
        //回调bind方法,多传一个data过去
        onBind(i, mDatas.get(i), v);
    }

    /**
     * 数据绑定方法
     *
     * @param pos 位置
     * @param t   数据
     * @param v   ItemView
     */
    abstract void onBind(int pos, T t, View v);

    public int getItemLayoutId() {
        return mItemLayoutId;
    }

    public void setItemLayoutId(int mItemLayoutId) {
        this.mItemLayoutId = mItemLayoutId;
    }

    public List<T> getDatas() {
        return mDatas;
    }

    public void setDatas(List<T> mDatas) {
        this.mDatas = mDatas;
    }

}

Adapter内存储数据源和ItemLayoutId,
暴漏void onBind(int i, View v)供NestFullListView使用,
并且在这个方法里,多传一个数据data,回调abstract void onBind(int pos, T t, View v);
该方法就是我们需要继承实现的方法,在里面完成数据的绑定操作即可。

3 成熟期

经过成长期的封装后,我们使用起来已经很方便了,可是我感觉它还是不太好:
每次updateUI() 时,它总是无脑的removeAllViews();,
如果新的datas的数量并没有变,我们界面上所有的View都是可以复用的,
如果新的datas的数量变化不大,我们可以动态的增删几个View,没必要无脑全部remove掉。
这样可以最大可能减少inflate ,addView的操作,提高性能。
所以我又改写了NestFullListView类:

public class NestFullListView extends LinearLayout {
    private LayoutInflater mInflater;
    private List<View> mViewCahces;//缓存ItemView的List,按照add的顺序缓存,
    //...省略和成长期相同代码
    private void init(Context context) {
        mInflater = LayoutInflater.from(context);
        mViewCahces = new ArrayList<View>();
        setOrientation(VERTICAL);
    }
    //...省略和成长期相同代码
    public void updateUI() {
        if (null != mAdapter) {
            if (null != mAdapter.getDatas() && !mAdapter.getDatas().isEmpty()) {
                //数据源有数据
                if (mAdapter.getDatas().size() > getChildCount()) {//数据源大于现有子View不清空

                } else if (mAdapter.getDatas().size() < getChildCount()) {//数据源小于现有子View,删除后面多的
                    removeViews(mAdapter.getDatas().size(), getChildCount() - mAdapter.getDatas().size());
                    //删除View也清缓存
                    while (mViewCahces.size() > mAdapter.getDatas().size()) {
                        mViewCahces.remove(mViewCahces.size() - 1);
                    }
                }
                for (int i = 0; i < mAdapter.getDatas().size(); i++) {
                    View v;
                    if (mViewCahces.size() - 1 >= i) {//说明有缓存,不用inflate,否则inflate
                        v = mViewCahces.get(i);
                    } else {
                        v = mInflater.inflate(mAdapter.getItemLayoutId(), this, false);
                        mViewCahces.add(v);//inflate 出来后 add进来缓存
                    }
                    mAdapter.onBind(i, v);
                    //如果View没有父控件 添加
                    if (null == v.getParent()) {
                        this.addView(v);
                    }
                }
            } else {
                removeAllViews();//数据源没数据 清空视图
            }
        } else {
            removeAllViews();//适配器为空 清空视图
        }
    }
}

增加一个变量private List<View> mViewCahces;//缓存ItemView的List,按照add的顺序缓存
每次updateUI()时,如果是异常情况:适配器为空 清空视图,数据源没数据 清空视图
那么数据源有数据的情况下,比较数据源的size 和现在子View(ItemView)的size,
如果数据源大于现有子View,说明屏幕上的View不够用,当然不remove子View,也不用清缓存。
如果数据源小于现有子View,删除尾部多的子View,清理多余缓存的ItemView,
遍历数据源,比较i(postion)和viewCaches的size,
如果缓存不够就inflate一个新View,
如果缓存有,就取出缓存的View。
回调Adapter的onBind方法,
判断这个View有没有父控件,
如果View没有父控件 才addView()。

(后话,文章写到这里时,我才发现这里本不需要viewCache,是我当时的时候思路不太对,可以通过LinearLayout.getChildAt()获取LinearLayout里的childView。 不过已不重要,因为成熟期,为了连findViewById()方法也尽可能的减少,引入了ViewHolder,是需要这么一个ViewHolderCache的。)

4 完全体

成熟期里,我们尽可能的避免了View的inflate,addView()操作。可是我们都知道,findViewById()的操作也是很费时的,能否像RecyclerView几兄弟….那样,引入ViewHolder来解决这个问题呢?
(桥黑板!!只能提高刷新时的效率!!)
熟悉洋神的朋友一定看过这篇文章。 Android 快速开发系列 打造万能的ListView GridView 适配器
http://blog.csdn.net/lmj623565791/article/details/38902805/
这里我们引入的NestFullViewHolder 就是这种思想的ViewHolder,

public class NestFullViewHolder {
    private SparseArray<View> mViews;
    private View mConvertView;
    private Context mContext;

    public NestFullViewHolder(Context context, View view) {
        mContext = context;
        this.mViews = new SparseArray<View>();
        mConvertView = view;
    }

    /**
     * 通过viewId获取控件
     *
     * @param viewId
     * @return
     */
    public <T extends View> T getView(int viewId) {
        View view = mViews.get(viewId);
        if (view == null) {
            view = mConvertView.findViewById(viewId);
            mViews.put(viewId, view);
        }
        return (T) view;
    }

    public View getConvertView() {
        return mConvertView;
    }

    public NestFullViewHolder setSelected(int viewId, boolean flag) {
        View v = getView(viewId);
        v.setSelected(flag);
        return this;
    }

    /**
     * 设置TextView的值
     *
     * @param viewId
     * @param text
     * @return
     */
    public NestFullViewHolder setText(int viewId, String text) {
        TextView tv = getView(viewId);
        tv.setText(text);
        return this;
    }
    ............省略大量常用set方法代码

建议看洋神文章详解,不过我这里也会简单讲解一下:
构造方法里NestFullViewHolder(Context context, View view)传入itemView,

利用private SparseArray<View> mViews,以viewId为key,存储ItemView里的各种View。

通过public <T extends View> T getView(int viewId)方法,以viewId为key,获取ItemView里的各种View,
该方法是先从mViews的缓存里寻找View,如果找到了直接返回,
如果没找到就view = mConvertView.findViewById(viewId);执行findViewById,得到这个View,并放入mViews的缓存里,这样下次就不用执行findViewById方法。

并封装一些常用的方法,例如setText、setImageResource等。。。

有了这个NestFullViewHolder,我们如下改写NestFullListView:

public class NestFullListView extends LinearLayout {
    private List<NestFullViewHolder> mVHCahces;//缓存ViewHolder,按照add的顺序缓存,
    //.....无关和成长期重复代码
    private void init(Context context) {
        mVHCahces = new ArrayList<NestFullViewHolder>();
    }
    //.....无关和成长期重复代码
    public void updateUI() {
        if (null != mAdapter) {
            if (null != mAdapter.getDatas() && !mAdapter.getDatas().isEmpty()) {
                //数据源有数据
                if (mAdapter.getDatas().size() > getChildCount()) {//数据源大于现有子View不清空

                } else if (mAdapter.getDatas().size() < getChildCount()) {//数据源小于现有子View,删除后面多的
                    removeViews(mAdapter.getDatas().size(), getChildCount() - mAdapter.getDatas().size());
                    //删除View也清缓存
                    while (mVHCahces.size() > mAdapter.getDatas().size()) {
                        mVHCahces.remove(mVHCahces.size() - 1);
                    }
                }
                for (int i = 0; i < mAdapter.getDatas().size(); i++) {
                    NestFullViewHolder holder;
                    if (mVHCahces.size() - 1 >= i) {//说明有缓存,不用inflate,否则inflate
                        holder = mVHCahces.get(i);
                    } else {
                        holder = new NestFullViewHolder(getContext(), mInflater.inflate(mAdapter.getItemLayoutId(), this, false));
                        mVHCahces.add(holder);//inflate 出来后 add进来缓存
                    }
                    mAdapter.onBind(i, holder);
                    //如果View没有父控件 添加
                    if (null == holder.getConvertView().getParent()) {
                        this.addView(holder.getConvertView());
                    }
                }
            } else {
                removeAllViews();//数据源没数据 清空视图
            }
        } else {
            removeAllViews();//适配器为空 清空视图
        }
    }
}

代码和成长期基本一致,
只是将缓存itemView的mViewCahces,换成了缓存itemView的ViewHolder的mVHCahces。
将以前增删mViewCahces的代码,换成增删mVHCahces的代码,
以前回调Adapter的onBind()方法时,给的是ItemView,现在给的是ItemViewHolder。

mAdapter.onBind(i, holder);

所以我们的Adapter也要对应改写两个onBind方法:

    /**
     * 被FullListView调用
     *
     * @param i
     * @param holder
     */
    public void onBind(int i, NestFullViewHolder holder) {
        //回调bind方法,多传一个data过去
        onBind(i, mDatas.get(i), holder);
    }

    /**
     * 数据绑定方法
     *
     * @param pos    位置
     * @param t      数据
     * @param holder ItemView的ViewHolder
     */
    public abstract void onBind(int pos, T t, NestFullViewHolder holder);

使用:

nestFullListView = (NestFullListView) findViewById(R.id.cstFullShowListView);
nestFullListView.setAdapter(new NestFullListViewAdapter<TestBean>(R.layout.item_lv, mDatas) {
    @Override
    public void onBind(int pos, TestBean testBean, NestFullViewHolder holder) {
        holder.setText(R.id.tv, testBean.getName());
    }
});

对比2成长期的使用,代码更简洁了,setText只要一句话,传入viewId,和value即可。


五 NestFullListView嵌套NestFullListView的onBind()执行次数截图:

嵌套两层时,使用方法:

        nestFullListView = (NestFullListView) findViewById(R.id.cstFullShowListView);

        nestFullListView.setAdapter(new NestFullListViewAdapter<TestBean>(R.layout.item_lv, mDatas) {
            @Override
            public void onBind(int pos, TestBean testBean, NestFullViewHolder holder) {
                Log.d(TAG, "嵌套第一层ScrollView onBind() called with: pos = [" + pos + "], testBean = [" + testBean + "], v = [" + holder + "]");
                holder.setText(R.id.tv, testBean.getName());
                Glide.with(MainActivity.this).load(testBean.getUrl()).into((ImageView) holder.getView(R.id.iv));
                ((NestFullListView) holder.getView(R.id.cstFullShowListView2)).setAdapter(new NestFullListViewAdapter<NestBean>(R.layout.item_nest_lv, testBean.getNest()) {
                    @Override
                    public void onBind(int pos, NestBean nestBean, NestFullViewHolder holder) {
                        Log.d(TAG, "嵌套第二层onBind() called with: pos = [" + pos + "], nestBean = [" + nestBean + "], v = [" + holder + "]");
                        Glide.with(MainActivity.this) .load(nestBean.getUrl()).into((ImageView) holder.getView(R.id.nestIv));
                    }
                });
            }
        });
这里写图片描述

除了使用方法不同,其他代码和第三节 李菊福里完全一致。可下载文末代码观看。
onBind方法次数截图:

两层NestFullListView,数据源加起来一共15个Item,那咱就onBind()15次,一次都不多,比隔壁ListViewForScrollView老实多了。
add,delete时,相当于ListView的notifydatasetchanged。onBind()执行次数依然规规矩矩~可下载项目验证。

六 总结:

其实这种方法,真的称不上优雅,只不过跟别的方法比起来,相对优雅吧。
在我心中最好的方法就是利用RecyclerView的ItemViewType来解决,可惜由于接口,以及数据结构限制,只能退而求其次。如若有朋友有更好的办法,欢迎交流。
再次强调一遍~本文的方法只是尽可能的节省刷新时的性能消耗
不再每次都无脑removeAllViews(),inflate(),addView()。
利用通用的ViewHolder,减少刷新时的findViewById()操作。

不管是ListViewForScrollView 还是本文的NestFullListView,
它们都是在一开始,就把所有的子View统统inflate add bind好了,
不像ListView,RecyclerView..兄弟们,是子View在屏幕上可见时才创建,添加,数据绑定。

github传送门:
https://github.com/mcxtzhang/NestFullListView
复制FullListView包下三个文件(NestFullListView NestFullListViewAdapter NestFullViewHolder)即可畅快使用,
欢迎讨论交流,拍板砖,如有更优方法,真心求指教。


20160923补充:

墙内开花墙外香,意外收获,在最近的项目使用中发现,由于这个控件是使用LinearLayout改造的,所以我们去掉在init()方法里设置的Orientation后,通过xml里传入android:orientation,它就自然的可以支持水平、垂直两种布局了。
在某些情况 需要动态往LinearLayout添加Item 就可以使用本控件简化操作。
所以最新的使用示例如下,详情可查看github,附带一个增加分割线的Demo。
横竖嵌套长这样:

    <com.mcxtzhang.cstnorecyclelistview.FullListView.NestFullListView
        android:id="@+id/cstFullShowListView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:divider="@drawable/divide"
        android:orientation="vertical"
        android:showDividers="middle" />
这里写图片描述

点这里可以跳转到人工智能网站

发表评论