您的位置:首页 > 其它

自定义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。

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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: