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 ----

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 138,009评论 20 590
  • 内容抽屉菜单ListViewWebViewSwitchButton按钮点赞按钮进度条TabLayout图标下拉刷新...
    皇小弟阅读 34,632评论 21 600
  • 这一天,我们踏上了征途…… 行 上午8点:五峰国酒整装出发,经过差不多4个小时的车程,我们到达了这座位于五峰县采花...
    郑武林阅读 194评论 7 4
  • 去年的今天,考完最后一门英语。没有像以前想过无数遍的,或狂欢,或大笑,或依依不舍的告别。告别往往是无声的,在告别时...
    饺子鸡萌萌哒_Shinaya阅读 60评论 1 2
  • 抬头望天,心里像是遗失了什么, 空落落的,抓不到,却想极力守住一丝光亮。企盼着,企盼着东方的天空重新向大地投射...
    活与谁看阅读 32评论 0 1