1. 程式人生 > >【Android】ListView、RecyclerView、ScrollView裡巢狀ListView 相對優雅的解決方案:NestFullListView

【Android】ListView、RecyclerView、ScrollView裡巢狀ListView 相對優雅的解決方案:NestFullListView

一 背景概述:

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
 * 郵箱:[email protected]
 * 時間: 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
 * 郵箱:[email protected]
 * 時間: 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
 * 郵箱:[email protected]
 * 時間: 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
 * 郵箱:[email protected]
 * 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" />

這裡寫圖片描述