RecyclerView使用详解

RecyclerView是Google官方推出的用于替代ListView的产品,本身就支持线性布局、网格布局和瀑布流布局等,同时,支持很多新的特性,使用RecyclerView时,必须制定一个适配器(Adapter,继承自RecyclerView.Adapter)和一个布局管理器,具体如下:

基本使用

定义Adapter继承自RecyclerView.Adapter,同时,在内部继承自RecyclerView.ViewHolder,复写以下方法:

public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType);

此方法加载布局,返回自己实现的ViewHolder。

public void onBindViewHolder(ViewHolder holder, int position);

此方法是在将数据绑定到ViewHolder上。

public int getItemCount();

此方法返回item数量。 示例代码如下:

import android.content.Context;
import android.net.Uri;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;

import com.bumptech.glide.Glide;
import com.cmbc.firefly.recyclerviewdemo.R;
import com.cmbc.firefly.recyclerviewdemo.bean.InfoBean;

import java.util.List;


/**
 * RecyclerView适配器
 */
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {


    private List<InfoBean> mInfos;
    private Context mContext;

    public MyAdapter(Context context, List<InfoBean> infos) {
        mInfos = infos;
        mContext = context;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(mContext).inflate(R.layout.item_layout, parent, false);
        ViewHolder viewHolder = new ViewHolder(view);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        if (mInfos != null) {
            InfoBean info = mInfos.get(position);
            if (info != null) {
                holder.mTvTitle.setText(info.getTitle());
                Glide.with(mContext).load(Uri.parse(info.getImgUrl())).into(holder.mIvImg);
            }
        }
    }

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

    class ViewHolder extends RecyclerView.ViewHolder {
        private TextView mTvTitle;
        private ImageView mIvImg;

        public ViewHolder(View itemView) {
            super(itemView);
            mTvTitle = (TextView) itemView.findViewById(R.id.tv_title);
            mIvImg = (ImageView) itemView.findViewById(R.id.iv_img);
        }
    }

}

同时,初始化RecyclerView、数据以及Adapter,同时设置RecyclerView的布局管理器,这里,我们使用线性布局:

import android.app.Activity;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import com.cmbc.firefly.recyclerviewdemo.R;
import com.cmbc.firefly.recyclerviewdemo.adapter.MyAdapter;
import com.cmbc.firefly.recyclerviewdemo.bean.InfoBean;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends Activity {
    private RecyclerView mRecyclerView;
    private MyAdapter mAdapter;
    private List<InfoBean> mInfos;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById();
        init();
    }
    private void findViewById() {
        mRecyclerView = (RecyclerView) findViewById(R.id.recyclerView);
    }
    private void init() {
        mInfos = new ArrayList<InfoBean>();
        for (int i = 0; i < 20; i++) {
            InfoBean infoBean = new InfoBean();
            infoBean.setTitle("标题" + i);
            infoBean.setImgUrl("http://img.my.csdn.net/uploads/201407/26/1406383092_3071.jpg");
            mInfos.add(infoBean);
        }
        mAdapter = new MyAdapter(this, mInfos);
        //设置线性布局
        mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
        mRecyclerView.setAdapter(mAdapter);
    }
}
设置分割线

RecyclerView通过:

public void addItemDecoration(ItemDecoration decor)

方法来设置分割线,ItemDecoration是一个抽象类,需要我们自己去实现它,主要实现以下的两个方法:

//绘制分割线
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state);
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state);
//设置原item的偏移大小
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state);

其中,onDraw和onDrawOver方法实现一个即可。 这里以线性布局为例,来说明RecyclerView分割线的实现方法。 在绘制分割线时,需要区分两种情况,垂直的线性布局和水平的线性布局,因为要分别画横线和竖线。我们定义两个常量,在构造ItemDecoration对象时,根据实际的布局,传入方向:

    public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;
    public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;

根据不同的线性布局方向来分别水平分割线和垂直分割线,我们编写两个构造方法,一个是使用系统的分割线,另一个是用户传入分割线的图片:

 /**
 * 使用系统的分割线
 *
 * @param context
 * @param orientation
 */
public MyDecoration(Context context, int orientation) {
    final TypedArray a = context.obtainStyledAttributes(new int[]{android.R.attr.listDivider});
    mDivider = a.getDrawable(0);
    a.recycle();
    setOrientation(orientation);
}

/**
 * 自定义分割线
 *
 * @param context
 * @param orientation 列表方向
 * @param drawableId  分割线图片
 */
public MyDecoration(Context context, int orientation, int drawableId) {
    this(context, orientation);
    mDivider = ContextCompat.getDrawable(context, drawableId);
    mDividerHeight = mDivider.getIntrinsicHeight();
}

其中通过Drawable的'getIntrinsicHeight()'方法来获取Drawable实际的高度来作为分割线的高度(或者宽度)。 在实际绘制分割线时,通过Drawable的:

    public void setBounds(int left, int top, int right, int bottom);

方法来确定在Canvas的哪个矩形区域内进行绘制Drawable。那么这四个坐标怎么计算呢,我们以绘制垂直先行布局的水平分割线来说明:

 /**
 * 绘制垂直线性布局排列的分割线
 *
 * @param c
 * @param parent
 */
public 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);
        RecyclerView v = new RecyclerView(parent.getContext());
        final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
        final int top = child.getBottom() + params.bottomMargin;
        final int bottom = top + mDividerHeight;
        if (mDivider != null) {
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }
}

left我们用RecyclerView获取的左边距即为分割线的left; rigth,为RecyclerView的宽度减去item的右边距; top,即图中蓝线的位置,为item本身的bottom坐标,再加上item相对于下一个item的距离,也就是item的layout_marginBottom属性(蓝线和黑线之间的距离); bottom,即为top再加上分割线本身的高度(宽度)。

由于设置了Divider分割线,分割线本身具有高度(垂直线性布局)和宽度(水平线性布局),因此,原来的item相比于原来的位置有迁移,而迁移的大小为分割线的高度或者宽度:

@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
    if (mOrientation == VERTICAL_LIST) {
        //item下边的Y坐标偏移分割线的高度
        outRect.set(0, 0, 0, mDividerHeight);
    } else {
        //item右边的x坐标偏移分割线的宽度
        outRect.set(0, 0, mDividerHeight, 0);
    }
}

在使用自定义Drawable作为分割线时,需要使用"rectangle",而不能使用"line",若分割线有左右偏移量,可以使用"inset",示例如下:

<?xml version="1.0" encoding="utf-8"?>
<inset xmlns:android="http://schemas.android.com/apk/res/android"
       android:insetLeft="15dp">
    <shape
        android:shape="rectangle"
        >
        <solid
            android:color="#eaeaea"/>
        <size android:height="1dp"/>
    </shape>
</inset>
设置头部布局和尾部布局

通过在Adapter中设置item的类型,设置一下三种类型:

//头部布局
private static final int TYPE_HEADER = 0;
//正常item布局
private static final int TYPE_ITEM = 1;
//尾部布局
private static final int TYPE_FOOTER = 2;

Adapter中提供设置头部布局和尾部布局的方法,

//设置尾部布局
 public void setFooterView(View view) {
    mFooterView = view;
    mFooterCnt = 1;
}
//设置尾部布局
public void setHeaderView(View view) {
    mHeaderView = view;
    mHeaderCnt = 1;
}

使用mFooterCnt和mHeaderCnt来计数,这样在返回item数量的方法里面可以直接使用:

 @Override
public int getItemCount() {
    return mInfos == null ? (mFooterCnt + mHeaderCnt) : (mInfos.size() + mFooterCnt + mHeaderCnt);
}

在Adapter中,复写

public int getItemViewType(int position){
    if (position == 0 && mHeaderView != null) {
        return TYPE_HEADER;
    } else if (position == (mHeaderCnt + mInfos.size()) && mFooterView != null) {
        return TYPE_FOOTER;
    } else {
        return TYPE_ITEM;
    }
}

根据不同的位置以及是否有头尾布局来确定item的类型。

在'onCreateViewHolder',根据不同的viewType,构造ViewHolder时,传入不同的itemView,如下:

@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    ViewHolder viewHolder = null;
    switch (viewType) {
        case TYPE_ITEM:
            View view = LayoutInflater.from(mContext).inflate(R.layout.item_layout, parent, false);
            viewHolder = new ViewHolder(view, viewType);
            return viewHolder;
        case TYPE_FOOTER:
            viewHolder = new ViewHolder(mFooterView, viewType);
            break;
        case TYPE_HEADER:
            viewHolder = new ViewHolder(mHeaderView, viewType);
            break;
    }
    return viewHolder;
}

注意,在'onBindViewHolder'绑定数据时,获取从List中获取数据Item对象时,要用当前的position减去头部布局的cnt,即:

InfoBean info = mInfos.get(position - mHeaderCnt);
item点击和长按事件

RecyclerView不像ListView一样,本身提供了item的长按和点击事件,需要自己去实现,首先定义长按和点击事件的回调街口:

/**
 * RecyclerView使用,点击事件
 */
public interface OnItemClickListener {
    void onItemClick(View view, int position);
}

/**
 * RecyclerView使用,长按事件
 */
public interface OnItemLongClickListener {
    boolean onItemLongClick(View view, int position);
}

然后在Adapter中增加设置点击和长按事件的方法:

public void setOnItemLongClickListener(OnItemLongClickListener onItemLongClickListener) {
    mOnItemLongClickListener = onItemLongClickListener;
}

public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
    mOnItemClickListener = onItemClickListener;
}

在"onBindViewHolder"中,数据绑定时,设置ItemView的长按或者点击事件,在事件中回调我们自定义接口的长按或者点击回调:

holder.itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (mOnItemClickListener != null) {
                        mOnItemClickListener.onItemClick(holder.itemView, position);
                    }
                }
            });
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
                @Override
                public boolean onLongClick(View v) {
                    if (mOnItemLongClickListener != null) {
                        return mOnItemLongClickListener.onItemLongClick(holder.itemView, position);
                    }
                    return false;
                }
            });
添加、删除Item的动画

RecyclerView通过:

public void setItemAnimator(ItemAnimator animator);

方法设置item添加和删除动画,我们可以使用系统自带的动画,如下:

mRecyclerView.setItemAnimator(new DefaultItemAnimator());

同时,如果要使用动画,在添加、删除数据,数据发生变化时,不能调用'notifyDataSetChanged()'方法,而是分别使用

 public final void notifyItemInserted(int position);
 public final void notifyItemRemoved(int position);

方法进行通知,如下,我们在adapter中增加添加item和删除item的方法:

public void addItem(int postition, InfoBean infoBean) {
    try {
        mInfos.add(postition, infoBean);
        notifyItemInserted(postition);
    } catch (Exception e) {
        Log.e(TAG, e.getMessage(), e);
    }
}

public void removeItem(int postition) {
    try {
        mInfos.remove(postition);
        notifyItemRemoved(postition);
    } catch (Exception e) {
        Log.e(TAG, e.getMessage(), e);
    }
}

效果如下: 当然,除了使用系统默认的动画,github上也有一些大神实现了一些炫酷的动画,例如: RecycerView开源动画

下拉加载更多

这里,还是以线性布局为例子,我们利用RecyclerView的OnScrollListener来实现,我们自定义'LoadMoreOnScrollListener'继承自'RecyclerView.OnScrollListener',如下:

import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;

public abstract class LoadMoreOnScrollListener extends RecyclerView.OnScrollListener {

    private static final String TAG = LoadMoreOnScrollListener.class.getSimpleName();

    private LinearLayoutManager mLinearLayoutManager;
    private int totalItemCount;

    //记录前一个totalItemCount
    private int previousTotal;
    private int visibleItemCount;

    //在屏幕可见的item中的第一个
    private int firstVisibleItem;

    //是否正在加载数据
    private boolean isLoading = false;

    public LoadMoreOnScrollListener(LinearLayoutManager linearLayoutManager) {
        mLinearLayoutManager = linearLayoutManager;
    }

    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        super.onScrolled(recyclerView, dx, dy);
        visibleItemCount = mLinearLayoutManager.getChildCount();
        totalItemCount = mLinearLayoutManager.getItemCount();
        firstVisibleItem = mLinearLayoutManager.findFirstVisibleItemPosition();
        Log.d(TAG, "visibleItemCount : " + visibleItemCount);
        if (totalItemCount > previousTotal) {
            //说明数据已经加载结束
            isLoading = false;
            previousTotal = totalItemCount;
        }
        if (!isLoading && totalItemCount - visibleItemCount <= firstVisibleItem) {
            loadMoreData();
            isLoading = true;
        }
    }

    public abstract void loadMoreData();
}

在onScrolled方法中进行判断,其中visibleItemCount为当前屏幕中可见item数量,totalItemCount为RecyclerView的所有Item数量,firstVisibleItem为当前屏幕第一个可见item数量,当用户滑动recyclerView到底部时,总数量-当前屏幕item数量要小于或者等于当前屏幕第一个课件item的position,即'totalItemCount - visibleItemCount <= firstVisibleItem'。 同时,为了防止已经在加载时还调用加载更多,我们设置一个isLoading的标志位,那么如何判断已经加载了更多数据呢,

整个Demo的地址如下: Demo地址