android RecyclerView一步步打造分组效果、类似QQ分组、折叠菜单、分组效果(二)


第一篇链接:android RecyclerView一步步打造分组效果、类似QQ分组、折叠菜单、分组效果(一)


上一篇写了分组效果的初步实现:

这里写图片描述

这里写图片描述


这一篇就继续增加分组折叠效果和基类的抽取与解决上一篇的bug(item布局宽度match_parent没有生效)

效果如下图:

这里写图片描述 这里写图片描述

三、点击头布局实现展开折叠效果

根据上一片文章最后的代码,继续修改代码让RecyclerView实现点击班级布局可以显示隐藏学生的效果,

首先,先画图分析可折叠的效果有几种情况:

这里写图片描述

与不可折叠的差别:

1.由上图可以看出,班级布局的position的可变的,也就是说,当折叠的时候无展开的时候,其他的班级布局的position会动态改变位置,所以,上一篇中用HashMap存放班级布局的index和position不可用于该效果,所以改为List集合来存放班级的position

2.根据当前选中情况来单独显示隐藏某个班级下的学生

怎么实现点击班级后显示学生,再次点击就隐藏学生呢?这个我们可以在返回班级对应学生人数哪里做文章,当我需要隐藏该班级学生,就返回0,显示的话就返回该班级的所有人数

代码改造如下:

    //存放班级对应的position
    private List<Integer> mHeaderIndex = new ArrayList<>();
    //存放班级对应的学生
    private HashMap<Integer, List<String>> mContentMap = new HashMap<>();
    //存放当前班级的是否展开
    private SparseBooleanArray mBooleanMap;

    /**
     * 条目的总数量
     * @return
     */
    @Override
    public int getItemCount() {
        return getHeadersCount() + getContentCount();
    }

    /**
     * 头布局的数量
     * @return
     */
    private int getHeadersCount(){
        return mContent.size();
    }

    /**
     * item的数量
     * @return
     */
    private int getContentCount(){

        mHeaderIndex.clear();
        mContentMap.clear();
        int itemCount = 0;
        int studentSize = 0;

        for (int i = 0; i < mContent.size(); i++) {

            if(i != 0){
                itemCount++;
            }

            //存储第几班的index位置
//            mHeaderIndex.put(i,new Integer(itemCount));
            mHeaderIndex.add(new Integer(itemCount));

            itemCount += getStudentSizeOfClass(i);
            studentSize += getStudentSizeOfClass(i);

            if(getStudentSizeOfClass(i) > 0){
                mContentMap.put(i, mContent.get(i).classStudents);
            }

        }

        return studentSize;
    }

    /**
     * 根据班级获取对应的学生人数
     * @param classIndex
     * @return
     */
    private int getStudentSizeOfClass(int classIndex){

        int count = mContent.get(classIndex).classStudents.size();

        if (!mBooleanMap.get(classIndex)) {
            count = 0;
        }
        return count;
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {

        //如果是head布局
        if(isHeaderView(position)){

            ((HeaderHolder)holder).tvClassName.setOnClickListener(null);
            ((HeaderHolder)holder).tvClassName.setText(mContent.get(getHeadRealCount(position)).className);

            ((HeaderHolder)holder).tvClassName.setTag(getHeadRealCount(position));
            ((HeaderHolder)holder).tvClassName.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    int position = (int) view.getTag();

                    boolean isOpen = mBooleanMap.get(position);

                    mBooleanMap.put(position, !isOpen);
                    notifyDataSetChanged();
                }
            });
            return ;
        }else {

            //根据position获取position对应的学生所在班级
            int classId = getStudentOfClass(position);
            //获取该班级所有学生
            List<String> classStudent = mContentMap.get(classId);
            //获取改班级head所在的position位置
            int classOfPosition = mHeaderIndex.get(classId);
            //根据当前位置position和班级head布局的position,计算当前学生在班级中的位置
            int studentIndex = position - classOfPosition - 1;
            //根据位置获取具体学生
            String studentName = classStudent.get(studentIndex);
            //显示出来学生名字
            ((ContentHolder) holder).tvInfo.setText(studentName);
        }
    }

    /**
     * 获取position是第几个班级
     * @param position
     * @return
     */
    private int getHeadRealCount(int position){
        return mHeaderIndex.indexOf(new Integer(position));
    }

    /**
     * 根据position获取所属的班级的index
     * @return
     */
    private int getStudentOfClass(int position){

        for (int i = 0; i < mHeaderIndex.size(); i++) {
            if(mHeaderIndex.get(i) > position){
                return i-1;
            }
        }
        return mHeaderIndex.size() - 1;
    }

从代码中可以看出,getHeadersCount()getItemCount()并没有改动,主要就是getContentCount()getHeadRealCount(int position)getStudentOfClass(int position)onBindViewHolder中改动较大,还多了个mBooleanMap,这个主要是存储当前班级是否展开。

运行效果如下:

这里写图片描述

该代码下载:点击下载代码

基本效果是实现了,如果想要listView样式的我们只需要修改布局管理器setLayoutManager就可以很轻松的切换成List样式了,那么,如果换成其他场景还要重复写这么多代码也不想我们的作风,程序员都是非常懒的,所以,我们需要抽出一个基类,在不同的地方使用,只需要继承该基类然后写一些少量的代码就可以实现该效果,这才是我们想要的,那么开始把。

四、抽出基类


首先,需要思考那些需要自己实现的,那些需要基类实现的,大概整理了一下不同需求需要改变的地方:

  1. 创建班级布局
  2. 创建学生布局
  3. 填充班级布局信息
  4. 填充学生布局信息
  5. 班级ViewHolder
  6. 学生ViewHolder
  7. 一共有多少个班级
  8. 每个班级有多少个学生

所以,我们在基类中,把这些方法定义为抽象方法,让子类必须去实现,注意:ViewHolder需要使用泛型!:

    /**
     * 头布局的总数(一共有多少个班级)
     * @return
     */
    public abstract int getHeadersCount();

    /**
     * 头布局对应内容的总数(也就是改头布局里面有多少条item)(根据班级获取该班级有多少个学生)
     * @param headerPosition 第几个头布局
     * @return
     */
    public abstract int getContentCountForHeader(int headerPosition);

    /**
     * 创建头布局(创建班级布局)
     * @param parent
     * @param viewType
     * @return
     */
    public abstract C onCreateHeaderViewHolder(ViewGroup parent, int viewType);

    /**
     * 创建内容布局(创建学生布局)
     * @param parent
     * @param viewType
     * @return
     */
    public abstract S onCreateContentViewHolder(ViewGroup parent, int viewType);

    /**
     * 填充头布局的数据(填充班级布局信息)
     * @param holder
     * @param position
     */
    public abstract void onBindHeaderViewHolder(C holder, int position);

    /**
     * 填充(填充学生布局信息)
     * @param holder
     * @param HeaderPosition
     * @param ContentPositionForHeader
     */
    public abstract void onBindContentViewHolder(S holder, int HeaderPosition, int ContentPositionForHeader);

下面把基类的adapter中对应的代码改一下:

    /**
     * 条目的总数量
     * @return
     */
    @Override
    public int getItemCount() {
        mHeaderIndex.clear();
        int count = 0;
        int headSize = getHeadersCount();
        for (int i = 0; i < headSize; i++) {
            if(i != 0){
                count++;
            }
            mHeaderIndex.add(new Integer(count));

            count += getContentCountForHeader(i);
        }
        Log.e("fan", "--getItemCount:" + count + "--headSize" + headSize);
        return count + 1;
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if(viewType == TYPE_HEADER){
            //班级header布局
            return onCreateHeaderViewHolder(parent, viewType);
        }else {
            //学生布局
            return onCreateContentViewHolder(parent, viewType);
        }
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {

        //如果是head布局
        if(isHeaderView(position)){     //班级header布局填充

            onBindHeaderViewHolder((C)holder, getHeadRealCount(position));
            return ;
        }else {      //学生信息填充
            //根据position获取position对应的学生所在班级
            int classId = getStudentOfClass(position);
            //获取改班级head所在的position位置
            int classOfPosition = mHeaderIndex.get(classId);
            //根据当前位置position和班级head布局的position,计算当前学生在班级中的位置
            int studentIndex = position - classOfPosition - 1;
            onBindContentViewHolder((S)holder, classId, studentIndex);
        }
    }

基类adapter完整的代码如下(如果不懂泛型的可以注意下类名后面跟着的代码):

这里写代码片

public abstract class ClassAdapter<C extends RecyclerView.ViewHolder, S extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    private static final int TYPE_HEADER = 1;
    private static final int TYPE_CONTENT = 0;

    private List<Integer> mHeaderIndex = new ArrayList<>();
    /**
     * 是否为头布局
     * @param position
     * @return
     */
    private boolean isHeaderView(int position){
        return mHeaderIndex.contains(new Integer(position));
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if(viewType == TYPE_HEADER){
            //班级header布局
            return onCreateHeaderViewHolder(parent, viewType);
        }else {
            //学生布局
            return onCreateContentViewHolder(parent, viewType);
        }
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {

        //如果是head布局
        if(isHeaderView(position)){     //班级header布局填充

            onBindHeaderViewHolder((C)holder, getHeadRealCount(position));
            return ;
        }else {      //学生信息填充
            //根据position获取position对应的学生所在班级
            int classId = getStudentOfClass(position);
            //获取改班级head所在的position位置
            int classOfPosition = mHeaderIndex.get(classId);
            //根据当前位置position和班级head布局的position,计算当前学生在班级中的位置
            int studentIndex = position - classOfPosition - 1;
            onBindContentViewHolder((S)holder, classId, studentIndex);
        }
    }

    @Override
    public int getItemViewType(int position) {

        if(isHeaderView(position)){
            return TYPE_HEADER;
        }else{
            return TYPE_CONTENT;
        }
    }

    /**
     * 条目的总数量
     * @return
     */
    @Override
    public int getItemCount() {
        mHeaderIndex.clear();
        int count = 0;
        int headSize = getHeadersCount();
        for (int i = 0; i < headSize; i++) {
            if(i != 0){
                count++;
            }
            mHeaderIndex.add(new Integer(count));

            count += getContentCountForHeader(i);
        }
        return count + 1;
    }

    /**
     * 获取position是第几个头布局
     * @param position
     * @return
     */
    private int getHeadRealCount(int position){
        return mHeaderIndex.indexOf(new Integer(position));
    }

    /**
     * 根据value获取所属的key
     * @return
     */
    private int getStudentOfClass(int position){

        for (int i = 0; i < mHeaderIndex.size(); i++) {
            if(mHeaderIndex.get(i) > position){
                return i-1;
            }
        }
        return mHeaderIndex.size() - 1;
    }

    @Override
    public void onAttachedToRecyclerView(RecyclerView recyclerView) {

        final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
        if(layoutManager instanceof GridLayoutManager){
            GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager;
            GridLayoutManager.SpanSizeLookup spanSizeLookup = gridLayoutManager.getSpanSizeLookup();
            gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup(){

                @Override
                public int getSpanSize(int position) {

                    int viewType = getItemViewType(position);
                    if(viewType == TYPE_HEADER){
                        return ((GridLayoutManager) layoutManager).getSpanCount();
                    }
                    return 1;
                }
            });
        }
    }

    @Override
    public void onViewAttachedToWindow(RecyclerView.ViewHolder holder){
        int position = holder.getLayoutPosition();
        if (isHeaderView(position))
        {
            ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();

            if (lp != null && lp instanceof StaggeredGridLayoutManager.LayoutParams)
            {

                StaggeredGridLayoutManager.LayoutParams p =
                        (StaggeredGridLayoutManager.LayoutParams) lp;

                p.setFullSpan(true);
            }
        }
    }

    /**
     * 头布局的总数
     * @return
     */
    public abstract int getHeadersCount();

    /**
     * 头布局对应内容的总数(也就是改头布局里面有多少条item)
     * @param headerPosition 第几个头布局
     * @return
     */
    public abstract int getContentCountForHeader(int headerPosition);

    /**
     * 创建头布局
     * @param parent
     * @param viewType
     * @return
     */
    public abstract C onCreateHeaderViewHolder(ViewGroup parent, int viewType);

    /**
     * 创建内容布局
     * @param parent
     * @param viewType
     * @return
     */
    public abstract S onCreateContentViewHolder(ViewGroup parent, int viewType);

    /**
     * 填充头布局的数据
     * @param holder
     * @param position
     */
    public abstract void onBindHeaderViewHolder(C holder, int position);

    /**
     * 填充
     * @param holder
     * @param HeaderPosition
     * @param ContentPositionForHeader
     */
    public abstract void onBindContentViewHolder(S holder, int HeaderPosition, int ContentPositionForHeader);

}

而我们平常使用的话就写个子类去继承该adapter,例如:

MyAdapter.class

public class MyAdapter extends ClassAdapter<MyAdapter.ClassHolder, MyAdapter.StudentHolder> {

    private Context context;
    private List<ClassBean> mContent;

    //用于记录当前班级是隐藏还是显示
    private SparseBooleanArray mBooleanMap;

    public MyAdapter(Context context, List mContent) {
        this.context = context;
        this.mContent = mContent;

        mBooleanMap = new SparseBooleanArray();
    }

    @Override
    public int getHeadersCount() {
        return mContent.size();
    }

    @Override
    public int getContentCountForHeader(int headerPosition) {

        int count = mContent.get(headerPosition).classStudents.size();

        if (!mBooleanMap.get(headerPosition)) {
            count = 0;
        }
        return count;
    }

    /**
     * 创建头布局header的viewholder
     * @param parent
     * @param viewType
     * @return
     */
    @Override
    public MyAdapter.ClassHolder onCreateHeaderViewHolder(ViewGroup parent, int viewType) {
        return new ClassHolder(View.inflate(context, R.layout.item, null));
    }

    /**
     * 创建内容布局item的viewholder
     * @param parent
     * @param viewType
     * @return
     */
    @Override
    public MyAdapter.StudentHolder onCreateContentViewHolder(ViewGroup parent, int viewType) {
        return new StudentHolder(View.inflate(context, R.layout.item, null));
    }

    @Override
    public void onBindHeaderViewHolder(MyAdapter.ClassHolder holder, int position) {
        holder.tvClassName.setOnClickListener(null);
        holder.tvClassName.setText(mContent.get(position).className);

        holder.tvClassName.setTag(position);
        holder.tvClassName.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                int position = (int) view.getTag();

                boolean isOpen = mBooleanMap.get(position);

                mBooleanMap.put(position, !isOpen);
                notifyDataSetChanged();
            }
        });
    }

    @Override
    public void onBindContentViewHolder(StudentHolder holder, int HeaderPosition, int ContentPositionForHeader) {
        holder.tvInfo.setText(mContent.get(HeaderPosition).classStudents.get(ContentPositionForHeader));
    }



    class ClassHolder extends RecyclerView.ViewHolder{

        public TextView tvClassName;

        public ClassHolder(View itemView) {
            super(itemView);
            tvClassName = itemView.findViewById(R.id.tvInfo);
        }
    }

    class StudentHolder extends RecyclerView.ViewHolder{

        public TextView tvInfo;

        public StudentHolder(View itemView) {
            super(itemView);
            tvInfo = itemView.findViewById(R.id.tvInfo);
        }
    }

}

这里需要注意一点的是,我们把显示隐藏的放到子类的getContentCountForHeader(int headerPosition)里面了,所以,如果我们不想要隐藏,只要一直显示内容的话就把里面的map给注释了,如下:

    @Override
    public int getContentCountForHeader(int headerPosition) {

        int count = mContent.get(headerPosition).classStudents.size();

        //这里是控制显示隐藏内容的部分
        //if (!mBooleanMap.get(headerPosition)) {
        //    count = 0;
        //}
        return count;
    }

对了,还有Activity中使用的话就直接创建我们写的子类就可以了,比如:

        for (int i = 1; i < 4; i++) {

            List<String> studentName = new ArrayList<>();
            for (int j = 1; j < 56; j++) {
                studentName.add(i + "班 学生" + j);
            }
            ClassBean bean = new ClassBean();
            bean.className = "二年级" + i + "班";
            bean.classStudents = studentName;
            mListClass.add(bean);
        }

        //可以修改布局管理器来显示网格布局还是线性布局
        rvShow.setLayoutManager(new GridLayoutManager(this, 4));
        //这个就用我们的子类adapter
        MyAdapter mWrapper = new MyAdapter(this, mListClass);
        rvShow.setAdapter(mWrapper);

ok,搞定,对了,还有最后一步,就是修改之前的match_parent不生效问题

五、修复布局match_parent不生效问题

这个百度一下估计多的是,这里就简要说一下解决方法:

在创建ViewHolder的时候,我们之前使用的是用的以前Listview的adapter的写法:

View.inflate(context, R.layout.item, null)

需要修改为:

LayoutInflater.from(context).inflate(R.layout.item, parent, false)

就可以了,最终效果图就是:


这里写图片描述 这里写图片描述

完整版代码:点击下载

教程结束!

猜你喜欢

转载自blog.csdn.net/fan7983377/article/details/77970063
今日推荐