Android RecyclerView 定制单选多选模式

ListView的小插曲

其实ListView或者说它们这一类都是实现了单选多选模式的,如果你使用CheckedTextview等实现了Checkable的子类的话,根本不用写什么代码,单选多选模式就搞定了。

AdapterView.png
 * Register a callback to be invoked when an item in this AdapterView has
 * been selected.
 *
 * @param listener The callback that will run
 */
public void setOnItemSelectedListener(@Nullable OnItemSelectedListener listener) {
    mOnItemSelectedListener = listener;
}

但是,但是,这个回调在 ListView 或者 GridView 里面似乎并没有什么卵用呢!这是为撒呢?!

AdapterView里面有个handleDataChanged()的方法,这个方法,就是正常执行回调OnItemSelectedListener滴,但是,这个方法被AbsListView给重写了,而且,没有再去回调对应的OnItemSelectedListener了,所以,在ListView和GridView中设置了这个回调也是没有用的。详细的自己去扒一扒源码看看吧,这里不细说了。。
Why is my onItemSelectedListener not called in a ListView?

虽然没有了相关回调,但是我们可以在适当的地方通过调用方法:SparseBooleanArray positions = mListView.getCheckedItemPositions() 来获取我们选中的位置的。

RecyclerView实现单选和多选模式

上面说了 ListView 里面的单选多选模式,接下来就是 RecyclerView 的实现了,首先考虑需要处理哪些问题:

选择标识的添加和相关选择状态保存,item的点击事件(普通模式点击响应,选择模式下刷新为选中或非选中状态),最后就是要弄好回调方法。

这个库还是基于之前的Android RecycleView轻松实现下拉刷新、加载更多,所以呢,这次创建了一个SelectRefreshRecycleAdapter的类用来专门处理单选多选模式,当然,它肯定是继承自RefreshRecycleAdapter<T>滴。在这个类中,根据是选择模式或者是普通模式来进行了不同的点击处理。处理了第一个问题。

选择标识的添加 选择状态保存

对于选择标识的添加,首先这个一般来说应该和各种业务没有关系,所以不应该定义在json数据中,当然啦,如果选择状态你真的已经定义了相关字段并且时刻根据后台数据刷新的话,这里也是可以解决滴。
首先我这里定义了一个接口Iselect,然后,这里接着也定义好了一个实现类了。

public interface ISelect {
    boolean isSelected();
    void setSelected(boolean selected);
}

public class SelectBean implements ISelect, Parcelable {
    private boolean isSelected;

    public boolean isSelected() {
        return isSelected;
    }

    public void setSelected(boolean selected) {
        isSelected = selected;
    }
    ...
}

在使用的时候,如果你的Bean中已经有了相关选中状态的字段了,那么实现Iselect接口,如果没有定义相关字段,那么直接继承SelectBean,其他的就不用去管了。这是关于添加选择标识的解决,然后就是选中状态的保存,这个很简单了,内部维护了一个集合,选中了添加进去,取消选中就移除了。

Item的点击及同步刷新

因为这里有单选模式和多选模式两种情况嘛,接下来一次分析下。

如果是单选模式的话,我们在选中item2之后,不仅要刷新item2的状态,而且还要将之前的选中item的状态更新,意思就是可能要更新两个item。所以这里要定义一个prePos的字段来保存之前的那个item。

如果是多选模式的话,那么就比较简单了,糊糊选就好了(不要纠结为什么用糊糊)。

得益于RecyclerView的后天优势,我们每次只需要调用notifyItemChanged(pos)的方法来刷新指定的item就行了。

@Override
public void performClick(final View itemView, final int position) {
    final T testBean = list.get(position);

    if (isSelectMode) {
        Log.e("TAG", "onViewHolderBind: " + position + "点击了!!");
        //点击后取反当前位置
        boolean selected = !testBean.isSelected();
        testBean.setSelected(selected);
        dispatchSelected(itemView, position, testBean, selected);
        if (currentMode == SingleMode && position != prePos && testBean.isSelected()) {
            //单选模式的prePos处理
            list.get(prePos).setSelected(false);
            dispatchSelected(itemView, prePos, testBean, false);
            notifyItemChanged(prePos);
        }
        notifyItemRangeChanged(position, 1);
        prePos = position;
    } else {
        //不是选择模式就正常回调咯
        if (listener != null) {
            listener.onItemClick(itemView, position);
        }
    }
}

最后就是Viewholder里面的具体实现了:

private static class MyViewHolder extends RecyclerView.ViewHolder {
    private final CheckedTextView mTv;
    public MyViewHolder(View itemView) {
        super(itemView);
        mTv = (CheckedTextView) itemView.findViewById(R.id.text);
    }

    public void bindDateView(TestBean s) {
        mTv.setText(s.isSelected() ? "选中:" + s.getName() : s.getName());
        mTv.setChecked(s.isSelected());
    }
}

相关回调

根据上面 ListView 的那个问题,如果在选择过程中,没有相关回调我们是会抓狂的。参照 ListView 的相关接口,有了以下的定义:

interface OnItemSelectedListener {
   
    void onItemSelected(View view, int position, boolean isSelected);

    
    void onNothingSelected();
}

额,最后加了一个isSelected的参数,感觉就说你选中了撒没有选中的不说是不是有点儿不厚道?!万一你自己要在这里去维护自己的一个数据呢?那不是坑你啦,其实我不想给你说你完全可以通过getSelectedBeans()的方法来直接获取已经选中的集合。

private void dispatchSelected(View itemView, int position, T testBean, boolean isSelected) {
    if (isSelected) {
        selectedBeans.add(testBean);
    } else {
        selectedBeans.remove(testBean);
        if (selectedListener != null && selectedBeans.isEmpty()) {
            selectedListener.onNothingSelected();
        }
    }
    if (selectedListener != null) {
        selectedListener.onItemSelected(itemView, position, isSelected);
    }
}

额外赠送

bug修复

之前写的版本中,加载更多还没有适配GridLayoutManager,这里也做了相关的适配,并且提供了回调,方便你指定对应的某个 positionspanSize

public void setLayoutManager(final RecyclerView.LayoutManager manager) {
    this.manager = manager;
    if (manager instanceof GridLayoutManager) {
        ((GridLayoutManager) manager).setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
            @Override
            public int getSpanSize(int position) {
                switch (adapter.getItemViewType(position)) {
                    case TYPE_BOTTOM:
                        return ((GridLayoutManager) manager).getSpanCount();
                    default:
                        return (spanSizeCallBack != null ? spanSizeCallBack.getSpanSize(position) : 0) == 0 ? 1 : spanSizeCallBack.getSpanSize(position);
                }

            }
        });
    }

    mRecyclerView.setLayoutManager(manager);
}

你只需要这么调用:

    mRecycleView.setLayoutManager(manager);
    mRecycleView.setSpanSizeCallBack(new SwipeRefreshRecycleView.SpanSizeCallBack() {
        @Override
        public int getSpanSize(int position) {
            return 1;
        }
    });

强大的ItemDecoration

因为说到了GridLayoutManager了嘛,那么必须说下这个ItemDecoration,例如我们需要让多个item的留白是等距离的,那么就要使用这个东东了。

前后左右留白.png
前后留白.png

另外它还可以来用作头布局,还可以做出sticky的效果。上面的核心代码大概就是酱紫的:

/**
 *
 * @param space item之间的空间
 * @param count 列数
 * @param showEdge 是否显示左右边缘
 */
public SpacesItemDecoration(int space, int count, boolean showEdge) {
    this.spacing = space;
    this.spanCount = count;
    this.showEdge = showEdge;
    pre = spacing * 1.0f / spanCount;
}

@Override
public void getItemOffsets(Rect outRect, View view,
                           RecyclerView parent, RecyclerView.State state) {

    int position = parent.getChildLayoutPosition(view);
    int column = position % 3;
    if (showEdge) {
        outRect.left = (int) (spacing - column * pre);//left
        outRect.right = (int) ((column + 1) * pre);//right
    } else {
        outRect.left = (int) (column * pre);
        outRect.right = (int) (spacing - (column + 1) * pre);
    }

    if (position < spanCount) { // top
        outRect.top = spacing;
    }
    outRect.bottom = spacing; // bottom
}

到这里,RecyclerView 的单选或者多选模式就搞定了!如果对于这个 Adapter 有相关疑问的可以先去看看上面的一篇哟。

附上最新的相关效果:

PullRefresh3.gif

仓库地址:https://github.com/lovejjfg/PowerRecyclerView

---- Edit By Joe At 2016 11 26 ----

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容