【Android】ListView妥凳、RecyclerView、ScrollView里嵌套ListView 相對優(yōu)雅的解決方案:NestFullListView

轉(zhuǎn)載請標(biāo)明出處: http://www.reibang.com/p/9b6e12d8eea0
本文出自:【張旭童的簡書】 (http://www.reibang.com/users/8e91ff99b072/latest_articles)
代碼傳送門:喜歡的話盗誊,隨手點(diǎn)個star。多謝
https://github.com/mcxtzhang/NestFullListView

一 背景概述:

ScrollView里嵌套ListView隘弊,一直是Android開發(fā)者(反正至少是我們組)最討厭的設(shè)計(jì)之一哈踱,完美打破ListView(RecyclerView)的復(fù)用機(jī)制,成功的將Native頁面變成一個又臭又長的H5網(wǎng)頁效果梨熙,但由于這種設(shè)計(jì)需求在我司項(xiàng)目實(shí)在太多見开镣,無奈之下,我還是決定封裝一下咽扇,畢竟邪财,一個項(xiàng)目里同樣的代碼寫第二遍的程序員都不是好的圣斗士。但是我真的是拒絕的 质欲!拒絕的树埠!拒絕的!真的不喜歡這種界面:

這里寫圖片描述

還拿我前兩天做的這個項(xiàng)目來說吧嘶伟,如上圖怎憋,技能認(rèn)可是一個“ListView”,工作經(jīng)歷是一個“ListView”,每個"ListView"的Item里還會有評論绊袋,評論又是一個“ListView”毕匀,項(xiàng)目經(jīng)歷 教育經(jīng)歷與此類似。愤炸。世界上最恐怖的事期揪,不是ListView套ListView,是ListView套的ListView规个,里面還要繼續(xù)嵌套ListView凤薛。。
(題外話诞仓,這個頁面頭部是個巨幅Headerview缤苫,巨幅HeaderView里面嵌套最多兩層ListView,然后底部還是一個分頁的列表墅拭,不斷加載更多....... 這個坑爹貨也導(dǎo)致了我另一篇文章的產(chǎn)生: 讓HeaderView也參與回收機(jī)制,自我感覺是優(yōu)雅的為 RecyclerView 添加 HeaderView (FooterView)的解決方案http://blog.csdn.net/zxt0601/article/details/52267325


二 競品分析:

對于以上情況活玲, 由于需要在ScrollView中嵌套ListView ,或者ListView中嵌套ListView....總結(jié)就是要嵌套ListView在另外的可以滑動的ViewGroup中谍婉,這就有兩個問題舒憾,
一,ListView和ViewGroup的滑動沖突穗熬。
二镀迂,ListView并不是全部展開的(View是復(fù)用的,ListView最多只有一屏的高度)唤蔗。
市面上的解決方案探遵,常見三種:
1、手動遍歷子View妓柜,設(shè)置ListView高度(麻煩箱季,且Item的根布局是RelativeLayout的時(shí)候無法測量,在android系統(tǒng)版本在17級以下(包含17的時(shí)候)棍掐,RelativeLayout.measure(w,h)時(shí)藏雏,會出現(xiàn)空指針,只能外層再套一個其他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);
    }

以前項(xiàng)目里常用這個,最容易百度出來的“最優(yōu)”解最疆,代碼量最少杯巨,那時(shí)年少的我看到它是如獲至寶的,因?yàn)槟瞧恼吕锟诳诼暵暩嬖V我努酸,這樣的話View還可以復(fù)用服爷,真的很”優(yōu)雅”。
但我經(jīng)過實(shí)戰(zhàn)發(fā)現(xiàn)Adapter的getView()會被重復(fù)調(diào)用多次,如果嵌套兩層,getView()倍數(shù)調(diào)用仍源,太傷性能心褐,它根本不能復(fù)用View,不僅不復(fù)用笼踩,反而變本加厲逗爹。
故棄用之。下一節(jié)中會提供證據(jù),一定讓你李菊福嚎于。
而且在某些極端情況下掘而,例如每個Item的高度不一樣寻仗,這個ListView的高度計(jì)算偶爾會不準(zhǔn)確犹菇。
3、使用LinearLayout模擬ListView(寫起來麻煩艺玲,inflate 的死去活來肋僧,但無明顯缺點(diǎn)斑胜。
一開始我是拒絕這種方案的,太傻啦嫌吠,自己inflate addView findViewById 多蠢止潘,我有方法2 搭配CommonAdapter ViewHolder等工具類,要他何用辫诅。
但是在我知道方法2的真面目后覆山,我只能選用本方法,它至少不會多次調(diào)用getView(),重復(fù)渲染視圖泥栖,反正View的復(fù)用機(jī)制已經(jīng)被打破,使用ListView不再有任何意義勋篓。
So本文就是基于此種思路吧享,封裝一下固定代碼,方便二次快速使用譬嚣,且盡量的優(yōu)化钢颂,一定程度上提高性能)

本文做了啥:

抽象封裝往LinearLayout里inflate,addView的過程拜银,暴漏出綁定數(shù)據(jù)的方法殊鞭,并一定程度上考慮性能,緩存View尼桶。
在此基礎(chǔ)上操灿,利用ViewHolder 思想,盡量避免每次刷新都走findViewById這些耗性能的方法泵督。

為啥要使用ViewHolder趾盐,為什么要封裝這些緩存?

這種頁面往往需要刷新,最無腦的辦法就是removeAllViews(),簡單粗暴久窟,啥都不考慮斥扛,用戶體驗(yàn)將會變成,刷新時(shí)閃一下丹锹,很差卷仑,因?yàn)閂iew全部要inflate峻村,addView,findViewById一遍粘昨。
所以我們在封裝的NestFullListView里盡力避免刷新時(shí) View的inflate addView张肾,
在ViewHolder 盡力避免刷新時(shí) findViewById();


三 李菊福: 如方法2重寫onMeasure()后的getView()執(zhí)行多少遍:

本節(jié)代碼極其簡單吞瞪,沒有營養(yǎng)驾孔,只為驗(yàn)證翠勉,不具有參考價(jià)值对碌,故注釋張 不再細(xì)細(xì)講解闡述朽们,請大家光速閱讀骑脱。
布局如下:一個簡單的ScrollView里面放一個重寫onMeasure()方法的ListView,兩個按鈕用來添加刪除數(shù)據(jù)源惜姐,

<?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代碼:

/**
 * 本類用于驗(yàn)證重寫onMeasure()方法的ListView枫耳,性能有多低迁杨。
 * getView會被重復(fù)調(diào)用多次
 */
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();
    }
}

基礎(chǔ)工作準(zhǔn)備就緒捷沸,先來看只嵌套一層ListView的getView()方法執(zhí)行的次數(shù)
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
 * 本類用于驗(yàn)證重寫onMeasure()方法的ListView,性能有多低苍柏。
 * getView會被重復(fù)調(diào)用多次
 * 作者:zhangxutong
 * 郵箱:zhangxutong@imcoming.com
 * 時(shí)間: 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()執(zhí)行了多少次呢?如圖:

這里寫圖片描述

我們有九個Item熄捍,大概執(zhí)行了9*7 = 63次吧治唤,這個getView()執(zhí)行次數(shù)好像和數(shù)據(jù)源的數(shù)量也有關(guān)系,但為什么會循環(huán)的走了N遍柜裸,我沒有深究疙挺,我只知道铐然!我被嚇壞了搀暑。 而且 每當(dāng)你點(diǎn)擊add del 增刪數(shù)據(jù)源自点,刷新整個ListView的時(shí)候桂敛,這些getView()又會瘋狂的走幾十遍术唬,有興趣的自己下載DEMO驗(yàn)證粗仓。潦牛。巴碗。

膽子小的已經(jīng)不愿意再繼續(xù)看這一節(jié)了橡淆,但是我滿足膽子大的逸爵,別忘了 我開頭放的那張圖师倔,評論可是ListViewForScrollView里在嵌套一個ListViewForScrollView趋艘,嗯瓷胧,那么繼續(xù):
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
 * 本類用于驗(yàn)證重寫onMeasure()方法的ListView杂数,性能有多低揍移。
 * getView會被重復(fù)調(diào)用多次
 * 作者:zhangxutong
 * 郵箱:zhangxutong@imcoming.com
 * 時(shí)間: 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()次數(shù):


這里寫圖片描述

這里寫圖片描述

當(dāng)我第一次見到getView()執(zhí)行這么多次時(shí),我是被嚇壞的读规,有人管管這個ListView嗎束亏?他瘋了嗎碍遍?getView()不要錢嗎怕敬?執(zhí)行這么多次东跪?
UI美如畫虽填,getView()次數(shù)是丑成渣罢铡恶守!
有興趣下載文末Demo自行驗(yàn)證熬的,我只有一個請求,理逊。

別再用了方法二了


四 相對優(yōu)雅的解決方法:

1 幼年期

那么當(dāng)我一開始被嚇壞的時(shí)候晋被,我決定不這么做了羡洛,我就老老實(shí)實(shí)的用LinearLayout然后遍歷數(shù)據(jù)源欲侮,往里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威蕉,
然后遍歷數(shù)據(jù)源韧涨,inflate出這些item虑粥,填充數(shù)據(jù)第晰,
利用LinearLayout.addView(item),將item塞進(jìn)去育勺。
值得注意的是涧至,每次遍歷數(shù)據(jù)源塞item的時(shí)候南蓬,要注意container.removeAllViews();,
否則刷新界面的時(shí)候烧颖,view會重復(fù)增加在LinearLayout的尾部炕淮。

2 成長期

這么做涂圆,性能是得到了一定程度的緩解润歉,至少是不會重復(fù)執(zhí)行g(shù)etView()方法了踩衩。
可是這種寫法驱富,使用過的朋友肯定知道萌朱,其實(shí)代碼量是比使用ListViewForScrollView多的晶疼,而且都是重復(fù)的沒有意義的代碼翠霍。尤其在需要嵌套兩層ListView效果的時(shí)候寒匙,代碼爆炸锄弱。
當(dāng)項(xiàng)目里寫多了這種代碼的時(shí)候会宪,我就開始厭倦它了塞帐。我要更簡單的用葵姥!
于是它進(jìn)化了:
進(jìn)化后使用方法

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是位置榔幸,第二個參數(shù)是數(shù)據(jù)削咆,第三個參數(shù)是ItemView态辛,
void onBind(int pos, TestBean testBean, View v)

給Adapter傳入item的layoutId,以及數(shù)據(jù)源后熟史,我們只需要關(guān)注核心的數(shù)據(jù)綁定細(xì)節(jié)蹂匹,其他完全不管限寞。

增刪數(shù)據(jù)時(shí)履植,如下調(diào)用:

    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();
    }

只要調(diào)用NestFullListView的updateUI()即可。

NestFullListView如下:

/**
 * 介紹:完全伸展開的ListView(LinearLayout)
 * 作者:zhangxutong
 * 郵箱:zhangxutong@imcoming.com
 * 時(shí)間: 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;

    /**
     * 外部調(diào)用  同時(shí)刷新視圖
     *
     * @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 初始化時(shí)設(shè)置LinearLayout的布局方向?yàn)樨Q直鼻种,
2 對外暴漏一個setAdapter()方法普舆,
3 每次設(shè)置完Adapter后沼侣,自動調(diào)用updateUI() 方法進(jìn)行視圖渲染,
4 updateUI() 時(shí)轧膘,先removeAllViews()谎碍,
5 然后從Adapter里拿到數(shù)據(jù)源蟆淀,和itemLayoutId, inflate出這個Item熔任。
6 回調(diào)Adapter的onBind()方法疑苔。
7 add剛剛inflate的這個View進(jìn)LinearLayout里兵迅。

Adapter如下:

/**
 * 介紹:完全伸展開的ListView的適配器
 * 作者:zhangxutong
 * 郵箱:mcxtzhang@163.com
 * CSDN:http://blog.csdn.net/zxt0601
 * 時(shí)間: 16/09/09.
 */

public abstract class FullListViewAdapter<T> {
    private int mItemLayoutId;//看名字
    private List<T> mDatas;//數(shù)據(jù)源

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

    /**
     * 被FullListView調(diào)用
     *
     * @param i
     * @param v
     */
    public void onBind(int i, View v) {
        //回調(diào)bind方法喷兼,多傳一個data過去
        onBind(i, mDatas.get(i), v);
    }

    /**
     * 數(shù)據(jù)綁定方法
     *
     * @param pos 位置
     * @param t   數(shù)據(jù)
     * @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內(nèi)存儲數(shù)據(jù)源和ItemLayoutId,
暴漏void onBind(int i, View v)供NestFullListView使用勉抓,
并且在這個方法里藕筋,多傳一個數(shù)據(jù)data隐圾,回調(diào)abstract void onBind(int pos, T t, View v);
該方法就是我們需要繼承實(shí)現(xiàn)的方法,在里面完成數(shù)據(jù)的綁定操作即可盐碱。

3 成熟期

經(jīng)過成長期的封裝后瓮顽,我們使用起來已經(jīng)很方便了暖混,可是我感覺它還是不太好:
每次updateUI() 時(shí)善绎,它總是無腦的removeAllViews();,
如果新的datas的數(shù)量并沒有變,我們界面上所有的View都是可以復(fù)用的牧嫉,
如果新的datas的數(shù)量變化不大,我們可以動態(tài)的增刪幾個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()) {
                //數(shù)據(jù)源有數(shù)據(jù)
                if (mAdapter.getDatas().size() > getChildCount()) {//數(shù)據(jù)源大于現(xiàn)有子View不清空

                } else if (mAdapter.getDatas().size() < getChildCount()) {//數(shù)據(jù)源小于現(xiàn)有子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進(jìn)來緩存
                    }
                    mAdapter.onBind(i, v);
                    //如果View沒有父控件 添加
                    if (null == v.getParent()) {
                        this.addView(v);
                    }
                }
            } else {
                removeAllViews();//數(shù)據(jù)源沒數(shù)據(jù) 清空視圖
            }
        } else {
            removeAllViews();//適配器為空 清空視圖
        }
    }
}

增加一個變量private List<View> mViewCahces;//緩存ItemView的List,按照add的順序緩存
每次updateUI()時(shí),如果是異常情況:適配器為空 清空視圖遥赚,數(shù)據(jù)源沒數(shù)據(jù) 清空視圖
那么數(shù)據(jù)源有數(shù)據(jù)的情況下,比較數(shù)據(jù)源的size 和現(xiàn)在子View(ItemView)的size愧薛,
如果數(shù)據(jù)源大于現(xiàn)有子View,說明屏幕上的View不夠用衫画,當(dāng)然不remove子View毫炉,也不用清緩存。
如果數(shù)據(jù)源小于現(xiàn)有子View削罩,刪除尾部多的子View瞄勾,清理多余緩存的ItemView,
遍歷數(shù)據(jù)源丰榴,比較i(postion)和viewCaches的size,
如果緩存不夠就inflate一個新View秆撮,
如果緩存有四濒,就取出緩存的View。
回調(diào)Adapter的onBind方法职辨,
判斷這個View有沒有父控件盗蟆,
如果View沒有父控件 才addView()。

(后話舒裤,文章寫到這里時(shí)喳资,我才發(fā)現(xiàn)這里本不需要viewCache,是我當(dāng)時(shí)的時(shí)候思路不太對腾供,可以通過LinearLayout.getChildAt()獲取LinearLayout里的childView仆邓。 不過已不重要鲜滩,因?yàn)槌墒炱冢瑸榱诉BfindViewById()方法也盡可能的減少节值,引入了ViewHolder徙硅,是需要這么一個ViewHolderCache的。)

4 完全體

成熟期里搞疗,我們盡可能的避免了View的inflate嗓蘑,addView()操作∧淠耍可是我們都知道桩皿,findViewById()的操作也是很費(fèi)時(shí)的,能否像RecyclerView幾兄弟....那樣幢炸,引入ViewHolder來解決這個問題呢泄隔?
(橋黑板!宛徊!只能提高刷新時(shí)的效率C酚取!)
熟悉洋神的朋友一定看過這篇文章岩调。 Android 快速開發(fā)系列 打造萬能的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;
    }

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

建議看洋神文章詳解赡盘,不過我這里也會簡單講解一下:
構(gòu)造方法里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);執(zhí)行findViewById皿桑,得到這個View,并放入mViews的緩存里蔬啡,這樣下次就不用執(zhí)行findViewById方法诲侮。

并封裝一些常用的方法,例如setText箱蟆、setImageResource等沟绪。。空猜。

有了這個NestFullViewHolder绽慈,我們?nèi)缦赂膶慛estFullListView:

public class NestFullListView extends LinearLayout {
    private List<NestFullViewHolder> mVHCahces;//緩存ViewHolder,按照add的順序緩存恨旱,
    //.....無關(guān)和成長期重復(fù)代碼
    private void init(Context context) {
        mVHCahces = new ArrayList<NestFullViewHolder>();
    }
    //.....無關(guān)和成長期重復(fù)代碼
    public void updateUI() {
        if (null != mAdapter) {
            if (null != mAdapter.getDatas() && !mAdapter.getDatas().isEmpty()) {
                //數(shù)據(jù)源有數(shù)據(jù)
                if (mAdapter.getDatas().size() > getChildCount()) {//數(shù)據(jù)源大于現(xiàn)有子View不清空

                } else if (mAdapter.getDatas().size() < getChildCount()) {//數(shù)據(jù)源小于現(xiàn)有子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進(jìn)來緩存
                    }
                    mAdapter.onBind(i, holder);
                    //如果View沒有父控件 添加
                    if (null == holder.getConvertView().getParent()) {
                        this.addView(holder.getConvertView());
                    }
                }
            } else {
                removeAllViews();//數(shù)據(jù)源沒數(shù)據(jù) 清空視圖
            }
        } else {
            removeAllViews();//適配器為空 清空視圖
        }
    }
}

代碼和成長期基本一致,
只是將緩存itemView的mViewCahces裙士,換成了緩存itemView的ViewHolder的mVHCahces入客。
將以前增刪mViewCahces的代碼,換成增刪mVHCahces的代碼腿椎,
以前回調(diào)Adapter的onBind()方法時(shí)桌硫,給的是ItemView,現(xiàn)在給的是ItemViewHolder啃炸。

mAdapter.onBind(i, holder);

所以我們的Adapter也要對應(yīng)改寫兩個onBind方法:

    /**
     * 被FullListView調(diào)用
     *
     * @param i
     * @param holder
     */
    public void onBind(int i, NestFullViewHolder holder) {
        //回調(diào)bind方法铆隘,多傳一個data過去
        onBind(i, mDatas.get(i), holder);
    }

    /**
     * 數(shù)據(jù)綁定方法
     *
     * @param pos    位置
     * @param t      數(shù)據(jù)
     * @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()執(zhí)行次數(shù)截圖:

嵌套兩層時(shí)肿嘲,使用方法:

        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));
                    }
                });
            }
        });

除了使用方法不同,其他代碼和第三節(jié) 李菊福里完全一致筑公■撸可下載文末代碼觀看。
onBind方法次數(shù)截圖:


這里寫圖片描述

兩層NestFullListView匣屡,數(shù)據(jù)源加起來一共15個Item封救,那咱就onBind()15次,一次都不多捣作,比隔壁ListViewForScrollView老實(shí)多了誉结。
add,delete時(shí)券躁,相當(dāng)于ListView的notifydatasetchanged惩坑。onBind()執(zhí)行次數(shù)依然規(guī)規(guī)矩矩~可下載項(xiàng)目驗(yàn)證。

六 總結(jié):

其實(shí)這種方法也拜,真的稱不上優(yōu)雅旭贬,只不過跟別的方法比起來,相對優(yōu)雅吧搪泳。
在我心中最好的方法就是利用RecyclerView的ItemViewType來解決稀轨,可惜由于接口,以及數(shù)據(jù)結(jié)構(gòu)限制岸军,只能退而求其次奋刽。如若有朋友有更好的辦法瓦侮,歡迎交流。
再次強(qiáng)調(diào)一遍~本文的方法只是盡可能的節(jié)省刷新時(shí)的性能消耗佣谐,
不再每次都無腦removeAllViews()肚吏,inflate(),addView()狭魂。
利用通用的ViewHolder罚攀,減少刷新時(shí)的findViewById()操作。

不管是ListViewForScrollView 還是本文的NestFullListView雌澄,
它們都是在一開始斋泄,就把所有的子View統(tǒng)統(tǒng)inflate add bind好了,
不像ListView镐牺,RecyclerView..兄弟們炫掐,是子View在屏幕上可見時(shí)才創(chuàng)建,添加睬涧,數(shù)據(jù)綁定募胃。

github傳送門:
https://github.com/mcxtzhang/NestFullListView
復(fù)制FullListView包下三個文件(NestFullListView NestFullListViewAdapter NestFullViewHolder)即可暢快使用,
歡迎討論交流畦浓,拍板磚痹束,如有更優(yōu)方法,真心求指教讶请。


20160923補(bǔ)充:

墻內(nèi)開花墻外香祷嘶,意外收獲,在最近的項(xiàng)目使用中發(fā)現(xiàn)秽梅,由于這個控件是使用LinearLayout改造的,所以我們?nèi)サ粼趇nit()方法里設(shè)置的Orientation后剿牺,通過xml里傳入android:orientation企垦,它就自然的可以支持水平、垂直兩種布局了晒来。
在某些情況 需要動態(tài)往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" />
這里寫圖片描述
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市攒读,隨后出現(xiàn)的幾起案子朵诫,更是在濱河造成了極大的恐慌,老刑警劉巖薄扁,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件剪返,死亡現(xiàn)場離奇詭異废累,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)脱盲,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進(jìn)店門邑滨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人钱反,你說我怎么就攤上這事掖看。” “怎么了面哥?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵哎壳,是天一觀的道長。 經(jīng)常有香客問我幢竹,道長耳峦,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任焕毫,我火速辦了婚禮蹲坷,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘邑飒。我一直安慰自己循签,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布疙咸。 她就那樣靜靜地躺著县匠,像睡著了一般。 火紅的嫁衣襯著肌膚如雪撒轮。 梳的紋絲不亂的頭發(fā)上乞旦,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天,我揣著相機(jī)與錄音题山,去河邊找鬼兰粉。 笑死,一個胖子當(dāng)著我的面吹牛顶瞳,可吹牛的內(nèi)容都是我干的玖姑。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼慨菱,長吁一口氣:“原來是場噩夢啊……” “哼焰络!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起符喝,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤闪彼,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后协饲,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體备蚓,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡课蔬,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了郊尝。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片二跋。...
    茶點(diǎn)故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖流昏,靈堂內(nèi)的尸體忽然破棺而出扎即,到底是詐尸還是另有隱情,我是刑警寧澤况凉,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布谚鄙,位于F島的核電站,受9級特大地震影響刁绒,放射性物質(zhì)發(fā)生泄漏闷营。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一知市、第九天 我趴在偏房一處隱蔽的房頂上張望傻盟。 院中可真熱鬧,春花似錦嫂丙、人聲如沸娘赴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽诽表。三九已至,卻和暖如春隅肥,著一層夾襖步出監(jiān)牢的瞬間竿奏,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工腥放, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留泛啸,地道東北人。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓捉片,卻偏偏與公主長得像平痰,于是被迫代替她去往敵國和親汞舱。 傳聞我的和親對象是個殘疾皇子伍纫,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內(nèi)容