自定义Adapter实现RecyclerView的可展开二级列表expand效果
2017-12-06 09:12
811 查看
网上实现可展开效果的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。
二、重要的adapter实现方法如下:
之所以前面的数据类型都继承同一个BaseInfo,就是为了能把不同数据都装进一个list中。
原理主要是传入的数据和显示的数据分开,维护一个显示数据列表,展开就添加item,收起就移除item,这样添加和移除都可以利用RecyclerView自身的动画效果。
当然,如果想更改动画效果貌似还可以自定义自己的ItemAnimator,这个有空可以研究研究。
三、布局文件,activity只是一个RecyclerView,这里不贴出来了。
以下是item_chapter.xml文件:
四、Activity中的代码:
五、代码看似简单,其中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
想着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
相关文章推荐
- 使用RecyclerView 简单实现QQ好友列表展开效果
- Android中RecyclerView实现多级折叠列表效果(二)
- Android中RecyclerView实现多级折叠列表效果(TreeRecyclerView)
- Android RecyclerView 二级列表实现
- IOS实现展开二级列表效果
- Android 使用RecyclerView实现列表item重叠效果(使用addItemDecoration方法)
- Android RecyclerView 二级列表实现
- 使用RecyclerView实现列表展开动画
- Android RecyclerView实现数据列表展示效果
- Android -- RecyclerView(超简单)实现可展开列表
- Android中RecyclerView点击item展开列表详细内容(超简单实现)
- Android单个RecyclerView实现列表嵌套的效果
- 使用RecyclerView代替ExpandableListView实现多层级展开列表的方法。
- Android RecyclerView(超简单)实现可展开列表——单项展开
- 使用RecycleView实现动态获取数据展开二级列表展示
- Android -- RecyclerView(超简单)实现可展开列表
- Android RecyclerView (一)初学,实现ListView列表效果。
- Android---RecyclerView之动画(工具类)实现可展开列表
- RecyclerView条目跳转+SpringView数据刷新加载+MVP+OKhttp+拦截器+自定义view 实现请求网络数据的二级列表购物车
- RecyclerView实现列表上下渐变过渡效果