平時開發的時候我們總會碰到這樣的需求。
image.png
有時是多選,有時是單選,這樣的頁面基本都是用RecyclerView來做的,而如果每次做操作的時候都要去寫這個單選框/多選框的邏輯,那就太麻煩了,是以我就想把這樣的單選/多選清單功能給封裝起來。
一.思路
這種功能的基本布局頁面都是這樣
或者有些需要把選擇框放在右邊
我的思路是外層還是用RecyclerView來做,裡層(就是實線内)我打算用ViewModel來做,這樣既能封裝外層的選擇框邏輯,對内層也低耦合。
二.效果展示
1.單選模式
15063446875651506344674549.gif
2.多選模式
15063446848301506344654342.gif
PS:左邊的布局是個ViewModel可以自由定義,因為時間的問題我沒時間再寫另外的布局來展示。
三.調用方法
調用方法很簡單:
1.定義自己的ViewModel
2.調用
checkRecyclerView = new CheckRecyclerView.Builder<ChoseShopEntity>(this,"com.berchina.o2omerchant.ui.viewModel.shop.ChoseShopViewModel")
.setItemDecoration(new XXXItemDecoration(10))
.setBackgroupColorResID(R.color.divider_grey)
// .setCheckboxResID(R.drawable.cb_shop_create)
.setIsRadio(false)
.builder();
framelayout.addView(checkRecyclerView.getContentView());
Builder中的第二個參數我傳的就是ViewModel的類名。
3.設定資料
checkRecyclerView.setAdapter(datalist);
封裝好了用起來就是快,不會每次都要重複去寫單選和複選的邏輯。
四.CheckRecyclerView内的操作
我這裡把RecyclerView、Adapter和ViewHolder都寫在一個類中,仿照RecyclerView源碼的寫法,當然分開寫也行。
先貼全部代碼吧
public class CheckRecyclerView<T extends CheckRecyclerView.CheckRecyclerEntity> {
protected Context context;
protected RecyclerView recyclerView;
protected static CheckRecyclerAdapter adapter;
// 0 表示都不顯示,1表示顯示左邊,2表示顯示右邊
protected static int isShow;
protected static String vmName;
protected static int checkboxResID;
protected static boolean isRadio;
protected static RadioObserver radioObserver; // 監聽單選
protected static MultiObserver multiObserver; // 監聽多選
protected Builder builder;
// 單選情況下的觀察者
private static Action1<Integer> observer1 = new Action1<Integer>() {
@Override
public void call(Integer integer) {
if (adapter != null){
adapter.checkChange(integer);
}
}
};
// 多選情況下的觀察者
private static Action1<Integer> observer2 = new Action1<Integer>() {
@Override
public void call(Integer integer) {
if (adapter != null){
adapter.multiChose(integer);
}
}
};
private CheckRecyclerView(Context context,Builder builder){
this.context = context;
this.builder = builder;
this.isShow = builder.isShow;
this.vmName = builder.vmName;
this.checkboxResID = builder.checkboxResID;
this.isRadio = builder.isRadio;
this.radioObserver = builder.radioObserver;
this.multiObserver = builder.multiObserver;
initView();
}
private void initView(){
recyclerView = new RecyclerView(context);
recyclerView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
recyclerView.setBackgroundResource(builder.backgroupColorResID);
recyclerView.setLayoutManager(builder.layoutManager);
if (builder.itemDecoration != null) {
recyclerView.addItemDecoration(builder.itemDecoration);
}
}
/**
* 設定擴充卡
*/
public void setAdapter(){
List<T> datalist = builder.datalist;
if (datalist == null){
datalist = new ArrayList<T>();
}
adapter = new CheckRecyclerAdapter(context,datalist);
recyclerView.setAdapter(adapter);
}
/**
* 設定擴充卡
*/
public void setAdapter(List<T> datalist){
adapter = new CheckRecyclerAdapter(context,datalist);
recyclerView.setAdapter(adapter);
}
/**
* get and set 方法
*/
public RecyclerView getContentView() {
return recyclerView;
}
public static CheckRecyclerAdapter getAdapter() {
return adapter;
}
public void setIsShow(int isShow) {
this.isShow = isShow;
}
public static void setVmName(String vmName) {
CheckRecyclerView.vmName = vmName;
}
/**
* 擷取到選擇的結果
* 規則:-1表示沒有選擇
*/
// 單選左
public int getLeftRadio(){
if (adapter != null && adapter.getDatas() != null && adapter.getDatas().size() > 0){
return adapter.getLeftRadio();
}
return -1;
}
// 單選右
public int getRightRadio(){
if (adapter != null && adapter.getDatas() != null && adapter.getDatas().size() > 0){
return adapter.getRightRadio();
}
return -1;
}
// 多選左 傳回被選中的數組
public List<Integer> getLeftMulti(){
List<Integer> multiList = new ArrayList<>();
if (adapter != null && adapter.getDatas() != null && adapter.getDatas().size() > 0){
for (int i = 0; i < adapter.getDatas().size(); i++) {
if (((CheckRecyclerEntity)adapter.getDatas().get(i)).leftCheck == true) {
multiList.add(i);
}
}
}
return multiList;
}
// 多選右 傳回被選中的數組
public List<Integer> getRightMulti(){
List<Integer> multiList = new ArrayList<>();
if (adapter != null && adapter.getDatas() != null && adapter.getDatas().size() > 0){
for (int i = 0; i < adapter.getDatas().size(); i++) {
if (((CheckRecyclerEntity)adapter.getDatas().get(i)).rightCheck == true) {
multiList.add(i);
}
}
}
return multiList;
}
/**
* 判斷在多選的情況下是否全部選擇
*/
public boolean isAllChose(){
Boolean isAllChose = true;
if (adapter != null && adapter.getDatas() != null && adapter.getDatas().size() > 0) {
if (isShow == 1) {
for (int i = 0; i < adapter.getDatas().size(); i++) {
if (((CheckRecyclerEntity)adapter.getDatas().get(i)).leftCheck == false){
return false;
}
}
} else if (isShow == 2) {
for (int i = 0; i < adapter.getDatas().size(); i++) {
if (((CheckRecyclerEntity)adapter.getDatas().get(i)).rightCheck == false){
return false;
}
}
} else {
return false;
}
}else {
isAllChose = false;
}
return isAllChose;
}
/**
* 外部控制内部進行單選的操作
*/
public void setRadio(int position){
if (adapter != null){
adapter.checkChange(position);
}
}
/**
* 設定全選/全不選
* 規則:true表示全選 false表示全不選
*/
public void setAllMulti(Boolean bol){
if (adapter != null){
adapter.setAllMulti(bol);
}
}
/**
* recycelerView 的 adapter
*/
public static class CheckRecyclerAdapter<T extends CheckRecyclerEntity> extends XXXRecyclerViewBaseAdapter<T>{
// 記錄單選的選項(用空間換時間)
private int leftRadio = -1;
private int rightRadio = -1;
public CheckRecyclerAdapter(Context context, List dataSource) {
super(context, dataSource);
}
@Override
protected BerRecyclerViewHolder getViewHolder(ViewGroup parent) {
return new CheckRecyclerViewHolder(LayoutInflater.from(context).inflate(R.layout.item_check_recycler,parent,false),context);
}
/**
* 單選
*/
public void checkChange(int position){
if (position < dataSource.size()) {
// 先将所有都設為未點選
for (int i = 0; i < dataSource.size(); i++) {
dataSource.get(i).leftCheck = false;
dataSource.get(i).rightCheck = false;
}
// 再設定點選的Item
if (isShow == 1) {
dataSource.get(position).leftCheck = true;
leftRadio = position;
}else if (isShow == 2){
dataSource.get(position).rightCheck = true;
rightRadio = position;
}
// 做響應
if (radioObserver != null) {
radioObserver.radioClick(position, isShow);
}
this.notifyDataSetChanged();
}
}
/**
* 多選
*/
public void multiChose(int position){
// 點選後展示相反的情況
if (isShow == 1) {
dataSource.get(position).leftCheck = !dataSource.get(position).leftCheck;
}else if (isShow == 2){
dataSource.get(position).rightCheck = !dataSource.get(position).rightCheck;
}
// 做響應
if (multiObserver != null) {
multiObserver.multiClick(position, isShow);
}
this.notifyDataSetChanged();
}
/**
* 設定全選
*/
public void setAllMulti(Boolean bol){
if (isShow == 1) {
for (int i = 0; i < dataSource.size(); i++) {
dataSource.get(i).leftCheck = bol;
}
this.notifyDataSetChanged();
}else if (isShow == 2){
for (int i = 0; i < dataSource.size(); i++) {
dataSource.get(i).rightCheck = bol;
}
this.notifyDataSetChanged();
}
}
// get and set
public int getLeftRadio() {return leftRadio;}
public int getRightRadio() {return rightRadio;}
}
/**
* recycelerView 的 viewholder
*/
public static class CheckRecyclerViewHolder<T extends CheckRecyclerEntity> extends XXXRecyclerViewHolder<T>{
@InjectView(R.id.cb_left)
CheckBox cbLeft;
@InjectView(R.id.cb_right)
CheckBox cbRight;
@InjectView(R.id.fl_content)
FrameLayout flContent;
private BaseViewModel viewmodel;
public CheckRecyclerViewHolder(View itemView, Context context) {
super(itemView, context);
ButterKnife.inject(this,itemView);
if (isShow == 0){
cbLeft.setVisibility(View.GONE);
cbRight.setVisibility(View.GONE);
}else if (isShow == 1){
cbLeft.setVisibility(View.VISIBLE);
cbRight.setVisibility(View.GONE);
}else if (isShow == 2){
cbLeft.setVisibility(View.GONE);
cbRight.setVisibility(View.VISIBLE);
}
// 設定CheckBox的樣式
if (checkboxResID != 0) {
cbLeft.setButtonDrawable(checkboxResID);
cbRight.setButtonDrawable(checkboxResID);
}
// 應反射建立viewmodel對象
try {
Class<?> myClass = Class.forName(vmName);//完整類名
Class[] paramTypes = { Context.class };
Object[] params = {context};
Constructor con = myClass.getConstructor(paramTypes);
viewmodel = (BaseViewModel) con.newInstance(params);
flContent.addView(viewmodel.getContentView());
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
@Override
public void setDataToView() {
super.setDataToView();
// 處理選擇框的點選 , 隻做資料展示,邏輯放在外邊處理
cbLeft.setChecked(data.leftCheck);
cbRight.setChecked(data.rightCheck);
//為viewmodel添加資料
viewmodel.setData(data);
}
/**
* 接下來做 單選 和 多選 的操作 todo 先測試單選看看能不能用
*/
@OnClick({R.id.cb_left,R.id.cb_right})
public void itemClick(View v){
switch (v.getId()){
case R.id.cb_left:
if (isRadio) {
// 單選
Observable.just(getAdapterPosition()).subscribe(observer1);
}else {
// 多選
Observable.just(getAdapterPosition()).subscribe(observer2);
}
break;
case R.id.cb_right:
if (isRadio) {
Observable.just(getAdapterPosition()).subscribe(observer1);
}else {
Observable.just(getAdapterPosition()).subscribe(observer2);
}
break;
}
}
}
/**
* 定義兩個資料來記錄是否點選選擇框
* 規則:調用封裝的資料類要繼承這個類
*/
public static class CheckRecyclerEntity{
public boolean leftCheck;
public boolean rightCheck;
}
/**
* =======================================================================
* Builder方法
* ========================================================================
*/
public static class Builder<T extends CheckRecyclerView.CheckRecyclerEntity>{
private Context context;
private int isShow = 1; // 預設為顯示左邊的
private String vmName;
private List<T> datalist;
//RecyclerView相關
private RecyclerView.ItemDecoration itemDecoration;
private RecyclerView.LayoutManager layoutManager;
private int backgroupColorResID; //RecyclerView背景顔色,預設為白色
// checkbox相關
private int checkboxResID = 0;
private boolean isRadio = true;// 定義單選(true)和多選(true)
private RadioObserver radioObserver; // 監聽單選
private MultiObserver multiObserver; // 監聽多選
// 這個一定要傳完整 包名+類名
public Builder(Context context,String vmName){
this.context = context;
this.vmName = vmName;
layoutManager = new LinearLayoutManager(context);
backgroupColorResID = R.color.white;
}
public Builder setIsShow(int isShow) {
this.isShow = isShow;
return this;
}
public Builder setDatalist(List<T> datalist) {
this.datalist = datalist;
return this;
}
public Builder setItemDecoration(RecyclerView.ItemDecoration itemDecoration) {
this.itemDecoration = itemDecoration;
return this;
}
public Builder setLayoutManager(RecyclerView.LayoutManager layoutManager) {
this.layoutManager = layoutManager;
return this;
}
public Builder setCheckboxResID(int checkboxResID) {
this.checkboxResID = checkboxResID;
return this;
}
public Builder setBackgroupColorResID(int backgroupColorResID) {
this.backgroupColorResID = backgroupColorResID;
return this;
}
public Builder setIsRadio(boolean radio) {
isRadio = radio;
return this;
}
public Builder setRadioObserver(RadioObserver radioObserver) {
this.radioObserver = radioObserver;
return this;
}
public Builder setMultiObserver(MultiObserver multiObserver) {
this.multiObserver = multiObserver;
return this;
}
public CheckRecyclerView builder(){
return new CheckRecyclerView(context,this);
}
}
/**
* 單選時的響應
*/
public interface RadioObserver{
public void radioClick(int position,int isShow);
}
/**
* 多選時的響應
*/
public interface MultiObserver{
public void multiClick(int position,int isShow);
}
}
代碼也不能說長,500行這樣,其實還好。也隻封裝了一些基本的功能操作,之後還可以擴充。其實我還算挺良心的,加了很多注解,我個人習慣先寫注解再寫方法。
基類的XXXRecyclerViewBaseAdapter和XXXRecyclerViewHolder是我自己寫的Adapter和ViewHolder的基類
1.基本的流程
我有注解也不一行一行解釋,主要是用了Builder模式,在外邊使用時可以addView,用自定義寫XML控件也行。
定義單選和多選的規則:
(1)protected static int isShow; 0 表示都不顯示,1表示顯示左邊,2表示顯示右邊,預設為1
(2)protected static boolean isRadio; 表示單選/多選,true表示單選,false表示多選
定義RecyclerView
private void initView(){
recyclerView = new RecyclerView(context);
recyclerView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
recyclerView.setBackgroundResource(builder.backgroupColorResID);
recyclerView.setLayoutManager(builder.layoutManager);
if (builder.itemDecoration != null) {
recyclerView.addItemDecoration(builder.itemDecoration);
}
}
資料要繼承
public static class CheckRecyclerEntity{
public boolean leftCheck;
public boolean rightCheck;
}
表示左邊是否點選和右邊是否點選
最關鍵是ViewHolder的兩個方法:
(1)initView
public CheckRecyclerViewHolder(View itemView, Context context) {
super(itemView, context);
ButterKnife.inject(this,itemView);
if (isShow == 0){
cbLeft.setVisibility(View.GONE);
cbRight.setVisibility(View.GONE);
}else if (isShow == 1){
cbLeft.setVisibility(View.VISIBLE);
cbRight.setVisibility(View.GONE);
}else if (isShow == 2){
cbLeft.setVisibility(View.GONE);
cbRight.setVisibility(View.VISIBLE);
}
// 設定CheckBox的樣式
if (checkboxResID != 0) {
cbLeft.setButtonDrawable(checkboxResID);
cbRight.setButtonDrawable(checkboxResID);
}
// 應反射建立viewmodel對象
try {
Class<?> myClass = Class.forName(vmName);//完整類名
Class[] paramTypes = { Context.class };
Object[] params = {context};
Constructor con = myClass.getConstructor(paramTypes);
viewmodel = (BaseViewModel) con.newInstance(params);
flContent.addView(viewmodel.getContentView());
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
首先判斷isShow 是單選/多選/隐藏 來展示頁面。然後設定CheckBox的樣式。
最後建立ViewModel對象添加到最上邊圖中實線的區域中。但是我們展示的頁面并不是固定的,也就是說我們會在不同的情況建立不同的viewmodel對象,是以這裡不能直接寫,我用了反射來添加viewmodel,是以在上邊調用的地方傳了viewmodel的包名+類名,是為了用類名建立相應的viewmodel對象添加到區域中。
(2)setDataToView
@Override
public void setDataToView() {
super.setDataToView();
// 處理選擇框的點選 , 隻做資料展示,邏輯放在外邊處理
cbLeft.setChecked(data.leftCheck);
cbRight.setChecked(data.rightCheck);
//為viewmodel添加資料
viewmodel.setData(data);
}
這個就很簡單了,根據leftCheck和rightCheck的值設定左右checkbox的展示,然後給viewmodel設定資料。
2.點選時的處理
做完展示時的處理之後就要做點選時的邏輯處理。
@OnClick({R.id.cb_left,R.id.cb_right})
public void itemClick(View v){
switch (v.getId()){
case R.id.cb_left:
if (isRadio) {
// 單選
Observable.just(getAdapterPosition()).subscribe(observer1);
}else {
// 多選
Observable.just(getAdapterPosition()).subscribe(observer2);
}
break;
case R.id.cb_right:
if (isRadio) {
Observable.just(getAdapterPosition()).subscribe(observer1);
}else {
Observable.just(getAdapterPosition()).subscribe(observer2);
}
break;
}
}
先判斷點左還是點右,再判斷是單選模式還是多選模式,我這裡用了觀察者模式。
// 單選情況下的觀察者
private static Action1<Integer> observer1 = new Action1<Integer>() {
@Override
public void call(Integer integer) {
if (adapter != null){
adapter.checkChange(integer);
}
}
};
// 多選情況下的觀察者
private static Action1<Integer> observer2 = new Action1<Integer>() {
@Override
public void call(Integer integer) {
if (adapter != null){
adapter.multiChose(integer);
}
}
};
點選之後會在adapter中做響應操作。
/**
* 單選
*/
public void checkChange(int position){
if (position < dataSource.size()) {
// 先将所有都設為未點選
for (int i = 0; i < dataSource.size(); i++) {
dataSource.get(i).leftCheck = false;
dataSource.get(i).rightCheck = false;
}
// 再設定點選的Item
if (isShow == 1) {
dataSource.get(position).leftCheck = true;
leftRadio = position;
}else if (isShow == 2){
dataSource.get(position).rightCheck = true;
rightRadio = position;
}
// 做響應
if (radioObserver != null) {
radioObserver.radioClick(position, isShow);
}
this.notifyDataSetChanged();
}
}
/**
* 多選
*/
public void multiChose(int position){
// 點選後展示相反的情況
if (isShow == 1) {
dataSource.get(position).leftCheck = !dataSource.get(position).leftCheck;
}else if (isShow == 2){
dataSource.get(position).rightCheck = !dataSource.get(position).rightCheck;
}
// 做響應
if (multiObserver != null) {
multiObserver.multiClick(position, isShow);
}
this.notifyDataSetChanged();
}
注釋都有,算法也不難,不用詳細去講了。主要封裝的邏輯其實也就這些,而其他的方法基本都是和外邊互動時用到的方法,我都寫了注釋。
比如
/**
* 外部控制内部進行單選的操作
*/
public void setRadio(int position){
if (adapter != null){
adapter.checkChange(position);
}
}
這個就是外面要求裡面單選哪個。比如說有些需求是要預設選第一個,是以可以在外面調用checkRecyclerView.setRadio(0);
/**
* 設定全選/全不選
* 規則:true表示全選 false表示全不選
*/
public void setAllMulti(Boolean bol){
if (adapter != null){
adapter.setAllMulti(bol);
}
}
這是設定全選,比如外面點選哪個按鈕後裡面顯示全選,那就在外面調用checkRecyclerView.setAllMulti(true);
其它的什麼我都加了注釋,就不都說了。
3.Builder的屬性
簡單介紹在Builder中定義的屬性吧
(1)isShow 顯示選擇框在左還是右
(2)datalist需要适配的資料
(3)itemDecoration RecyclerView的分割線
(4)layoutManager RecyclerView的布局
(5)backgroupColorResID RecyclerView的顔色
(6)checkboxResID Checkbox的樣式ID
(7)isRadio 單選還是多選
(8)radioObserver 單選的監聽,要實作這個接口
(9)multiObserver 多選的監聽
(10)vmName viewmodel的包名+類名
注意要傳包名+類名,隻傳類名不行嗎?不行,因為不同的包下可以有相同的類名,我怎麼懂是要哪個。
其實我覺得代碼也不算多,也不難了解,最近沒時間也沒能寫個demo放到github上,之後閑下來再弄吧。然後也沒經過嚴格的測試,可能一些地方寫得不是很好。
更新
後來我用這個東西發現一個錯的地方,就是内部類和成員都不要使用static,不然在多處使用的時候會出錯。當初習慣性的會下意思使用靜态内部類,後來發現用着用着就有問題了。還是太粗心了。
把代碼中的靜态内部類和靜态成員改成非靜态的就行。