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地址