Android RecyclerView详解

介绍

RecyclerView用于在有限的窗口展现大量的数据,其实早已经有了类似的控件,如ListView、GridView,那么相比它们,RecyclerView有什么样优势呢?
RecyclerView标准化了ViewHolder,而且异常的灵活,可以轻松实现ListView实现不了的样式和功能,通过布局管理器LayoutManager可控制Item的布局方式,通过设置Item操作动画自定义Item添加和删除的动画,通过设置Item之间的间隔样式,自定义间隔。

可实现效果

  • 设置布局管理器以控制Item的布局方式,横向、竖向以及瀑布流方式。

  • 可设置Item操作的动画(删除或者添加等)

  • 可设置Item的间隔样式(可绘制)

关于Item的点击和长按事件,需要用户自己去实现

使用

  • 使用RecyclerView时候,必须指定一个适配器Adapter和一个布局管理器LayoutManager。
  • 适配器继承RecyclerView.Adapter类,具体实现类似ListView的适配器,取决于数据信息以及展示的UI。
  • 布局管理器用于确定RecyclerView中Item的展示方式以及决定何时复用已经不可见的Item,避免重复创建以及执行高成本的findViewById()方法

用法

示例

mRecyclerView = (RecyclerView) findViewById(R.id.my_recycler_view);
LinearLayoutManager mLayoutManager=new LinearLayoutManager(this);
// 设置布局管理器
mRecyclerView.setLayoutManager(mLayoutManager);
// 设置adapter
mRecyclerView.setAdapter(mAdapter);
// 设置Item添加和移除的动画
mRecyclerView.setItemAnimator(new DefaultItemAnimator());
// 设置Item之间间隔样式
mRecyclerView.addItemDecoration(mDividerItemDecoration);

基本使用

首先需要在在 build.gradle 文件中引入 RecyclerView 类

  compile 'com.android.support:recyclerview-v7:23.4.0'

Fragment代码

package com.demo.fragment;

import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import com.demo.R;
import com.demo.adapter.VideoRecyclerViewAdapter;
import com.demo.bean.VideoBean;

import java.util.ArrayList;
import java.util.List;

public class ListViewFragment extends Fragment{

    public static ListViewFragment newInstance() {
        return new ListViewFragment();
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.activity_recycler_view, container, false);
        initView(view);
        return view;
    }

    private void initView(View view) {
        RecyclerView recyclerView = view.findViewById(R.id.rv);
        LinearLayoutManager layoutManager=new LinearLayoutManager(getActivity());
        layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
        recyclerView.setLayoutManager(layoutManager);// 设置布局管理器
        DefaultItemAnimator itemAnimator = new DefaultItemAnimator();
        recyclerView .setItemAnimator(itemAnimator);// 设置Item添加和移除的动画
        itemAnimator.setSupportsChangeAnimations(false);
        recyclerView.setAdapter(new VideoRecyclerViewAdapter(getVideoList(), getActivity()));

    }

    public List getVideoList() {
        List videoList = new ArrayList<>();
        //...添加数据

        return videoList;
    }
}

R.layout.activity_recycler_view


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">
    <android.support.v7.widget.RecyclerView
        android:id="@+id/rv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

LinearLayout>

RecyclerView适配器Adapter代码

package com.demo.adapter;

import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;

import com.bumptech.glide.Glide;
import com.demo.R;
import com.demo.bean.VideoBean;
import com.demo.view.CustomVideoView ;

import java.util.List;

public class VideoRecyclerViewAdapter extends RecyclerView.Adapter.VideoHolder> {


        private List videos;
        private Context context;

        public VideoRecyclerViewAdapter(List videos, Context context) {
            this.videos = videos;
            this.context = context;
        }

        @Override
        public VideoHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View itemView = LayoutInflater.from(context).inflate(R.layout.item_video_auto_play, parent, false);
            return new VideoHolder(itemView);

        }

        @Override
        public void onBindViewHolder(final VideoHolder holder, int position) {

            VideoBean videoBean = videos.get(position);
            holder.title.setText(videoBean.getTitle());
            holder.videoView .setPlayUrl(videoBean.getUrl());

        }

        @Override
        public int getItemCount() {
            return videos.size();
        }

        public class VideoHolder extends RecyclerView.ViewHolder {

            private CustomVideoView videoView;
            private TextView title;

            VideoHolder(View itemView) {
                super(itemView);
                videoView= (CustomVideoView )itemView.findViewById(R.id.video_player);
                int widthPixels = context.getResources().getDisplayMetrics().widthPixels;
                videoView .setLayoutParams(new LinearLayout.LayoutParams(widthPixels, widthPixels / 16 * 9));
                title = itemView.findViewById(R.id.tv_title);
            }
        }
    }

布局管理器:RecyclerView.LayoutManager

上述代码中mLayoutManager 对象是布局管理器,RecyclerView提供了三种布局管理器:

  • LinerLayoutManager(线性):以垂直或者水平列表方式展示Item
  • GridLayoutManager (网格):以网格方式展示Item
  • StaggeredGridLayoutManager(瀑布流): 以瀑布流方式展示Item

适配器:RecyclerView.Adapter

RecyclerView.Adapter的使用方式和ListView的ListAdapter 类似,向RecyclerView提供显示的数据。
但是RecyclerView.Adapter做了一件了不起的优化,那就是RecyclerView.Adapter的

 public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) 

方法能够保证当前RecyclerView是确实需要你创建一个新的ViewHolder对象。而ListView的ListAdapter 对应的方法

    @Override
    public View getView(int i, View view, ViewGroup viewGroup)

需要在方法内部判断是重新创建一个View还是刷新一个View的数据,而不明所以的客户可能每次都会返回一个新创建的View造成ListView的卡顿和资源的浪费;创建和刷新作为两个不同的功能本来就应该在两个方法中实现—庆幸的是RecyclerView.Adapter解决了这个问题

视图容器:RecyclerView.ViewHolder

RecyclerView中强制客户使用ViewHolder,谈及ListView的时候就经常说到使用ViewHolder来进行优化。使用ViewHolder其中一点好处是能够避免重复调用方法findViewById(),对当前item的View中的子View进行管理。
ViewHolder 描述RecylerView中某个位置的itemView和元数据信息,属于Adapter的一部分。其实该类通常用于保存 findViewById 的结果
ViewHolder的mFlags属性

  • FLAG_BOUND ——ViewHolder已经绑定到某个位置,mPosition、mItemId、mItemViewType都有效
  • FLAG_UPDATE ——ViewHolder绑定的View对应的数据过时需要重新绑定,mPosition、mItemId还是一致的
  • FLAG_INVALID ——ViewHolder绑定的View对应的数据无效,需要完全重新绑定不同的数据
  • FLAG_REMOVED ——ViewHolder对应的数据已经从数据集移除
  • FLAG_NOT_RECYCLABLE ——ViewHolder不能复用
  • FLAG_RETURNED_FROM_SCRAP ——这个状态的ViewHolder会加到scrap list被复用。
  • FLAG_CHANGED ——ViewHolder内容发生变化,通常用于表明有ItemAnimator动画
  • FLAG_IGNORE ——ViewHolder完全由LayoutManager管理,不能复用
  • FLAG_TMP_DETACHED ——ViewHolder从父RecyclerView临时分离的标志,便于后续移除或添加回来
  • FLAG_ADAPTER_POSITION_UNKNOWN ——ViewHolder不知道对应的Adapter的位置,直到绑定到一个新位置
  • FLAG_ADAPTER_FULLUPDATE ——方法 addChangePayload(null) 调用时设置

间隔样式:RecyclerView.ItemDecoration

  • 用于绘制itemView之间的一些特殊UI,比如在itemView之前设置空白区域、画线等。
  • RecyclerView 将itemView和装饰UI分隔开来,装饰UI即 ItemDecoration ,主要用于绘制item间的分割线、高亮或者margin等
  • 通过recyclerView.addItemDecoration(new DividerDecoration(this))对item添加装饰;对RecyclerView设置多个ItemDecoration,列表展示的时候会遍历所有的ItemDecoration并调用里面的绘制方法,对Item进行装饰。
  • public void onDraw(Canvas c, RecyclerView parent) 装饰的绘制在Item条目绘制之前调用,所以这有可能被Item的内容所遮挡
  • public void onDrawOver(Canvas c, RecyclerView parent) 装饰的绘制在Item条目绘制之后调用,因此装饰将浮于Item之上
  • public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) 与padding或margin类似,LayoutManager在测量阶段会调用该方法,计算出每一个Item的正确尺寸并设置偏移量

展示效果和ListView基本上无差别,但是Item之间并没有分割线,在xml去找divider属性的时候,发现RecyclerView没有divider属性,当然也可以在Item布局中加上分割线,但是这样做并不是很优雅。
其实RecyclerView是支持自定义间隔样式的。通过

mRecyclerView.addItemDecoration()

来设置我们定义好的间隔样式

自定义间隔样式需要继承RecyclerView.ItemDecoration类,该类是个抽象类,主要有三个方法

  • onDraw(Canvas c, RecyclerView parent, State state):在Item绘制之前被调用,该方法主要用于绘制间隔样式
  • onDrawOver(Canvas c, RecyclerView parent, State state):在Item绘制之前被调用,该方法主要用于绘制间隔样式
  • getItemOffsets(Rect outRect, View view, RecyclerView parent, State state):设置item的偏移量,偏移的部分用于填充间隔样式,在RecyclerView的onMesure()中会调用该方法

onDraw()和onDrawOver()这两个方法都是用于绘制间隔样式,我们只需要复写其中一个方法即可。直接来看一下自定义的间隔样式的实现,参考官方实例

public class MyDividerItemDecoration extends RecyclerView.ItemDecoration {

    private static final int[] ATTRS = new int[]{
            android.R.attr.listDivider
    };
    public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;
    public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;
    /**
     * 用于绘制间隔样式
     */
    private Drawable mDivider;
    /**
     * 列表的方向,水平/竖直
     */
    private int mOrientation;


    public MyDividerItemDecoration(Context context, int orientation) {
        // 获取默认主题的属性
        final TypedArray a = context.obtainStyledAttributes(ATTRS);
        mDivider = a.getDrawable(0);
        a.recycle();
        setOrientation(orientation);
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        // 绘制间隔
        if (mOrientation == VERTICAL_LIST) {
            drawVertical(c, parent);
        } else {
            drawHorizontal(c, parent);
        }
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        if (mOrientation == VERTICAL_LIST) {
            outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
        } else {
            outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
        }
    }

    private void setOrientation(int orientation) {
        if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {
            throw new IllegalArgumentException("invalid orientation");
        }
        mOrientation = orientation;
    }

    /**
     * 绘制间隔
     */
    private void drawVertical(Canvas c, RecyclerView parent) {
        final int left = parent.getPaddingLeft();
        final int right = parent.getWidth() - parent.getPaddingRight();
        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                    .getLayoutParams();
            final int top = child.getBottom() + params.bottomMargin +
                    Math.round(ViewCompat.getTranslationY(child));
            final int bottom = top + mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }

    /**
     * 绘制间隔
     */
    private void drawHorizontal(Canvas c, RecyclerView parent) {
        final int top = parent.getPaddingTop();
        final int bottom = parent.getHeight() - parent.getPaddingBottom();
        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                    .getLayoutParams();
            final int left = child.getRight() + params.rightMargin +
                    Math.round(ViewCompat.getTranslationX(child));
            final int right = left + mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }
}

然后在代码中设置RecyclerView的间隔样式

mRecyclerView.addItemDecoration(new MyDividerItemDecoration(this, LinearLayoutManager.VERTICAL));

动画:RecyclerView.ItemAnimator

RecyclerView可以设置列表中Item删除和添加的动画,在v7包中给我们提供了一种默认的Item删除和添加的动画,如果没有特殊的需求,默认使用这个动画即可

// 设置Item添加和移除的动画
mRecyclerView.setItemAnimator(new DefaultItemAnimator());

设置的动画用于在 item 项数据变化时的动画效果
当调用 Adapter 的 notifyItemChanged、notifyItemInserted、notifyItemMoved 等方法,会触发该对象显示相应的动画。
RecyclerView 的 ItemAnimator 使得 item 的动画实现变得简单而样式丰富,我们可以自定义 item 项不同操作(如添加,删除)的动画效果;
ItemAnimator 触发于以下三种事件:

  • 某条数据被插入到数据集合中 ,对应 public final void notifyItemInserted(int position) 方法
  • 从数据集合中移除某条数据 ,对应 public final void notifyItemRemoved(int position) 方法
  • 更改数据集合中的某条数据,对应 public final void notifyItemChanged(int position) 方法

注意:notifyDataSetChanged(),会触发列表的重绘,并不会出现任何动画效果
使用:Animator使用到的逻辑比较多,因此最方便的就是使用第三方库:https://github.com/wasabeef/recyclerview-animators

点击事件

RecyclerView并没有像ListView一样暴露出Item点击事件或者长按事件处理的api,也就是说使用RecyclerView时候,需要我们自己来实现Item的点击和长按等事件的处理。
实现方法有很多

  • 可以监听RecyclerView的Touch事件然后判断手势做相应的处理
  • 可以通过在绑定ViewHolder的时候设置监听,然后通过Apater回调出去
  • 使用点击、长按事件支持类

第二种方法:在绑定ViewHolder的时候设置监听,通过Apater回调出去 Adapter 的完整代码

public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerViewAdapter .ViewHolder>{

    /**
     * 展示数据
     */
    private ArrayList mData;

    /**
     * 事件回调监听
     */
    private RecyclerViewAdapter.OnItemClickListener onItemClickListener;

    public RecyclerViewAdapter(ArrayList data) {
        this.mData = data;
    }

    public void updateData(ArrayList data) {
        this.mData = data;
        notifyDataSetChanged();
    }

    /**
     * 添加新的Item
     */
    public void addNewItem() {
        if(mData == null) {
            mData = new ArrayList<>();
        }
        mData.add(0, "new Item");
        notifyItemInserted(0);
    }

    /**
     * 删除Item
     */
    public void deleteItem() {
        if(mData == null || mData.isEmpty()) {
            return;
        }
        mData.remove(0);
        notifyItemRemoved(0);
    }

    /**
     * 设置回调监听
     * 
     * @param listener
     */
    public void setOnItemClickListener(MyAdapter.OnItemClickListener listener) {
        this.onItemClickListener = listener;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        // 实例化展示的view
        View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.view_rv_item, parent, false);
        // 实例化viewholder
        ViewHolder viewHolder = new ViewHolder(v);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(final ViewHolder holder, int position) {
        // 绑定数据
        holder.mTv.setText(mData.get(position));

        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(final View v) {
                if(onItemClickListener != null) {
                    int pos = holder.getLayoutPosition();
                    onItemClickListener.onItemClick(holder.itemView, pos);
                }
            }
        });

        holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                if(onItemClickListener != null) {
                    int pos = holder.getLayoutPosition();
                    onItemClickListener.onItemLongClick(holder.itemView, pos);
                }
                //表示此事件已经消费,不会触发单击事件
                return true;
            }
        });
    }

    @Override
    public int getItemCount() {
        return mData == null ? 0 : mData.size();
    }

    public static class ViewHolder extends RecyclerView.ViewHolder {

        TextView mTv;

        public ViewHolder(View itemView) {
            super(itemView);
            mTv = (TextView) itemView.findViewById(R.id.item_tv);
        }
    }

    public interface OnItemClickListener {
        void onItemClick(View view, int position);
        void onItemLongClick(View view, int position);
    }
}

Activity 设置 Adapter 事件监听

mAdapter.setOnItemClickListener(new MyAdapter.OnItemClickListener() {
    @Override
    public void onItemClick(View view, int position) {
        Toast.makeText(MyActivity.this,"click " + position + " item", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onItemLongClick(View view, int position) {
        Toast.makeText(MyActivity.this,"long click " + position + " item", Toast.LENGTH_SHORT).show();
    }
});

第三种方法:使用点击、长按事件支持类代码

参考 Hugo 的文章:Getting your clicks on RecyclerView

  1. 先要准备一份resources
    res -> values -> ids.xml ->

<resources>
    <item name="item_click_support" type="id" />
resources>
  1. 具体的支持类
public class ItemClickSupport {
    private final RecyclerView mRecyclerView;
    private OnItemClickListener mOnItemClickListener;
    private OnItemLongClickListener mOnItemLongClickListener;
    private View.OnClickListener mOnClickListener = new View.OnClickListener() {
        @Override public void onClick(View v) {
            if (mOnItemClickListener != null) {
                RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v);
                mOnItemClickListener.onItemClicked(mRecyclerView, holder.getAdapterPosition(), v);
            }
        }
    };
    private View.OnLongClickListener mOnLongClickListener = new View.OnLongClickListener() {
        @Override public boolean onLongClick(View v) {
            if (mOnItemLongClickListener != null) {
                RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v);
                return mOnItemLongClickListener.onItemLongClicked(mRecyclerView, holder.getAdapterPosition(), v);
            }
            return false;
        }
    };
    private RecyclerView.OnChildAttachStateChangeListener mAttachListener = new RecyclerView.OnChildAttachStateChangeListener() {
        @Override public void onChildViewAttachedToWindow(View view) {
            if (mOnItemClickListener != null) {
                view.setOnClickListener(mOnClickListener);
            }
            if (mOnItemLongClickListener != null) {
                view.setOnLongClickListener(mOnLongClickListener);
            }
        }

        @Override public void onChildViewDetachedFromWindow(View view) {

        }
    };

    private ItemClickSupport(RecyclerView recyclerView) {
        mRecyclerView = recyclerView;
        mRecyclerView.setTag(R.id.item_click_support, this);
        mRecyclerView.addOnChildAttachStateChangeListener(mAttachListener);
    }

    public static ItemClickSupport addTo(RecyclerView view) {
        ItemClickSupport support = (ItemClickSupport) view.getTag(R.id.item_click_support);
        if (support == null) {
            support = new ItemClickSupport(view);
        }
        return support;
    }

    public static ItemClickSupport removeFrom(RecyclerView view) {
        ItemClickSupport support = (ItemClickSupport) view.getTag(R.id.item_click_support);
        if (support != null) {
            support.detach(view);
        }
        return support;
    }

    public ItemClickSupport setOnItemClickListener(OnItemClickListener listener) {
        mOnItemClickListener = listener;
        return this;
    }

    public ItemClickSupport setOnItemLongClickListener(OnItemLongClickListener listener) {
        mOnItemLongClickListener = listener;
        return this;
    }

    private void detach(RecyclerView view) {
        view.removeOnChildAttachStateChangeListener(mAttachListener);
        view.setTag(R.id.item_click_support, null);
    }

    // 点击接口
    public interface OnItemClickListener {
        void onItemClicked(RecyclerView recyclerView, int position, View v);
    }

    // 长按接口
    public interface OnItemLongClickListener {
        boolean onItemLongClicked(RecyclerView recyclerView, int position, View v);
    }
}
  1. 在setAdapter()之后调用
// 点击
ItemClickSupport.addTo(rv).setOnItemClickListener(new ItemClickSupport.OnItemClickListener() {
    @Override public void onItemClicked(RecyclerView recyclerView, int position, View v) {
        Toast.makeText(MainActivity.this, mDatas.get(position), Toast.LENGTH_SHORT).show();
    }
});

// 长按
ItemClickSupport.addTo(rv).setOnItemLongClickListener(new ItemClickSupport.OnItemLongClickListener() {
    @Override public boolean onItemLongClicked(RecyclerView recyclerView, int position, View v) {

        Toast.makeText(MainActivity.this, "长按" + mDatas.get(position) + "已删除", Toast.LENGTH_SHORT).show();

        // 需要自己处理position在集合中的位置(需考虑头、身、脚布局数量)
        mDatas.remove(position);
        if (lastVisible + 1 == mAdapter.getItemCount()) {
            addmore();
        }
        mAdapter.notifyItemRemoved(position);

        // 消耗事件
        return true;
    }
});

总结

目前而言,我们已经知道RecyclerView的一些功能如下

  • 水平列表展示,设置LayoutManager的方向性
  • 竖直列表展示,设置LayoutManager的方向性
  • 自定义间隔,RecyclerView.addItemDecoration()
  • Item添加和删除动画,RecyclerView.setItemAnimator()

所以在项目中如果再遇见列表类的布局,就可以优先考虑使用 RecyclerView,更灵活更快捷的使用方式会给编码带来不一样的体验

你可能感兴趣的:(Android)