概述
最近学习了一下RecyclerView的封装与使用,感觉确实颇为神奇,真的节省了很多时间与代码量,特记录一下,以免自己的遗忘,也想帮助跟我一样的菜鸟,度过难关。
下面是取自偶像鸿洋大神的博客中的一段话,重点是介绍RecyclerView的基本用法和其方法的含义。
RecyclerView出现已经有一段时间了,想i性能大家肯定不陌生了,大家可以通过导入support-v7对其进行使用。
据官方的介绍,该控件用于在有限的窗口中展示大量数据集,其实这样功能的空间我们并不陌生,例如:ListView、GirdView。
那么有了ListView、GirdView。
那么有了ListView、GirdView为什么还需要RecyclerView这样的控件呢?整体上看RecyclerView架构,提供了一种插拔式的体验,高度的解耦,异常的灵活,通过设置它提供的不同LayoutManager,ItemDecoration,ItemAnimator实现令人瞠目的效果。
- 你想要控制其显示的方式,请通过布局管理器LayoutManager
- 你想要控制Item间的间隔(可绘制),请通过ItemDecoration
- 你想要控制Item增删的动画,请通过ItemAnimator
- 你想要控制点击、长按事件,请自己写。
基本使用
鉴于我们对于ListView的使用特别的熟悉,对比下Recycler的使用代码:
mRecyclerView = findView(R.id.id_recyclerview);
//设置布局管理器
mRecyclerView.setLayoutManager(layout);
//设置adapter
mRecyclerView.setAdapter(adapter)
//设置Item增加、移除动画
mRecyclerView.setItemAnimator(new DefaultItemAnimator());
//添加分割线
mRecyclerView.addItemDecoration(new DividerItemDecoration(
getActivity(), DividerItemDecoration.HORIZONTAL_LIST));
ok,相比较于ListView的代码,ListView可能只需要去设置一个adapter就能正常使用了,而RecyclerView基本需要上面一系列的步骤,那么为什么会添加这么多的步骤呢?
那么就必须解释下RecyclerView的这个名字了,从它类名上看,RecyclerView代表的意义是,我只管Recycler View ,也就是说RecyclerView只管回收与复用View,其他的你可以自己去设置。可以看出其高度的解耦,给与你充分的定制自由
(所以你才可以轻松的通过这个控件实现ListView,GirdView,瀑布流等效果)。
核心解密
我们的RecyclerView最重要的还是RecyclerAdapter方法,我们书写时是继承RecyclerView.Adapter的,并且需要传递一个ViewHolder。并且强制需要复写三个方法
创建ViewHolder
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
将数据绑定至ViewHolder
public void onBindViewHolder(MyViewHolder holder, int position)
获取总的条目数
public int getItemCount()
可见,RecyclerView对ViewHolder也进行了一定的封装,但是如果你仔细观察,你会发出一个疑问,ListView里面有个getView返回View为Item的布局,那么这个Item的样子在哪控制呢?其实是这样的,我们创建的ViewHolder必须继承RecyclerView.ViewHolder,这个RecyclerView.ViewHolder的构造时必须传入一个View,这个View相当与我们ListVIew getView中的convertView(即: 我们需要inflate的item布局需要传入)。
还有一点,ListView中convertView是复用的,在Recycler中,是把ViewHolder作为缓存的单位了,然后convertView作为ViewHolde的成员变量保持在ViewHolder中,也就是说,假设屏幕显示10条数据,则会创建10个ViewHolder缓存起来,每次复用的是ViewHolder,所以他把getView这个方法变为了onCreateViewHolder。
开始封装
通常我们的项目中有许许多多的列表,如果未经封装的话很有可能要重复写很多很多的Adapter,里面又大部分是重复的代码,而且这些Adapter和ViewHolder所做的事情又非常的像:视图绑定,数据绑定,点击事件分发等等,我们的目的是可以尽量复用我们的Adapter,所以我选择把Adapter设置成如下样式。这样做到我们基本可以复用一个Adapter,并且可以根据传入的泛型去改变Adapter最终显示的样子。
public abstract class RecyclerAdapter<Data>
extends RecyclerView.Adapter
ViewHolder的显示样式也是采用泛型的策略。因为最终的显示是由ViewHolder所绑定的那个View所决定的,所以真正的ViewHoldr需要外面的使用者传递给我们。
/**
* 自定义的ViewHolder
*
* @param <Data> 范型类型
*/
public static abstract class ViewHolder<Data> extends RecyclerView.ViewHolder {
private Unbinder unbinder;
private AdapterCallback<Data> callback;
protected Data mData;
public ViewHolder(View itemView) {
super(itemView);
}
/**
* 用于绑定数据的触发
*
* @param data 绑定的数据
*/
void bind(Data data) {
this.mData = data;
onBind(data);
}
/**
* 当触发绑定数据的时候的回掉;必须复写
*
* @param data 绑定的数据
*/
protected abstract void onBind(Data data);
/**
* Holder自己对自己对应的Data进行更新操作
*
* @param data Data数据
*/
public void updateData(Data data) {
if (this.callback != null) {
this.callback.update(data, this);
}
}
}
接口是发生数据改变时的回调,定义为如下样式
public interface AdapterCallback<Data> {
void update(Data data, RecyclerAdapter.ViewHolder<Data> holder);
}
剩下的就是我们一些简单的点击事件的回调,以及List增加数据的操作
整体的封装
package net.qiujuer.italker.common.widget.recycler;
import android.support.annotation.LayoutRes;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import net.qiujuer.italker.common.R;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import butterknife.ButterKnife;
import butterknife.Unbinder;
/**
* @author HFRX hfrx1314@qq.com
* @version 1.0.0
*/
@SuppressWarnings({"unchecked", "unused"})
public abstract class RecyclerAdapter<Data>
extends RecyclerView.Adapter<RecyclerAdapter.ViewHolder<Data>>
implements View.OnClickListener, View.OnLongClickListener,
AdapterCallback<Data> {
private final List<Data> mDataList;
private AdapterListener<Data> mListener;
/**
* 构造函数模块
*/
public RecyclerAdapter() {
this(null);
}
public RecyclerAdapter(AdapterListener<Data> listener) {
this(new ArrayList<Data>(), listener);
}
public RecyclerAdapter(List<Data> dataList, AdapterListener<Data> listener) {
this.mDataList = dataList;
this.mListener = listener;
}
/**
* 复写默认的布局类型返回
*
* @param position 坐标
* @return 类型,其实复写后返回的都是XML文件的ID
*/
@Override
public int getItemViewType(int position) {
return getItemViewType(position, mDataList.get(position));
}
/**
* 得到布局的类型
*
* @param position 坐标
* @param data 当前的数据
* @return XML文件的ID,用于创建ViewHolder
*/
@LayoutRes
protected abstract int getItemViewType(int position, Data data);
/**
* 创建一个ViewHolder
*
* @param parent RecyclerView
* @param viewType 界面的类型,约定为XML布局的Id
* @return ViewHolder
*/
@Override
public ViewHolder<Data> onCreateViewHolder(ViewGroup parent, int viewType) {
// 得到LayoutInflater用于把XML初始化为View
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
// 把XML id为viewType的文件初始化为一个root View
View root = inflater.inflate(viewType, parent, false);
// 通过子类必须实现的方法,得到一个ViewHolder
ViewHolder<Data> holder = onCreateViewHolder(root, viewType);
// 设置View的Tag为ViewHolder,进行双向绑定
root.setTag(R.id.tag_recycler_holder, holder);
// 设置事件点击
root.setOnClickListener(this);
root.setOnLongClickListener(this);
// 进行界面注解绑定
holder.unbinder = ButterKnife.bind(holder, root);
// 绑定callback
holder.callback = this;
return holder;
}
/**
* 得到一个新的ViewHolder
*
* @param root 根布局
* @param viewType 布局类型,其实就是XML的ID
* @return ViewHolder
*/
protected abstract ViewHolder<Data> onCreateViewHolder(View root, int viewType);
/**
* 绑定数据到一个Holder上
*
* @param holder ViewHolder
* @param position 坐标
*/
@Override
public void onBindViewHolder(ViewHolder<Data> holder, int position) {
// 得到需要绑定的数据
Data data = mDataList.get(position);
// 触发Holder的绑定方法
holder.bind(data);
}
/**
* 得到当前集合的数据量
*/
@Override
public int getItemCount() {
return mDataList.size();
}
/**
* 返回整个集合
* @return List<Data>
*/
public List<Data> getItems(){
return mDataList;
}
/**
* 插入一条数据并通知插入
*
* @param data Data
*/
public void add(Data data) {
mDataList.add(data);
notifyItemInserted(mDataList.size() - 1);
}
/**
* 插入一堆数据,并通知这段集合更新
*
* @param dataList Data
*/
public void add(Data... dataList) {
if (dataList != null && dataList.length > 0) {
int startPos = mDataList.size();
Collections.addAll(mDataList, dataList);
notifyItemRangeInserted(startPos, dataList.length);
}
}
/**
* 插入一堆数据,并通知这段集合更新
*
* @param dataList Data
*/
public void add(Collection<Data> dataList) {
if (dataList != null && dataList.size() > 0) {
int startPos = mDataList.size();
mDataList.addAll(dataList);
notifyItemRangeInserted(startPos, dataList.size());
}
}
/**
* 删除操作
*/
public void clear() {
mDataList.clear();
notifyDataSetChanged();
}
/**
* 替换为一个新的集合,其中包括了清空
*
* @param dataList 一个新的集合
*/
public void replace(Collection<Data> dataList) {
mDataList.clear();
if (dataList == null || dataList.size() == 0)
return;
mDataList.addAll(dataList);
notifyDataSetChanged();
}
@Override
public void update(Data data, ViewHolder<Data> holder) {
int pos = holder.getAdapterPosition();
if(pos>=0){
mDataList.remove(pos);
mDataList.add(pos,data);
notifyItemChanged(pos);
}
}
@Override
public void onClick(View v) {
ViewHolder viewHolder = (ViewHolder) v.getTag(R.id.tag_recycler_holder);
if (this.mListener != null) {
// 得到ViewHolder当前对应的适配器中的坐标
int pos = viewHolder.getAdapterPosition();
// 回掉方法
this.mListener.onItemClick(viewHolder, mDataList.get(pos));
}
}
@Override
public boolean onLongClick(View v) {
ViewHolder viewHolder = (ViewHolder) v.getTag(R.id.tag_recycler_holder);
if (this.mListener != null) {
// 得到ViewHolder当前对应的适配器中的坐标
int pos = viewHolder.getAdapterPosition();
// 回掉方法
this.mListener.onItemLongClick(viewHolder, mDataList.get(pos));
return true;
}
return false;
}
/**
* 设置适配器的监听
*
* @param adapterListener AdapterListener
*/
public void setListener(AdapterListener<Data> adapterListener) {
this.mListener = adapterListener;
}
/**
* 我们的自定义监听器
*
* @param <Data> 范型
*/
public interface AdapterListener<Data> {
// 当Cell点击的时候触发
void onItemClick(RecyclerAdapter.ViewHolder holder, Data data);
// 当Cell长按时触发
void onItemLongClick(RecyclerAdapter.ViewHolder holder, Data data);
}
/**
* 自定义的ViewHolder
*
* @param <Data> 范型类型
*/
public static abstract class ViewHolder<Data> extends RecyclerView.ViewHolder {
private Unbinder unbinder;
private AdapterCallback<Data> callback;
protected Data mData;
public ViewHolder(View itemView) {
super(itemView);
}
/**
* 用于绑定数据的触发
*
* @param data 绑定的数据
*/
void bind(Data data) {
this.mData = data;
onBind(data);
}
/**
* 当触发绑定数据的时候,的回掉;必须复写
*
* @param data 绑定的数据
*/
protected abstract void onBind(Data data);
/**
* Holder自己对自己对应的Data进行更新操作
*
* @param data Data数据
*/
public void updateData(Data data) {
if (this.callback != null) {
this.callback.update(data, this);
}
}
}
/**
* 对回调接口做一次实现
* @param <Data>
*/
public static abstract class AdapterListenerImpl<Data> implements AdapterListener<Data>{
@Override
public void onItemClick(ViewHolder holder, Data data) {
}
@Override
public void onItemLongClick(ViewHolder holder, Data data) {
}
}
}
拆解
其实大家在处理ListView的时候就会碰到很多图片显示错乱的问题,或者说是点击事件错乱的问题。大都可以通过一个setTag去解决。但是我个人就一直有个疑问,我知道要用setTag,我知道setTag可以解决图片错乱的问题,但是为啥呢,怎么解决的呢。
简单的说,Recycler就是把数据显示在View上的一个控件,他关注View的Recycler,具有复用View的功能;而对于对应关系,我们希望能够做到的是:当知道数据集合的一个Item的时候能够访问到对应的View,并更新View的信息,比如我们更新了数据的name字段,name我们希望更新View对应的界面;这个过程recycler已经给我们搞定了,name还有一种情况,什么呢?就是当我们操作界面的时候比如我们在界面的某个View上点击了一下,此时我们希望能够一下就知道我们点击的是哪一个数据;好的此时因为View的复用性,他可能对应任意数据;那么当前对应的是什么呢?接口上来说并不提供,所以我们利用View的tag机制,当进行数据绑定的时候把数据绑定到View的tag上,那么当我们操作View的时候就可以从View的tag中拿出对应的数据出来,从而实现快速的操作,减少不必要的循环查询,以及错位的情况,这就是类似于双向绑定的开发模式。
/**
* Sets a tag associated with this view and a key. A tag can be used
* to mark a view in its hierarchy and does not have to be unique within
* the hierarchy. Tags can also be used to store data within a view
* without resorting to another data structure.
*
* The specified key should be an id declared in the resources of the
* application to ensure it is unique (see the <a
* href="{@docRoot}guide/topics/resources/more-resources.html#Id">ID resource type</a>).
* Keys identified as belonging to
* the Android framework or not associated with any package will cause
* an {@link IllegalArgumentException} to be thrown.
*
* @param key The key identifying the tag
* @param tag An Object to tag the view with
*
* @throws IllegalArgumentException If they specified key is not valid
*
* @see #setTag(Object)
* @see #getTag(int)
*/
public void setTag(int key, final Object tag) {
// If the package id is 0x00 or 0x01, it's either an undefined package
// or a framework id
if ((key >>> 24) < 2) {
throw new IllegalArgumentException("The key must be an application-specific "
+ "resource id.");
}
setKeyedTag(key, tag);
}
setTag有一长串的解释,大致意思就是说我们可以设置一个Tag,将View与Key关联起来,这个tag可以被使用去标记一个View在他的数据结构中。最简单的方法就是创建一个id在Resource中。
在往里就是这个方法了,去更新对应的Tag,这样就把View和tag绑定并存储在了一个数据结构中,我们通过getTag的时候,就会去拿跟这个tag绑定的View。所以就一般不会出现错乱的问题。
private void setKeyedTag(int key, Object tag) {
if (mKeyedTags == null) {
mKeyedTags = new SparseArray<Object>(2);
}
mKeyedTags.put(key, tag);
}