我想要说

RecycleView 它是一个多种视图的总称,它可以展示像我上个月学习到的 ListView、GirdView,还有探索的瀑布流、各种布局混杂在一起。之前在了解 RecycleView 的时候,它也确实能做到。不过最近我在做一个入门的 APP ,发现就是横向的 ListView 无法直接去实现,然后发现 RecycleView 就能够去实现我想要的布局,才发现 RecycleView 的重要性。

不过 RecycleView 的灵活程度也特别的高,视图的复用管理程度也很高,难度可能会高一些,自己实现想要的布局还需要多花费一些时间的学了大概两三周的 Android 发现绝大部分都是需要去自己继承类来重写某些方法,也相当于是对 java 面向对象开发基础部分的一个复习吧。感觉 RecycleView 的内容会有一些多,所以记录的内容也会很多。

目前学习了三种布局方式:LinearLayoutManager、GridLayoutManager 和 StaggeredGridLayoutManager。将会主要以这三种布局管理进行说明。

好啦,现在正式开始吧。

开始的前提

在开始 RecycleView 的学习之旅之前,在 build.gradle 中添加一下依赖

compile 'com.android.support:design:26.0.1'

注意:第一位的 26 对应的是 compileSdkVersion 的版本号。

可能出现的问题

RecyclerView 相关

在重新编译的时候发现找不到,可以参考以下的解决办法

通过修改版本号的后两位应该能够解决此问题。

OOM 问题

OOM,也就是 Out of memory,即内存超出。

当使用图片和 addItemDecoration 方法时可能会导致内存溢出,目前还没有找到具体的解决办法。但是为了学习 RecyclerView 暂时在使用 List 布局时使用

唯一的解决办法就是在使用图片时不使用 addItemDecoration 方法

可能出现的错误如下

02-19 10:50:44.230 4504-4787/top.bestguo.androidlayout E/Bitmap: OOM allocating Bitmap with dimensions 15859853 x 15895097

2 月 20 号,好吧,我知道我哪里有问题了。要正确的获取配置文件的像素单位需要用以下方法获取😭😭😭,这样就不会出现 OOM 的问题了。

getResources().getDimensionPixelOffset(R.dimen.xxx);

这也是导致出现 OOM 的原因,不排除有其它的可能导致 OOM 错误的发生。

AppCompatTextView 无法实例化

在 RecyclerView 的依赖导入进来,可能会爆出以下错误,虽然会出现 AppCompatTextView 无法实例化的情况,在经过测试的时候这并不会影响到程序的正常运行

所以,直接不管它。😜😜😜

RecyclerView 方法介绍

在学习时,我学习到 RecyclerView 的三种方法

setLayoutManager 方法

参数:LayoutManager
该方法用于设置哪种类型的布局,通过实例化布局类的方式来创建布局。

下面将会介绍三种布局管理类。到后面厉害的话,你就直接写一个自己个 LayoutManager 出来,给我们这群懒人用(bushi)

setAdapter 方法

在学习 RecyclerView 之前,我们已经使用过这个方法。并且还是继承了 BaseAdapter 这个方法,然后还自己写了一个 ViewHolder。Adapter 是用来将数据展示到 item 当中的,ViewHolder 就是将每一个 item 中的组件给封装起来,用于后续调用的。

但是,在这里面,我们不能够继承自 BaseAdapter,那要写很多东西。我们需要继承自 RecyclerView.Adapter 这个抽象类,该抽象类在 RecyclerView 的内部,所以是内部类,而且还是带泛型的,该类的泛型必须是继承自 Recycler.ViewHolder 类的。所以我们自己创建 ViewHolder 类时还需继承自该类。

需要实现的方法有这三个:

class XXXAdapter extends RecyclerView.Adapter {
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return null;
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {

    }

    @Override
    public int getItemCount() {
        return 0;
    }
}

继承自 ViewHolder 的时候,写法和之前自己写 ViewHolder 有些差别

private class LinearViewHolder extends RecyclerView.ViewHolder {

    // StatementComponent......

    LinearViewHolder(View itemView) {
        super(itemView);
        // findViewToDo......
    }
}

后续的写法将在示例中详细的解释

setItemDecoration

这个方法用于设置 item 的样式,不过我只学习到利用这个来实现边框🤭🤭🤭,后面在研究的时候将会单独的写一篇日志,做一些好玩的东西出来。但是也正是因为设置了这个才导致了内存不足的问题。害,这个问题后续在解决吧。

布局管理类介绍

LinearLayoutManager

这是一个线性布局的管理类

构造方法

它有两种常用的构造方法

方法 说明
public LinearLayoutManager(Context context) 一个参数,传入当前的 Activity 对象,默认是垂直且顺序的排列下来。
public LinearLayoutManager(Context context, int orientation, boolean reverseLayout) 除了可以传递 Activity 还能指定是水平排列和竖直排列,还能指定是否逆序。

在指定水平或者垂直的方式时,这些布局管理类中都有 2 个常量用来指定是否垂直和水平,水平布局就是按照 XXXManager.HORIZENTAL 的格式,垂直布局就是 XXXManager.VERTICAL 的格式

它的展示效果和之前学习的 ListView 的效果是一样的。不过它比 ListView 更厉害,这可以水平排列的。在没有 RecyclerView 之前需要引入第三方的包来使得 ListView 横向排列,现在有了这个是非常方便的。

GridLayoutManager

这是一个网格布局的管理类,它有两种常用的构造方法。不过查看源码发现它是继承自 LinearLayoutManager 类的,嘻嘻。

构造方法

它也具有两种构造方法。

方法 说明
GridLayoutManager(Context context, int spanCount) 两个参数,传入当前的 Activity 对象并且指定一行几列,默认是垂直且顺序的排列下来。
public GridLayoutManager(Context context, int spanCount, int orientation, boolean reverseLayout) 除了可以传递以上参数以外,还能指定水平和垂直的方式,以及是否逆序

第二个构造方法也就是 LinearLayoutManger 中的大同小异。

StaggeredGridLayoutManager

这是瀑布流的一个管理类,不过它的本意并不是瀑布,而是错开的网格布局。瀑布流在 app 中特别常见,比如我们的淘宝,拼多多这种电商 app 中就使用了这种瀑布流的布局形式。

构造方法

它具有一种构造方法。

方法 说明
StaggeredGridLayoutManager(int spanCount, int orientation) 两个参数,一个指定一行几列,另一个指定排列方式。

不过很遗憾,RecyclerView 并不像 ListView 和 GridView 一样有点击事件。不过没关系,我们可以自己创造一个点击事件。利用接口回调的方式自己写一个点击事件的方法。

示例

以下的示例展示出 LinearLayoutManager、GridLayoutManager 和 StaggeredGridLayoutManager 三种布局的效果

示例1

布局文件

展示线性布局的文件

该 activity 的布局文件的内容非常简单,里面是一个 RecyclerView 标签

对应的 activity_linear_recycler_view.xml 如下

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v7.widget.RecyclerView
        android:background="#eee"
        android:id="@+id/rv_list"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

</RelativeLayout>
item 布局文件

在 item 中,我给它放了一个简单的 TextView 来放置 item 中的样式

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="#fff">

   <TextView
        android:id="@+id/tv_recycle_item"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="10dp"
        android:text="测试文字"
        android:textColor="@color/colorBlack"
        android:textSize="20sp"/>

</LinearLayout>

java 代码

Activity 代码

获取到 RecyclerView 这个组件时,这两个步骤是必须的,顺序可以打乱。

  1. 设置布局管理类
  2. 设置 Adapter

可选步骤
3. 设置偏移量等布局方式

代码如下

public class LinearRecyclerViewActivity extends AppCompatActivity {

    private RecyclerView rv_list;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_linear_recycler_view);

        rv_list = findViewById(R.id.rv_list);
        // 添加布局管理器,这里添加的是线性布局管理器,默认为竖直方向
        // rv_list.setLayoutManager(new LinearLayoutManager(this));
        // 将其设置成水平方向,默认不逆序展示
        rv_list.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
        // 添加一个 Adapter 用于渲染全部的 item
        rv_list.setAdapter(new LinearAdapter(this));
        // 添加一个 Decoration
        rv_list.addItemDecoration(new MyDecoration());

    }
}

以上的代码指定了一个 LinearLayoutManager 的布局管理类,传入当前的实例对象,设置为水平摆放且不设置为逆序。
设置的 Adapter 需要我们继承自 RecyclerView.Adapter 类,该类是 RecyclerView 中的内部类。

在重写 Adapter 之前,我们需要自己写一个 ViewHolder,并且继承自 RecyclerView.ViewHolder。本示例自己写了一个 LinearViewHolder 类

相关的代码如下:

// 自定义 LinearViewHolder
class LinearViewHolder extends RecyclerView.ViewHolder {

    private TextView textView;

    LinearViewHolder(View itemView) {
        super(itemView);
        textView = itemView.findViewById(R.id.tv_recycle_item);
    }

    TextView getTextView() {
        return textView;
    }
}

自定义 ViewHolder 完成之后,此时我们需要继承一个 RecyclerView.Adapter,本示例中自己编写了 LinearAdapter 类。

代码如下:

// 新建的 Adapter
class LinearAdapter extends RecyclerView.Adapter<LinearViewHolder> {

    private Context context;

    LinearAdapter(Context context){
        this.context = context;
    }

    @Override
    public LinearViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        // 将 layout_list_recycler_item.xml 布局文件填充到 RecyclerView 中。
        return new LinearViewHolder(LayoutInflater.from(context).inflate(R.layout.layout_list_recycler_item, parent, false));
    }

    @Override
    public void onBindViewHolder(LinearViewHolder holder, final int position) {
        holder.getTextView().setText("菜单选项_" + position);
        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(LinearRecyclerViewActivity.this, "点击了第 " + position + " 项", Toast.LENGTH_SHORT).show();
            }
        });
    }

    // 设置多少个 item
    @Override
    public int getItemCount() {
        return 20;
    }
}

onCreateViewHolder 方法中将我们自己写好的 LinearViewHolder 类实例化出去即可,同时也能够使用 ViewType 来返回不同的 ViewHolder 实现更加复杂的布局方式。

onBindViewHolder 方法就是给对每一个 item 项进行设置。比如以上的示例,可以设置字体,设置一个点击的监听事件等等。

getItemCount 方法是返回多少个 item。

自己创建了一个 Decoration,在此之前,设置了一个灰色的背景色。通过偏移量将灰色的背景展示出来,从而使用户感受到一种错觉,感觉就是有边框一样。
代码如下:

// 创建一个自己的 Decoration
class MyDecoration extends RecyclerView.ItemDecoration {
    // 设置偏移量
    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        outRect.set(0, 0, 0, getResources().getDimensionPixelOffset(R.dimen.divideHeight));
    }
}

运行效果

示例2

示例 2 和 示例 3 都差不多,继承的 Adapter 和 ViewHolder 都是来自 RecyclerView 中的内部类,很多部分,很多写法其实都是大同小异的。后面的布局方式也就不详细介绍了。

布局文件

展示网格布局的文件

activity_grid_recycler_view.xml

<?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"
    android:background="@color/colorGray">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler_view_grid"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</LinearLayout>
item 布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <ImageView
        android:id="@+id/header"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:background="@color/colorAccent"
        android:scaleType="centerCrop"/>

    <TextView
        android:padding="7dp"
        android:id="@+id/name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="测试文字"
        android:textSize="14dp"
        android:gravity="center"/>

</LinearLayout>

java 代码

Activity 代码

当前示例中使用的 GridLayoutManager 类,传入了当前的 Activity 类,还指定了一行 3 列。如果有逆序和水平的需要,使用 HORIZENTAL 常量、设置成 true 即可

public class GridRecyclerViewActivity extends AppCompatActivity {

    private RecyclerView recyclerView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_grid_recycler_view);

        recyclerView = findViewById(R.id.recycler_view_grid);
        recyclerView.setLayoutManager(new GridLayoutManager(this, 3));
        recyclerView.setAdapter(new GridRecyclerViewAdapter(this));
        recyclerView.addItemDecoration(new RecyclerView.ItemDecoration() {
            @Override
            public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
                super.getItemOffsets(outRect, view, parent, state);
                int gap = getResources().getDimensionPixelOffset(R.dimen.space);
                outRect.set(gap, gap, gap, gap);
            }
        });
    }

}
Adapter 代码
public class GridRecyclerViewAdapter extends RecyclerView.Adapter<GridRecyclerViewAdapter.GridViewHolder> {

    private Context context;

    @Override
    public GridRecyclerViewAdapter.GridViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return new GridViewHolder(LayoutInflater.from(context).inflate(R.layout.layout_grid_item, parent, false));
    }

    @Override
    public void onBindViewHolder(GridRecyclerViewAdapter.GridViewHolder holder, final int position) {
        // 加载网络图片
        Glide.with(context).load("http://www.topacg.com/wp-content/uploads" +
                "/2020/03/frc-11c619718c036bf579c246cdd07e6d77.jpeg").into(holder.imageView);
        holder.textView.setText("灶门祢豆子");
    }

    @Override
    public int getItemCount() {
        return 20;
    }

    class GridViewHolder extends RecyclerView.ViewHolder {

        ImageView imageView;
        TextView textView;

        public GridViewHolder(View itemView) {
            super(itemView);
            imageView = itemView.findViewById(R.id.header);
            textView = itemView.findViewById(R.id.name);
        }
    }
}

运行效果

示例3

布局文件

展示瀑布流布局的文件

activity_pu_recycler_view.xml

<?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"
    android:background="@color/colorGray">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler_view_pu"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</LinearLayout>
item 布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <ImageView
        android:id="@+id/image_asd"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</LinearLayout>

java 代码

Activity 代码

当前示例中使用的 GridLayoutManager 类,指定了一行 3 列,设置成垂直的瀑布流效果。如果有水平的需要,使用 HORIZENTAL 常量即可

public class PuRecyclerViewActivity extends AppCompatActivity {

    private RecyclerView recyclerView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_pu_recycler_view);

        recyclerView = findViewById(R.id.recycler_view_pu);
        recyclerView.setLayoutManager(new StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL));
        recyclerView.setAdapter(new PuAdapter(this));
        recyclerView.addItemDecoration(new RecyclerView.ItemDecoration() {
            @Override
            public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
                super.getItemOffsets(outRect, view, parent, state);
                int gap = getResources().getDimensionPixelOffset(R.dimen.space);
                outRect.set(gap, gap, gap, gap);
            }
        });
    }
}
Adapter 代码
public class PuAdapter extends RecyclerView.Adapter<PuAdapter.PuViewHolder> {

    private Context context;

    public PuAdapter(Context context) {
        this.context = context;
    }

    @Override
    public PuViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return new PuViewHolder(LayoutInflater.from(context).inflate(R.layout.layout_pu_recycler_item, parent, false));
    }

    @Override
    public void onBindViewHolder(PuViewHolder holder, int position) {
        switch (position % 3) {
            case 1:
                Glide.with(context).load("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fattachments.gfan.com%2Fforum%2F201501%2F15%2F1" +
                        "05418z33cyjacahzodgc3.jpg&refer=http%3A%2F%2Fattachments.gfan.com&a" +
                        "pp=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg").into(holder.imageView);
                break;
            case 2:
                Glide.with(context).load("https://ss1.baidu.com/9vo3dSag_xI4khGko9WTAnF" +
                        "6hhy/zhidao/pic/item/4610b912c8fcc3ce1a3113699045d688d53f20f3.jpg").into(holder.imageView);
                break;
            default:
                Glide.with(context).load("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fblog%2F201307%2F" +
                        "23%2F20130723121038_WxiVJ.jpeg&refer=http%3A%2F%2Fb-ssl.duitang.com&app" +
                        "=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg").into(holder.imageView);
                break;
        }
    }

    @Override
    public int getItemCount() {
        return 21;
    }

    static class PuViewHolder extends RecyclerView.ViewHolder {

        private ImageView imageView;

        public PuViewHolder(View itemView) {
            super(itemView);
            imageView = itemView.findViewById(R.id.image_asd);
        }
    }
}

运行效果

事件

上文也提到过 RecyclerView 并没有针对 item 来专门指定事件监听的方法。但是我们可以自己写一个接口,然后去实现这个接口,最后进行接口回调。(其实所谓的接口回调就是利用了面向对象的多态性,子类可以类型提升为父类的类型,父类强制转换成子类。)这样的目的目前是可以省去你在自己的 Adapter 类中频繁的修改代码,同时也降低的代码的耦合性。

现在我们以示例 2 的代码来举例。在下面的代码中,我们需要加载网络图片,那加载网络图片的时候肯定是需要得到它的组件。所以,我们在加载网络图片之后可以设置其点击的监听事件。以下我设置了两个事件,一个短按,一个长按。

事先在内部类中写好两个接口,并且利用该类型声明好成员变量。代码如下

public class GridRecyclerViewAdapter extends RecyclerView.Adapter<GridRecyclerViewAdapter.GridViewHolder> {

    private Context context;
    private ItemOnClickListener clickListener; // 短按事件
    private ItemOnLongClickListener longClickListener; // 长按事件

    // 构造方法,一个用于传递 Activity 类,一个用于传入一个已实现的该接口的点击类,另一个用于传入一个已实现的该接口长按的点击类
    public GridRecyclerViewAdapter(Context context, ItemOnClickListener clickListener, ItemOnLongClickListener longClickListener){
        this.context = context;
        this.clickListener = clickListener;
        this.longClickListener = longClickListener;
    }

    ......

    @Override
    public void onBindViewHolder(GridRecyclerViewAdapter.GridViewHolder holder, final int position) {
        // 加载网络图片
        Glide.with(context).load("http://www.topacg.com/wp-content/uploads" +
                "/2020/03/frc-11c619718c036bf579c246cdd07e6d77.jpeg").into(holder.imageView);
        holder.textView.setText("灶门祢豆子");
        // 短按事件
        holder.imageView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 短按事件接口回调
                clickListener.click(v, position);
            }
        });
        // 长按事件
        holder.imageView.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                // 长按事件接口回调
                return longClickListener.click(v, position);
            }
        });
    }

    ......

    /**
     * 点击事件
     */
    interface ItemOnClickListener{
        void click(View v, int position);
    }

    /**
     * 长按点击事件
     */
    interface ItemOnLongClickListener{
        boolean click(View v, int position);
    }

}

然后在 Activity 类中实现短按和长按的类,将其实例化传入构造方法中

public class GridRecyclerViewActivity extends AppCompatActivity {

    private RecyclerView recyclerView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_grid_recycler_view);

        recyclerView = findViewById(R.id.recycler_view_grid);
        recyclerView.setLayoutManager(new GridLayoutManager(this, 3));
        recyclerView.setAdapter(new GridRecyclerViewAdapter(this, new ClickListener(), new LongClickListener()));
    }

    ......

    // 实现短按的点击事件接口
    private class ClickListener implements GridRecyclerViewAdapter.ItemOnClickListener {
        @Override
        public void click(View v, int position) {
            Toast.makeText(GridRecyclerViewActivity.this, "我是祢豆子 " + (position + 1) + " 号", Toast.LENGTH_SHORT).show();
        }
    }

    // 实现长按的点击事件接口
    private class LongClickListener implements GridRecyclerViewAdapter.ItemOnLongClickListener {
        @Override
        public boolean click(View v, int position) {
            ImageView imageView = v.findViewById(R.id.header);
            Glide.with(GridRecyclerViewActivity.this).load("http://pic.17qq.com/img_qqtouxiang/87450489.jpeg").into(imageView);
            Toast.makeText(GridRecyclerViewActivity.this, "嘻嘻,偷偷抱走" + (position + 1) + "号祢豆子", Toast.LENGTH_SHORT).show();
            return true;
        }
    }

}

最后的效果如下,没错,又是我可爱的祢豆子还有善逸拿来举例😝😝😝

后记

项目的代码都在 AndroidStudy 的 github 仓库中。

本日志对应的代码:点我进入