相关文章推荐
大鼻子的奔马  ·  Android Studio - ...·  6 天前    · 
睿智的甜瓜  ·  报错:curl: (56) Recv ...·  21 小时前    · 
力能扛鼎的饼干  ·  module 'torch' has no ...·  2 月前    · 


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

Android Recycleview 自定义列表 android recyclerview 分组_android

Android Recycleview 自定义列表 android recyclerview 分组_List_02

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

效果如下图:

Android Recycleview 自定义列表 android recyclerview 分组_qq分组-分组效果_03

Android Recycleview 自定义列表 android recyclerview 分组_List_04

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

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

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

Android Recycleview 自定义列表 android recyclerview 分组_qq分组-分组效果_05

与不可折叠的差别:

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,这个主要是存储当前班级是否展开。

运行效果如下:

Android Recycleview 自定义列表 android recyclerview 分组_List_06

基本效果是实现了,如果想要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)

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

Android Recycleview 自定义列表 android recyclerview 分组_android_07

Android Recycleview 自定义列表 android recyclerview 分组_List_08

完整版代码: https://github.com/fan0424/RecyclerViewGroupDemo

教程结束!

番外一:根据点击child获取真实的position

/**
     * 获取真实位置
     * @param section
     * @param position 如果是标题栏,为-1
     * @return
    private int getRealPosition(int headerPosition, int ContentPositionForHeader){
        int realPosition = -1;
        if(headerPosition!= -1){
            realPosition = 0;
            for (int i = 0; i < headerPosition; i++) {
                realPosition++;
                realPosition += getContentCountForHeader(i);
            realPosition++;
            realPosition += ContentPositionForHeader;
        return realPosition;
                            
docker-compose下载很慢 docker下载太慢

Docker默认是国外的源,配置国内镜像仓库。1.进入docker路径 cd /etc/docker/   2.编辑daemon.json文件加入以下内容:{ "registry-mirrors": ["https://registry.docker-cn.com"]