天天看點

Android封裝單選與多選

平時開發的時候我們總會碰到這樣的需求。

Android封裝單選與多選

image.png

有時是多選,有時是單選,這樣的頁面基本都是用RecyclerView來做的,而如果每次做操作的時候都要去寫這個單選框/多選框的邏輯,那就太麻煩了,是以我就想把這樣的單選/多選清單功能給封裝起來。

一.思路

這種功能的基本布局頁面都是這樣

Android封裝單選與多選

或者有些需要把選擇框放在右邊

Android封裝單選與多選

我的思路是外層還是用RecyclerView來做,裡層(就是實線内)我打算用ViewModel來做,這樣既能封裝外層的選擇框邏輯,對内層也低耦合。

二.效果展示

1.單選模式
Android封裝單選與多選

15063446875651506344674549.gif

2.多選模式
Android封裝單選與多選

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,不然在多處使用的時候會出錯。當初習慣性的會下意思使用靜态内部類,後來發現用着用着就有問題了。還是太粗心了。

把代碼中的靜态内部類和靜态成員改成非靜态的就行。

繼續閱讀