轉載請標明出處: 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()
方法,
但仍然會重走需要修改的兩個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計算也需要時間,它在計算時也會遍歷整個新舊數據集,所以本文不提供這個方案以免誤導。
本文代碼不再單獨開一個工程,可于我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