自定义Adapter实现RecyclerView的可展开二级列表expand效果

来源:互联网 发布:unity3d 布料插件 编辑:程序博客网 时间:2024/06/01 18:24

网上实现可展开效果的RecyclerView做法很多,但转发党太多,几乎找不到比较符合效率的做法,其中坑也不少。

想着RecyclerView这么强大,决定自己研究一个,并基于以下四个原则:

1、作为一个有强迫症的人,我只想仅用一个RecyclerView搞定这个效果,不想任何RecyclerView嵌套GridView或者ListView之类的想着就蛋疼的做法,代码也不优美。

2、RecyclerView显示什么,它的数据列表应该也跟显示一致,这样比较好维护。

3、对item或者是item里面的一些控件的点击处理,希望能在Activity中监听处理,Adapter中仅对数据进行显示处理,不涉及复杂的修改数据信息的操作。

4、设想是通过RecyclerView的添加和移除item方式来做展开和收起的效果,这样可以利用RecyclerVIewinsert和remove item的动画效果,还可以自由设定展开的信息类似GridView的形式。大致效果如下图:



正式开始实现这个效果:

一、首先定义数据类CourseInfo、ChapterInfo、SectionInfo,这三个都继承同一个BaseInfo。

public class CourseInfo extends BaseInfo {    public int id;    public String name;    public List<ChapterInfo> chapterInfos = new ArrayList<>();}public class ChapterInfo extends BaseInfo {    public String name;    public int chapterIndex;    public List<SectionInfo> sectionInfos = new ArrayList<>();}public class SectionInfo extends BaseInfo {    public String name;    public int chapterIndex;    public int sectionIndex;}public class BaseInfo implements Serializable {}


二、重要的adapter实现方法如下:

之所以前面的数据类型都继承同一个BaseInfo,就是为了能把不同数据都装进一个list中。

原理主要是传入的数据和显示的数据分开,维护一个显示数据列表,展开就添加item,收起就移除item,这样添加和移除都可以利用RecyclerView自身的动画效果。

当然,如果想更改动画效果貌似还可以自定义自己的ItemAnimator,这个有空可以研究研究。


package com.ldw.testwork.expandrecyclerview;import android.support.v7.widget.RecyclerView;import android.view.LayoutInflater;import android.view.View;import android.view.ViewGroup;import android.widget.GridView;import android.widget.ImageView;import android.widget.LinearLayout;import android.widget.TextView;import com.ldw.testwork.R;import java.util.ArrayList;import java.util.List;import timber.log.Timber;/** * 一个可展开和收起的RecyclerView数据处理,传进的数据和显示的数据分开,展开添加item,收起则删除item。 * Created by ldw on 2017/12/1. */public class ChapterAdapter extends RecyclerView.Adapter implements View.OnClickListener {    public static final int VIEW_TYPE_CHAPTER = 1;    public static final int VIEW_TYPE_SECTION = 2;    //传进来的课程信息    private CourseInfo courseInfo;    //显示的数据集    private List<BaseInfo> dataInfos = new ArrayList<>();    //当前展开的课时,-1代表没有任何展开    private int curExpandChapterIndex = -1;    public ChapterAdapter(CourseInfo _courseInfo) {        this.courseInfo = _courseInfo;        for(BaseInfo info : courseInfo.chapterInfos){            dataInfos.add(info);        }    }    @Override    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {        View itemView;        if(viewType == VIEW_TYPE_CHAPTER){            itemView = LayoutInflater.from(parent.getContext())                    .inflate(R.layout.item_chapter, parent, false);            return new ItemHolder(itemView);        }else{            itemView = LayoutInflater.from(parent.getContext())                    .inflate(R.layout.item_section, parent, false);            return new ItemSectionHolder(itemView);        }    }    @Override    public void onBindViewHolder(RecyclerView.ViewHolder holder, final int position) {        //Timber.v("---onBindViewHolder---position = "+position);        if(getItemViewType(position) == VIEW_TYPE_CHAPTER){            ItemHolder itemHolder = (ItemHolder) holder;            itemHolder.itemView.setTag(position);            itemHolder.tvPractise.setTag(position);            ChapterInfo chapterInfo = (ChapterInfo) dataInfos.get(position);            itemHolder.tvName.setText(chapterInfo.name);            if(chapterInfo.sectionInfos.size() > 0){                itemHolder.ivArrow.setVisibility(View.VISIBLE);                if(curExpandChapterIndex == position){                    itemHolder.ivArrow.setBackgroundResource(R.drawable.arrow_up);                }else{                    itemHolder.ivArrow.setBackgroundResource(R.drawable.arrow_down);                }            }else{                itemHolder.ivArrow.setVisibility(View.INVISIBLE);            }        }else{            ItemSectionHolder itemSectionHolder = (ItemSectionHolder) holder;            itemSectionHolder.tvName.setTag(position);            SectionInfo sectionInfo = (SectionInfo) dataInfos.get(position);            itemSectionHolder.tvName.setText(sectionInfo.name);        }    }    //该方法只更改itemView的部分信息,不全部刷新    @Override    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position, List payloads) {        //Timber.v("---onBindViewHolder---payloads = "+payloads + ", "+position);        if(payloads.isEmpty()){            super.onBindViewHolder(holder, position, payloads);        }else{            String str = (String) payloads.get(0);            //更改view的tag            if(str.equals("change_position")){                if(getItemViewType(position) == VIEW_TYPE_CHAPTER){                    ItemHolder itemHolder = (ItemHolder) holder;                    itemHolder.itemView.setTag(position);                    itemHolder.tvPractise.setTag(position);                    //改变箭头方向                    if(curExpandChapterIndex == position){                        itemHolder.ivArrow.setBackgroundResource(R.drawable.arrow_up);                    }else{                        itemHolder.ivArrow.setBackgroundResource(R.drawable.arrow_down);                    }                }else{                    ItemSectionHolder itemSectionHolder = (ItemSectionHolder) holder;                    itemSectionHolder.tvName.setTag(position);                }            }        }    }    @Override    public long getItemId(int i) {        return i;    }    @Override    public int getItemCount() {        if(dataInfos == null){            return 0;        }else{            return dataInfos.size();        }    }    @Override    public int getItemViewType(int position) {        if(dataInfos.get(position) instanceof ChapterInfo){            return VIEW_TYPE_CHAPTER;        }else if(dataInfos.get(position) instanceof SectionInfo){            return VIEW_TYPE_SECTION;        }        return super.getItemViewType(position);    }    public class ItemHolder extends RecyclerView.ViewHolder {        public LinearLayout llBg;        public ImageView ivArrow;        public TextView tvName;        public TextView tvPractise;        public LinearLayout llSection;        public GridView gvSection;        public ItemHolder(View itemView) {            super(itemView);            ivArrow = (ImageView) itemView.findViewById(R.id.iv_item_chapter_arrow);            tvName = (TextView) itemView.findViewById(R.id.tv_item_chapter_name);            tvPractise = (TextView) itemView.findViewById(R.id.tv_item_chapter_practise);            //将创建的View注册点击事件            itemView.setOnClickListener(ChapterAdapter.this);            tvPractise.setOnClickListener(ChapterAdapter.this);        }    }    public class ItemSectionHolder extends RecyclerView.ViewHolder {        public TextView tvName;        public ItemSectionHolder(View itemView) {            super(itemView);            tvName = (TextView) itemView.findViewById(R.id.tv_item_section_name);            //将创建的View注册点击事件            tvName.setOnClickListener(ChapterAdapter.this);        }    }    ////////////////////////////以下为item点击处理///////////////////////////////    private OnRecyclerViewItemClickListener mOnItemClickListener = null;    public void setOnItemClickListener(OnRecyclerViewItemClickListener listener) {        this.mOnItemClickListener = listener;    }    /** item里面有多个控件可以点击 */    public enum ViewName {        CHAPTER_ITEM,        CHAPTER_ITEM_PRACTISE,        SECTION_ITEM    }    public interface OnRecyclerViewItemClickListener {        void onClick(View view, ViewName viewName, int chapterIndex, int sectionIndex);    }    @Override    public void onClick(View v) {        if (mOnItemClickListener != null) {            //注意这里使用getTag方法获取数据            int position = (int) v.getTag();            ViewName viewName = ViewName.CHAPTER_ITEM;            int chapterIndex = -1;            int sectionIndex = -1;            if(getItemViewType(position) == VIEW_TYPE_CHAPTER){                ChapterInfo chapterInfo = (ChapterInfo) dataInfos.get(position);                chapterIndex = chapterInfo.chapterIndex;                sectionIndex = -1;                if(v.getId() == R.id.tv_item_chapter_practise){                    viewName = ViewName.CHAPTER_ITEM_PRACTISE;                }else{                    viewName = ViewName.CHAPTER_ITEM;                    if(chapterInfo.sectionInfos.size() > 0){                        if(chapterIndex == curExpandChapterIndex){                            narrow(curExpandChapterIndex);                        }else{                            narrow(curExpandChapterIndex);                            expand(chapterIndex);                        }                    }                }            }else if(getItemViewType(position) == VIEW_TYPE_SECTION){                SectionInfo sectionInfo = (SectionInfo) dataInfos.get(position);                viewName = ViewName.SECTION_ITEM;                chapterIndex = sectionInfo.chapterIndex;                sectionIndex = sectionInfo.sectionIndex;            }            mOnItemClickListener.onClick(v, viewName, chapterIndex, sectionIndex);        }    }    /**     * 展开某个item     * @param chapterIndex     */    private void expand(int chapterIndex){        dataInfos.addAll(chapterIndex+1, courseInfo.chapterInfos.get(chapterIndex).sectionInfos);        curExpandChapterIndex = chapterIndex;        Timber.v("---expand---"+(chapterIndex+1)+", "+courseInfo.chapterInfos.get(chapterIndex).sectionInfos.size());        notifyItemRangeInserted(chapterIndex+1, courseInfo.chapterInfos.get(chapterIndex).sectionInfos.size());        /*notifyItemRangeChanged(chapterIndex + 1 + courseInfo.chapterInfos.get(chapterIndex).sectionInfos.size(),                getItemCount() - chapterIndex - 1, "change_position");*/        notifyItemRangeChanged(0, getItemCount(), "change_position");    }    /**     * 收起某个item     * @param chapterIndex     */    private void narrow(int chapterIndex){        if(chapterIndex != -1){            int removeStart = chapterIndex + 1;            int removeCount = 0;            for(int i=removeStart; i<dataInfos.size() && getItemViewType(i) == VIEW_TYPE_SECTION; i++){                removeCount++;            }            dataInfos.removeAll(courseInfo.chapterInfos.get(chapterIndex).sectionInfos);            curExpandChapterIndex = -1;            Timber.v("---narrow---"+removeStart+", "+removeCount);            notifyItemRangeRemoved(removeStart, removeCount);            //notifyItemRangeChanged(removeStart, getItemCount() - removeStart, "change_position");            notifyItemRangeChanged(0, getItemCount(), "change_position");        }    }}


三、布局文件,activity只是一个RecyclerView,这里不贴出来了。

以下是item_chapter.xml文件:

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="50dp"    android:background="#eff7e9"    android:orientation="horizontal">    <ImageView        android:id="@+id/iv_item_chapter_arrow"        android:layout_width="20dp"        android:layout_height="20dp"        android:layout_gravity="center_vertical"        android:background="@drawable/arrow_down"/>    <TextView        android:id="@+id/tv_item_chapter_name"        android:layout_width="0dp"        android:layout_height="match_parent"        android:layout_weight="1"        android:textSize="12sp"        android:paddingStart="5dp"        android:paddingEnd="30dp"        android:maxLines="1"        android:ellipsize="end"        android:gravity="start|center_vertical"        android:text="item_chapter_name"/>    <TextView        android:id="@+id/tv_item_chapter_practise"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:padding="5dp"        android:layout_gravity="center_vertical"        android:gravity="center"        android:text="practise"/></LinearLayout>
以下是item_section.xml文件:

<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="35dp"    android:layout_gravity="center"    android:gravity="center">    <TextView        android:id="@+id/tv_item_section_name"        android:layout_width="80dp"        android:layout_height="35dp"        android:layout_gravity="center"        android:gravity="center"        android:text="section"/></RelativeLayout>


四、Activity中的代码:

package com.ldw.testwork;import android.os.Bundle;import android.support.annotation.Nullable;import android.support.v7.app.AppCompatActivity;import android.support.v7.widget.GridLayoutManager;import android.support.v7.widget.LinearLayoutManager;import android.support.v7.widget.RecyclerView;import android.view.View;import com.ldw.testwork.expandrecyclerview.ChapterAdapter;import com.ldw.testwork.expandrecyclerview.ChapterInfo;import com.ldw.testwork.expandrecyclerview.CourseInfo;import com.ldw.testwork.expandrecyclerview.SectionInfo;import com.ldw.testwork.utils.ToastUtil;import timber.log.Timber;public class ExpandRecyclerViewActivity extends AppCompatActivity {    RecyclerView mRecyclerView;    CourseInfo mCourseInfo;    @Override    protected void onCreate(@Nullable Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_expandrecyclerview);        initData();        initViews();    }    private void initData(){        //假数据        mCourseInfo = new CourseInfo();        mCourseInfo.name = "假装是课程的名称";        for(int i=0; i<31; i++){            ChapterInfo chapterInfo = new ChapterInfo();            chapterInfo.name = "假装是课时名称"+(i+1);            chapterInfo.chapterIndex = i;            if(i==0){                for(int j=0; j<2; j++){                    SectionInfo sectionInfo = new SectionInfo();                    sectionInfo.name = "第"+(j+1)+"节";                    sectionInfo.chapterIndex = i;                    sectionInfo.sectionIndex = j;                    chapterInfo.sectionInfos.add(sectionInfo);                }            }else if(i==1){                for(int j=0; j<3; j++){                    SectionInfo sectionInfo = new SectionInfo();                    sectionInfo.name = "第"+(j+1)+"节";                    sectionInfo.chapterIndex = i;                    sectionInfo.sectionIndex = j;                    chapterInfo.sectionInfos.add(sectionInfo);                }            }else if(i==2){            }else{                for (int j = 0; j < 4; j++) {                    SectionInfo sectionInfo = new SectionInfo();                    sectionInfo.name = "第" + (j + 1) + "节";                    sectionInfo.chapterIndex = i;                    sectionInfo.sectionIndex = j;                    chapterInfo.sectionInfos.add(sectionInfo);                }            }            mCourseInfo.chapterInfos.add(chapterInfo);        }    }    private void initViews(){        mRecyclerView = (RecyclerView) findViewById(R.id.rv_expand);        mRecyclerView.setLayoutManager(new LinearLayoutManager(this));        final ChapterAdapter chapterAdapter = new ChapterAdapter(mCourseInfo);        mRecyclerView.setAdapter(chapterAdapter);        chapterAdapter.setOnItemClickListener(new ChapterAdapter.OnRecyclerViewItemClickListener() {            @Override            public void onClick(View view, ChapterAdapter.ViewName viewName, int chapterIndex, int sectionIndex) {                //Timber.v("---onClick---"+viewName+", "+chapterIndex+", "+sectionIndex);                switch (viewName){                    case CHAPTER_ITEM:                        if(mCourseInfo.chapterInfos.get(chapterIndex).sectionInfos.size() > 0){                            Timber.v("---onClick---just expand or narrow: "+chapterIndex);                            if(chapterIndex + 1 == mCourseInfo.chapterInfos.size()){                                //如果是最后一个,则滚动到展开的最后一个item                                mRecyclerView.smoothScrollToPosition(chapterAdapter.getItemCount());                                Timber.v("---onClick---scroll to bottom");                            }                        }else{                            onClickChapter(chapterIndex);                        }                        break;                    case CHAPTER_ITEM_PRACTISE:                        onClickPractise(chapterIndex);                        break;                    case SECTION_ITEM:                        onClickSection(chapterIndex, sectionIndex);                        break;                }            }        });        //以下是对布局进行控制,让课时占据一行,小节每四个占据一行,结果就是相当于一个ListView嵌套GridView的效果。        final GridLayoutManager manager = new GridLayoutManager(this, 4);        manager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {            @Override            public int getSpanSize(int position) {                return chapterAdapter.getItemViewType(position) == ChapterAdapter.VIEW_TYPE_CHAPTER ? 4 : 1;            }        });        mRecyclerView.setLayoutManager(manager);    }        private void onClickChapter(int chapterIndex){        Timber.v("---onClick---play chapter: "+chapterIndex);        ToastUtil.showToast(ExpandRecyclerViewActivity.this, "播放"+chapterIndex);    }        private void onClickSection(int chapterIndex, int sectionIndex){        Timber.v("---onClick---play---section: "+chapterIndex+", "+sectionIndex);        ToastUtil.showToast(ExpandRecyclerViewActivity.this, "播放"+chapterIndex+", "+sectionIndex);    }        private void onClickPractise(int chapterIndex){        Timber.v("---onClick---practise: "+chapterIndex);    }}

五、代码看似简单,其中adapter有几个坑:

1、插入或者移除后其他的item中的position不会变(此position不仅是指我设置的tag那个position),比如原来有4个item,position为0 1 2 3,中间插入两个item后,position变为0 1 2 3 2 3,非常奇葩,会出现各种问题,当然如果调用notifyDataSetChanged();进行刷新,那不会有什么问题,只是RecyclerView的添加移除item动画效果就没了。网上解决方法千篇一律,重点是还达不到效果。个人的解决方法在2中说明。

2、解决之前,先说明另一个方法public void onBindViewHolder(RecyclerView.ViewHolder holder, int position, List payloads),看到没有,它笔平常使用的多了一个参数payloads,经过了解,它的用途是可以在不用刷新整个item,而对部分item中的某些信息进行修改。我们正好利用这个特点,前面说position不会自动更新,那好,我们就调用这个方法notifyItemRangeChanged(0, getItemCount(), "change_position");让它去更新item,这样既不会重新刷新整个item,又能保证position的正确性。这样就完美解决事情了。


六、总结,RecyclerView有以下几个好东西:

ItemDecoration :设置item间隔

GridLayoutManager :设置显示布局,比如上面例子中,哪个item占据一行,哪个item多个占据一行,可以方便实现GridView效果。

itemAnimator:设置item添加和移除等的各种效果。

public void onBindViewHolder(RecyclerView.ViewHolder holder, int position, List payloads):改方法最后一个参数可以设置不同值,例如notifyItemRangeChanged(0, getItemCount(), "change_position");,从而在更新中能只对item中某个信息进行修改,而不用整个item刷新。


原创文章,转载请注明出处:http://blog.csdn.net/lin_dianwei/article/details/78725014




原创粉丝点击