【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()
方法,
但仍然會重走需要修改的兩個Item的getView()/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計算也需要時間,它在計算時也會遍歷整個新舊資料集,所以本文不提供這個方案以免誤導。