1. 程式人生 > >【Android】 RecyclerView、ListView實現單選列表的優雅之路.

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

一 概述:

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

需求來源:一個類似餓了麼這種電商優惠券的選擇介面
其實就是 一個普通的列表,實現了單選功能,
效果如圖:
這裡寫圖片描述
(不要怪圖渣了,我擼了四五遍,公司錄出來的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計算也需要時間,它在計算時也會遍歷整個新舊資料集,所以本文不提供這個方案以免誤導。