zoukankan      html  css  js  c++  java
  • 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()方法, 
    但仍然会重走需要修改的两个ItemgetView()/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里取: 
    https://github.com/mcxtzhang/Demos/tree/master/selectcoupondemo

  • 相关阅读:
    Java学习笔记
    Winform中ListView设置了ColumnHeader不显示问题
    GitHub私有仓库为他人授权
    (转)一步一步教你如何在GitHub上上传自己的项目
    【转】C#中的Explicit和Implicit
    C#对象深表复制方法
    mongodb 设置 ssl
    zabbix 4.0.1部署
    redis编译安装
    VMware网络连接模式——桥接模式、NAT模式以及仅主机模式的介绍和区别
  • 原文地址:https://www.cnblogs.com/zhujiabin/p/7569224.html
Copyright © 2011-2022 走看看