频道栏目
首页 > 资讯 > 其他 > 正文

移动开发中如何自定义Adapter实现RecyclerView的可展开二级列表expand效果

17-12-06        来源:[db:作者]  
收藏   我要投稿

网上实现可展开效果的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 chapterInfos = new ArrayList<>();
}

public class ChapterInfo extends BaseInfo {

    public String name;
    public int chapterIndex;

    public List 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 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

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

以下是item_chapter.xml文件:




    
    
    

以下是item_section.xml文件:



    

四、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刷新。

相关TAG标签
上一篇:比特币价格再创新高 一度逼近12000美元
下一篇:三星手机国内市场份额再降 是国产手机集体发力导致
相关文章
图文推荐

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训 | 举报中心

版权所有: 红黑联盟--致力于做实用的IT技术学习网站