默认的RecyclerView.Adapter创建时是如下方式的.
public class RecyclerViewAdapter extends RecyclerView.Adapter {
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return null;
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
}
@Override
public int getItemCount() {
return 0;
}
}
我们可以发现,我们需要复写这三个方法,每次都这样大家会觉得比较麻烦,接下来我们来一步步实现.
首先我们先创建一个默认的ViewHolder
public class ViewHolder extends RecyclerView.ViewHolder{
public ViewHolder(@NonNull View itemView) {
super(itemView);
}
}
接下来我们创建一个RecyclerCommonAdapter继承自RecyclerView.Adapter,ViewHolder用我们自定义的这个.
代码如下:
public class RecyclerCommonAdapterextends RecyclerView.Adapter<ViewHolder> {
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return null;
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
}
@Override
public int getItemCount() {
return 0;
}
}
下一步我们分析一下构造方法我们需要传入什么参数,首先第一个就是上下文对象Context,这个在LayoutInflater创建时需要用到,
第二个我们需要传入我们的数据集合,这里我们用List泛型,因为我们不知道用户会传入什么类型的数据,第三个我们需要传入的是布局文件资源.
代码如下.
protected Context mContext;
protected int mLayoutId;
protected List<T> mDatas;
private LayoutInflater mLayoutInflater;
public RecyclerCommonAdapter(Context context, List<T> mDatas, int mLayoutId) {
mLayoutInflater = LayoutInflater.from(context);
this.mContext = context;
this.mLayoutId = mLayoutId;
this.mDatas = mDatas;
}
在这里我们先把LayoutInflater初始化一下,后面需要用到.
下一步我们需要复写onCreateViewHolder和getItemCount方法
这里比较简单,我们只需要把contentView初始化一下.并放到ViewHolder中
View itemView = mLayoutInflater.inflate(mLayoutId,parent,false);
return new ViewHolder(itemView);
getItemCount就只是返回条目的数量
@Override
public int getItemCount() {
return mDatas.size();
}
下一步是比较关键的,我们需要复写onBindViewHolder,在这里我们需要定义一个abstract方法,保证使用我们这个工具的人都会复写这个方法.
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, final int position) {
convert(holder,mDatas.get(position),position);
}
protected abstract void convert(ViewHolder holder, T t, int position);
在这里我们将当前的ViewHolder,当前条目的数据以及当前条目的position传出去.
写到这里,我们先来定义一个TestAdapter来测试一下这个Adapter
public class TestAdapter extends RecyclerCommonAdapter<String> {
public TestAdapter(Context context, List<String> data){
super(context,data,R.layout.item_common_adapter);
}
@Override
protected void convert(ViewHolder holder, String s, int position) {
}
}
在这里我们直接继承自RecyclerViewAdapter,数据类型我们直接用String,我们只是一个测试,就不搞那么复杂了.
在这个构造方法里面我们传入上下文对象和我们的数据集合,Layout资源我们可以从外面传过来,也可以直接在里面写,我们这里就直接在里面写了,接下来我们还需要实现convert方法.
item_common_adapter布局我里面就放了两个TextView和一个ImageView.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/icon"
android:scaleType="centerInside"
android:adjustViewBounds="true"
android:layout_width="80dp"
android:layout_height="80dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:orientation="vertical">
<TextView
android:id="@+id/text1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="?android:attr/listPreferredItemPaddingStart" />
<TextView
android:id="@+id/text2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="?android:attr/listPreferredItemPaddingStart" />
</LinearLayout>
</LinearLayout>
接下来我们来写convert方法里面的内容.这里我们直接用findViewById的方式去找到我们的控件
TextView text1 = holder.itemView.findViewById(R.id.text1);
TextView text2 = holder.itemView.findViewById(R.id.text2);
ImageView imageView = holder.itemView.findViewById(R.id.icon);
text1.setText(s);
text2.setText(s+s);
imageView.setImageResource(R.mipmap.ic_launcher);
我们来测试一下,我们先在布局文件activity_main中放一个RecyclerView
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFFFFF" />
</LinearLayout>
下一步在MainActivity中给这个RecyclerView设置Adapter
public class MainActivity extends AppCompatActivity {
private RecyclerView mRecyclerView;
private List<String> mDatas;
private TestAdapter mAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initData();
mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
mAdapter = new TestAdapter(this, mDatas);
mRecyclerView.setAdapter(mAdapter);
// 设置显示分割 ListView样式
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
}
protected void initData() {
mDatas = new ArrayList<String>();
for (int i = 'A'; i < 'Z'+1; i++) {
mDatas.add("" + (char) i);
}
}
}
我们运行下看下效果
现在我们发现,我们的布局已经出来了,效果没问题
下面我们优化一下我们的ViewHolder;
在TestAdapter里面,我们可以看到,我们是通过findViewById的方式找到我们的View,如果有多个TextView,我们需要写多个findViewById.所以我们可以在ViewHolder里面来实现我们的setText方法.
首先我们定义一个getView方法来获取我们的控件
// 用来存放子View减少findViewById的次数
private SparseArray<View> mViews;
public ViewHolder(@NonNull View itemView) {
super(itemView);
mViews = new SparseArray<>();
}
/**
* 通过id获取view
*/
public <T extends View> T getView(int viewId){
// 先从缓存中找
View view = mViews.get(viewId);
if (null == view){
// 直接从ItemView中找
view = itemView.findViewById(viewId);
mViews.put(viewId,view);
}
return (T)view;
}
在这里我们可以通过缓存的方式将我们的控件存起来,这样可以减少findViewById的次数.我们使用SparseArray来存放我们的View.
接下来我们来写setText方法.
/**
* 设置TextView文本
*/
public ViewHolder setText(int viewId, String text){
View view = getView(viewId);
if (view instanceof TextView){
((TextView) view).setText(text);
}
return this;
}
这里我们返回this是为了可以链式调用,这个在Builder模式中使用的比较多,多次调用有优势.
这样,我们前面的
TextView text1 = holder.itemView.findViewById(R.id.text1);
TextView text2 = holder.itemView.findViewById(R.id.text2);
text1.setText(s);
text2.setText(s+s);
就可以修改为
holder.setText(R.id.text1,s).setText(R.id.text2,s+s);
这样是不是简单多了
同样的,ImageView我们也可以这么做
/**
* 设置ImageView的资源
*/
public ViewHolder setImageResource(int viewId,int resourceId){
View view = getView(viewId);
if (view instanceof ImageView){
((ImageView) view).setImageResource(resourceId);
}
return this;
}
这样imageView就可以通过
holder.setImageResource(R.id.icon,R.mipmap.ic_launcher);
来设置了
接下来我们看看如何加载网络中的图片我们要怎么做呢.
因为我们不能指定用户用什么框架来加载图片,所以我们需要回调给用户自己去处理
/**
* 图片加载,这里稍微处理得复杂一些,因为考虑加载图片的第三方可能不太一样
* 也可以不写这个类
*/
public abstract static class HolderImageLoader{
private String url;
public HolderImageLoader(String url){
this.url = url;
}
/**
* 要去复写这个方法加载图片
* @param imageView
* @param path
*/
public abstract void loadImage(ImageView imageView, String path);
public String getUrl(){
return url;
}
}
我们先定义一个HolderImageLoader,然后在setImageByUrl传入我们要的viewId和HolderImageLoader
/**
* 设置图片通过路径,这里稍微处理得复杂一些,因为考虑加载图片的第三方可能不太一样
* 也可以直接写死
*/
public ViewHolder setImageByUrl(int viewId,HolderImageLoader imageLoader){
View view = getView(viewId);
if (view instanceof ImageView){
imageLoader.loadImage((ImageView) view,imageLoader.getUrl());
}
return this;
}
这样,在我们写的TestAdapter中就需要定义一个ImageLoader.
当然,大家也可以将这个ImageLoader类定义在外面,这样整个工程都可以使用,这里我们用Glide来加载图片
public class ImageLoader extends ViewHolder.HolderImageLoader {
public ImageLoader(String url){
super(url);
}
@Override
public void loadImage(ImageView imageView, String url) {
Glide.with(imageView.getContext()).load(url).placeholder(R.drawable.ic_discovery_default_channel).into(imageView);
}
}
在TestAdapter里面这么用就行了.
holder.setImageByUrl(R.id.icon,new ImageLoader("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1563269254640&di=3ba62579d87d361cd93ab10cdecea49f&imgtype=0&src=http%3A%2F%2Fp2.bahamut.com.tw%2FM%2F2KU%2F37%2F0001126337.JPG%3F_%3D1421643125179"));
这样,TextView和ImageView的设置方法我们就写好了,最后我们来加上ItemClick和ItemLongClick监听.
首先,自定义ItemClickListener和ItemLongClickListener接口
public interface ItemClickListener {
public void onItemClick(int position);
}
public interface ItemLongClickListener {
public boolean onLongClick(int position);
}
接下来在ViewHolder里面增加两个方法
/**
* 设置条目点击事件
*/
public void setOnItemClickListener(View.OnClickListener listener) {
itemView.setOnClickListener(listener);
}
/**
* 设置条目长按事件
*/
public void setOnItemLongClickListener(View.OnLongClickListener listener) {
itemView.setOnLongClickListener(listener);
}
然后在RecyclerCommonAdapter中增加
private ItemClickListener mItemClickListener;
private ItemLongClickListener mItemLongClickListener;
/***************
* 给条目设置点击和长按事件
*********************/
public void setOnItemClickListener(ItemClickListener itemClickListener){
this.mItemClickListener = itemClickListener;
}
public void setOnItemLongClickListener(ItemLongClickListener itemLongClickListener){
this.mItemLongClickListener = itemLongClickListener;
}
在RecyclerCommonAdapter.onBindViewHolder中增加
// 设置点击和长按事件
if (null != mItemClickListener){
holder.setOnItemClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mItemClickListener.onItemClick(position);
}
});
}
if (null != mItemLongClickListener){
holder.setOnItemLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
return mItemLongClickListener.onLongClick(position);
}
});
}
最后,在MainAActivity中使用
mAdapter.setOnItemClickListener(new ItemClickListener() {
@Override
public void onItemClick(int position) {
Toast.makeText(MainActivity.this, "点击--"+position, Toast.LENGTH_SHORT).show();
}
});
mAdapter.setOnItemLongClickListener(new ItemLongClickListener() {
@Override
public boolean onLongClick(int position) {
Toast.makeText(MainActivity.this, "长按--"+position, Toast.LENGTH_SHORT).show();
return true;
}
});
注意,onLongClick是需要返回true的,不然长按时onItemClick也会被调用.
至此,我们的自定义Adapter就基本完成了,大家可以自己试下
谢谢大家
源码地址 源码