平时开发的时候我们总会碰到这样的需求。





android 单选按钮 默认选中_ui


image.png


有时是多选,有时是单选,这样的页面基本都是用RecyclerView来做的,而如果每次做操作的时候都要去写这个单选框/多选框的逻辑,那就太麻烦了,所以我就想把这样的单选/多选列表功能给封装起来

一.思路

这种功能的基本布局页面都是这样


android 单选按钮 默认选中_单选_02


image.png


或者有些需要把选择框放在右边


android 单选按钮 默认选中_ui_03


image.png


我的思路是外层还是用RecyclerView来做,里层(就是实线内)我打算用ViewModel来做,这样既能封装外层的选择框逻辑,对内层也低耦合。

二.效果展示

1.单选模式


android 单选按钮 默认选中_android 单选按钮 默认选中_04


15063446875651506344674549.gif


2.多选模式


android 单选按钮 默认选中_ui_05


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,不然在多处使用的时候会出错。当初习惯性的会下意思使用静态内部类,后来发现用着用着就有问题了。还是太粗心了。
把代码中的静态内部类和静态成员改成非静态的就行。