【Android】 RecyclerView、ListView實現單選列表的優雅之路.

轉載請標明出處: http://www.lxweimin.com/p/1ac13f74da63
本文出自:【張旭童的簡書】 (http://www.lxweimin.com/users/8e91ff99b072/latest_articles)
代碼傳送門:喜歡的話,隨手點個star。多謝
https://github.com/mcxtzhang/Demos/tree/master/selectcoupondemo

一 概述:

這篇文章需求來源還是比較簡單的,但做的優雅仍有值得挖掘的地方。

需求來源:一個類似餓了么這種電商優惠券的選擇界面
其實就是 一個普通的列表,實現了單選功能,
效果如圖:

這里寫圖片描述

(不要怪圖渣了,我擼了四五遍,公司錄出來的GIF就這么渣。。。)

常規方法:
在Javabean里增加一個boolean isSelected字段,
并在Adapter里根據這個字段的值設置“CheckBox”的選中狀態。
在每次選中一個新優惠券時,改變數據源里的isSelected字段,
notifyDataSetChanged()刷新整個列表。
這樣實現起來很簡單,代碼量也很少,唯一不足的地方就是性能有損耗,不是最優雅。
So作為一個有追求 今天比較閑 的程序員,我決心分享一波優雅方案。

本文會列舉分析一下在ListView和RecyclerView中, 列表實現單選的幾種方案,并推薦采用定向刷新 部分綁定的方案,因為更高效and優雅。


二 RecyclerView 方案一覽:

RecyclerView是我的最愛 ,所以我先說它。

1常規方案:

常規方案 請光速閱讀,直接上碼:
Bean結構:

public class TestBean extends SelectedBean {
    private String name;
    public TestBean(String name,boolean isSelected) {
        this.name = name;
        setSelected(isSelected);
    }
}

我項目里有好多單選需求,懶得寫isSelected字段,所以弄了個父類供子類繼承。

public class SelectedBean {
    private boolean isSelected;
    public boolean isSelected() {
        return isSelected;
    }
    public void setSelected(boolean selected) {
        isSelected = selected;
    }
}

Acitivity 和Adapter其他方法都是最普通的不再贅述。
Adapter的onBindViewHolder()如下:

Log.d("TAG", "onBindViewHolder() called with: holder = [" + holder + "], position = [" + position + "]");
        holder.ivSelect.setSelected(mDatas.get(position).isSelected());//“CheckBox”
        holder.tvCoupon.setText(mDatas.get(position).getName());//TextView
        holder.ivSelect.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                //實現單選,第一種方法,十分簡單, Lv Rv通用,因為它們都有notifyDataSetChanged()方法
                // 每次點擊時,先將所有的selected設為false,并且將當前點擊的item 設為true, 刷新整個視圖
                for (TestBean data : mDatas) {
                    data.setSelected(false);
                }
                mDatas.get(position).setSelected(true);
                notifyDataSetChanged();


            }
        });

ViewHolder:

    public static class CouponVH extends RecyclerView.ViewHolder {
        private ImageView ivSelect;
        private TextView tvCoupon;

        public CouponVH(View itemView) {
            super(itemView);
            ivSelect = (ImageView) itemView.findViewById(R.id.ivSelect);
            tvCoupon = (TextView) itemView.findViewById(R.id.tvCoupon);
        }
    }

方案優點:

簡單粗暴

方案缺點:

其實需要修改的Item只有兩項
一個當前處于選中狀態的Item->普通狀態
再將當前手指點擊的這個Item->選中狀態
但采用普通方案,則會刷新整個一屏可見的Item,重走他們的getView()/onBindViewHolder()方法。
其實一個屏幕一般最多可見10+個Item,遍歷一遍也無傷大雅。
但咱們還是要有追求優雅的心,所以我們繼續往下看。

2 利用Rv的notifyItemChanged()定向刷新:

本方案可以中速閱讀
⑴本方案需要在Adapter里新增一個字段:

    private int mSelectedPos = -1;//實現單選  方法二,變量保存當前選中的position

⑵在設置數據集時(構造函數,setData()方法等:),初始化 mSelectedPos 的值。

        //實現單選方法二: 設置數據集時,找到默認選中的pos
        for (int i = 0; i < mDatas.size(); i++) {
            if (mDatas.get(i).isSelected()) {
                mSelectedPos = i;
            }
        }

⑶onClick里代碼如下:

                //實現單選方法二: notifyItemChanged() 定向刷新兩個視圖
                //如果勾選的不是已經勾選狀態的Item
                if (mSelectedPos!=position){
                    //先取消上個item的勾選狀態
                    mDatas.get(mSelectedPos).setSelected(false);
                    notifyItemChanged(mSelectedPos);
                    //設置新Item的勾選狀態
                    mSelectedPos = position;
                    mDatas.get(mSelectedPos).setSelected(true);
                    notifyItemChanged(mSelectedPos);
                }

本方案由于調用了notifyItemChanged(),所以還會伴有“白光一閃”的動畫。

方案優點:

本方案,較優雅了,不會重走一屏可見的Item的getView()/onBindViewHolder()方法,
但仍然會重走需要修改的兩個ItemgetView()/onBindViewHolder()方法,

方案缺點:

我們實際上需要修改的,只是里面“CheckBox”的值,
按照在DiffUtil一文學習到的姿勢,術語應該是“Partial bind ",
(安利時間,沒聽過DiffUtil和Partial bind的 戳->:【Android】詳解7.0帶來的新工具類:DiffUtil
我們需要的只是部分綁定。

一個疑點:
使用方法2 在第一次選中其他Item時,切換selected狀態時,
查看log,并不是只重走了新舊Item的onBindViewHolder()方法,還走了兩個根本不在屏幕范圍里的Item的onBindViewHolder()方法,
如,本例中 在還有item 0-3 在屏幕里,默認勾選item1,我選中item0后,log顯示postion 4,5,0,1 依次執行了onBindViewHolder()方法。
但是再次切換其他Item時, 會符合預期:只走需要修改的兩個Item的getView()/onBindViewHolder()方法。
原因未知,有朋友知道煩請告知,多謝。

3 Rv 實現部分綁定(推薦):

利用RecyclerView的 findViewHolderForLayoutPosition()方法,獲取某個postion的ViewHolder,按照源碼里這個方法的注釋,它可能返回null。所以我們需要注意判空,(空即在屏幕不可見)。
與方法2只有onClick里的代碼不一樣,核心還是利用mSelectedPos 字段搞事情。

    //實現單選方法三: RecyclerView另一種定向刷新方法:不會有白光一閃動畫 也不會重復onBindVIewHolder
    CouponVH couponVH = (CouponVH) mRv.findViewHolderForLayoutPosition(mSelectedPos);
    if (couponVH != null) {//還在屏幕里
        couponVH.ivSelect.setSelected(false);
    }else {
        //add by 2016 11 22 for 一些極端情況,holder被緩存在Recycler的cacheView里,
        //此時拿不到ViewHolder,但是也不會回調onBindViewHolder方法。所以add一個異常處理
        notifyItemChanged(mSelectedPos);
    }
    mDatas.get(mSelectedPos).setSelected(false);//不管在不在屏幕里 都需要改變數據
    //設置新Item的勾選狀態
    mSelectedPos = position;
    mDatas.get(mSelectedPos).setSelected(true);
    holder.ivSelect.setSelected(true);

方案優點:

定向刷新兩個Item,只修改必要的部分,不會重走onBindViewHolder(),屬于手動部分綁定。代碼量也適中,不多。

方案缺點:

沒有白光一閃動畫???(如果這算缺點)

4 Rv 利用payloads實現部分綁定(不推薦):

本方案屬于開拓思維,是在方案2的基礎上,利用payloads和notifyItemChanged(int position, Object payload)搞事情。
不知道payloads是什么的,看不懂此方案的,我又要安利:(戳->:【Android】詳解7.0帶來的新工具類:DiffUtil
onClick代碼如下:

                //實現單選方法四:
                if (mSelectedPos != position) {
                    //先取消上個item的勾選狀態
                    mDatas.get(mSelectedPos).setSelected(false);
                    //傳遞一個payload 
                    Bundle payloadOld = new Bundle();
                    payloadOld.putBoolean("KEY_BOOLEAN", false);
                    notifyItemChanged(mSelectedPos, payloadOld);
                    //設置新Item的勾選狀態
                    mSelectedPos = position;
                    mDatas.get(mSelectedPos).setSelected(true);
                    Bundle payloadNew = new Bundle();
                    payloadNew.putBoolean("KEY_BOOLEAN", true);
                    notifyItemChanged(mSelectedPos, payloadNew);
                }

需要重寫三參數的onBindViewHolder() 方法:

    @Override
    public void onBindViewHolder(CouponVH holder, int position, List<Object> payloads) {
        if (payloads.isEmpty()) {
            onBindViewHolder(holder, position);
        } else {
            Bundle payload = (Bundle) payloads.get(0);
            if (payload.containsKey("KEY_BOOLEAN")) {
                boolean aBoolean = payload.getBoolean("KEY_BOOLEAN");
                holder.ivSelect.setSelected(aBoolean);
            }
        }
    }

方案優點:

同方法3

方案缺點:

代碼量多,實現效果和方法三一樣,僅做開拓思維用,所以選擇方法三。


三 ListView 方案一覽:

老實說,現在如果你還在用ListView,不是歷史遺留問題的話,你需要面壁思過。
但是畢竟還有人在用,就像還有人在用Android4.x,咱也要考慮這部分人的感受是不是。

1 常規方案:

常規方案 和Rv一毛一樣,不上碼,參考 二.1:

方案優點:

同 二.1

方案缺點:

同 二.1

2 ListView里尋找優雅之路:

此方案,思路是同二.3。
只不過ListView沒有提供 findViewHolderForLayoutPosition() 這種方法,通過postion獲取緩存的ViewHolder。這是廢話,因為它設計的時候就沒有強迫我們使用ViewHolder模式,所以我們是獲取不到ViewHolder的,那么我們另辟蹊徑,直接通過ViewGroup的getChildAt() 獲取子View,拿到子View就能拿到ViewHolder,就能搞事情。上碼:

                //實現單選:方法二:Lv的定向刷新
                //如果 當前選中的View 在當前屏幕可見,且不是自己,要定向刷新一下之前的View的狀態
                if (position != mSelectedPos) {
                    int firstPos = mLv.getFirstVisiblePosition() - mLv.getHeaderViewsCount();//這里考慮了HeaderView的情況
                    int lastPos = mLv.getLastVisiblePosition() - mLv.getHeaderViewsCount();
                    if (mSelectedPos >= firstPos && mSelectedPos <= lastPos) {
                        View lastSelectedView = mLv.getChildAt(mSelectedPos - firstPos);//取出選中的View
                        CouponVH lastVh = (CouponVH) lastSelectedView.getTag();
                        lastVh.ivSelect.setSelected(false);
                    }
                    //不管在屏幕是否可見,都需要改變之前的data
                    mDatas.get(mSelectedPos).setSelected(false);

                    //改變現在的點擊的這個View的選中狀態
                    couponVH.ivSelect.setSelected(true);
                    mDatas.get(position).setSelected(true);
                    mSelectedPos = position;
                }

方案優點:

也是定向刷新 + 部分綁定 兩個Item,不會重走getView()。

方案缺點:

代碼量貌似略多。


四 總結:

本文寫作之前,也和郭神討論過,確實,如他所說,刷新時getView、onBindViewHolder的次數一般都是個位數(屏幕可見ItemView的數量),所以就算你采用最常規的方法實現,也無傷大雅。據郭神說,他之前寫,參考是gmail的實現方案,之前看過gmail的多選功能就是采用常規方案做的。
so,如果項目時間緊急,采用常規方案也未嘗不可。(我趕工時也會經常用常規方案)

本文的方案,也可以用于列表點贊,下拉篩選器等場景。
比如列表點贊時,重走一遍onBindViewHolder()的話,圖片九宮格控件就要重新set一下數據集,有些九宮格寫的不好,那里面的View都要remove,重新構建渲染一遍。此時用,便是極好的。

其實用RecyclerView+DiffUtil也能實現 定向刷新 部分綁定,可參見我上篇博文,但是有種殺雞牛刀的感覺。
畢竟DiffUtil計算也需要時間,它在計算時也會遍歷整個新舊數據集,所以本文不提供這個方案以免誤導。

本文代碼不再單獨開一個工程,可于我github Demos里?。?/p>

轉載請標明出處: http://www.lxweimin.com/p/1ac13f74da63
本文出自:【張旭童的簡書】 (http://www.lxweimin.com/users/8e91ff99b072/latest_articles)
代碼傳送門:喜歡的話,隨手點個star。多謝
https://github.com/mcxtzhang/Demos/tree/master/selectcoupondemo


最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,837評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,196評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,688評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,456評論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,955評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,195評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,725評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,608評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,802評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,048評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,424評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,762評論 2 372

推薦閱讀更多精彩內容