从0系统学Android--3.6 RecyclerView

2019-12-26 15:57:04 浏览数 (1)

本系列持续更新中…. 参考《第一行代码》

首先说明一点昨天发了一篇关于 ListView 的使用入门文章,得到了大家的一致调侃。我的想法是这样的,虽然现在 ListView 已经被 RecyclerView 替代了,但是本系列作为入门系列,力求内容完整!还是有必要提及一下这么重要的控件的,谁能保证老的项目没有 ListView 呢?

作为入门,一个 Android 开发者不会使用或者根本没有听说过 ListView 说不过去把!

3.6 更强大的滚动控件---RecyclerView

ListView 虽然很强大,但是缺点也不少,比如如果我们刚刚不给它优化的话,效率就会很低。而且 ListView 的扩展性不好,只能实现数据的纵向滚动效果,如果想要实现横向滚动的话就做不到了。

为此 Android 提供了更为强大的控件--RecyclerView。ListView 能够实现的功能它都可以实现,而且还优化了 ListView 的那些不足。还有许多功能是 ListView 所做不多的,就比如横向滑动。

Android 官方更加推荐使用 RecyclerView

3.6.1 RecyclerView 的基本用法

如果你没有使用 androidx 的话,使用 RecyclerView 也是需要引入支持库。

complie 'com.android.support.recyclerview-v7:24.2.1'

现在都推荐使用 androidx 库了,可以这样引入

compile 'androidx.recyclerview:recyclerview:1.0.0'

然后在 xml 中添加 RecyclerView

代码语言:javascript复制
<?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:orientation="vertical">
    <androidx.recyclerview.widget.RecyclerView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@ id/rlv"/>

</LinearLayout>

下面我就来实现和上面的例子一样的效果。

下面需要给 RecyclerView 准备一个适配器,这个适配器需要继承 RecyclerView.Adapter ,并且将泛型指定为 FruitAdapter.ViewHodler 其中 ViewHolder 是我们在 FruitAdapter 中定义的一个内部类。代码如下

代码语言:javascript复制
public class FruitAdapter2 extends RecyclerView.Adapter<FruitAdapter2.ViewHolder> {
    private List<Fruit> listFruit;

    public FruitAdapter2(List<Fruit> listFruit){
        this.listFruit = listFruit;
    }
    static class ViewHolder extends RecyclerView.ViewHolder{
        ImageView iv;
        TextView tv;
        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            iv = itemView.findViewById(R.id.iv);
            tv = itemView.findViewById(R.id.tv_name);
        }
    }

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

    @Override
    public void onBindViewHolder(@NonNull FruitAdapter2.ViewHolder holder, int position) {
        holder.iv.setImageResource(listFruit.get(position).getImgId());
        holder.tv.setText(listFruit.get(position).getName());

    }

    @Override
    public int getItemCount() {
        return listFruit.size();
    }
}

代码看上很长,其实很简单,容易理解。首先在内部定义了一个类 ViewHolder 这个类是继承自 RecyclerView.ViewHolder 的,在构造方法中需要传入一个 View 参数,这个参数就是我们 RecyclerView 的子项的最外层的布局,然后就可以通过 findViewById() 方法来获取内部的各个控件。

FruitAdapter2 也有一个构造方法,需要传入用于展示的数据源,后续在这个数据源的基础上进行。

FruitAdapter2 继承自 RecyclerView.Adaprer 就必须要实现三个方法onCreateViewHolder()、onBindViewHolder() 和 getItemCount()

  • onCreateViewHolder() 从方法名也很容易可以得出,是用来创建 ViewHolder 的,把此方法内创建的 ViewHolder 通过 return 返回。
  • onBindViewHolder() 就是用于对 RecyclerView 的子项数据绑定到 ViewHolder 上面,这个方法会在每个子项被滚动到屏幕内的时候执行,通过这里的 position 参数得到当前子项的数据,然后设置到 ViewHolder 中就可以了。
  • getItemCount() 方法很简单就告诉 RecyclerView 一共有多少子项,直接返回数据源的长度就可以了。

适配器创建好,就可以使用 RecyclerView 了。

代码语言:javascript复制
public class RecyclerViewActivity extends AppCompatActivity {

    List<Fruit> list;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 初始化数据源
        initData();
        setContentView(R.layout.activity_recyclerview);
        RecyclerView recyclerView = findViewById(R.id.rlv);
        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        recyclerView.setLayoutManager(layoutManager);
        FruitAdapter2 fruitAdapter2 = new FruitAdapter2(list);
        recyclerView.setAdapter(fruitAdapter2);

    }
    public void initData(){
        list  = new ArrayList<>();
        for (int i=0;i<30;i  ){
            Fruit fruit = new Fruit("水果" i,R.mipmap.ic_launcher);
            list.add(fruit);
        }
    }

}

首先获取了 RecyclerView 的实例,然后创建了一个 LinearlayoutManager 的对象,并将它设置到了 RecyclerView 中。LinearLayoutManager 用于指定 RecyclerView 的布局方式,是线性布局的意思,可以实现和 ListView 同样的效果。然后创建了适配器,将数据传入到适配器中,调用 RecyclerView 的 setAdapter 来完成适配器设置,让 RecyclerView 和 数据产生联系。

可以看到 RecyclerView 实现了和 ListView一样的效果,虽然代码量没有明显减少,但是逻辑更加清晰了。这只是 RecyclerView 的最基本的用法而已,下面来一些 ListView 所实现不了的功能。

3.6.2 实现横向滚动和瀑布流布局

Listview 的可扩展性不好,只能实现纵向滚动,如果想要横向滚动的话 ListView 就做不到了。下面用 RecyclerView 来实现横向滚动。

首先对子项布局进行修改一下,目前的布局是水平排列的,不适合水平滚动。

代码语言:javascript复制
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="100dp"
    android:layout_height="wrap_content"
    android:orientation="vertical">
    <ImageView
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="10dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@ id/iv"/>
    <TextView
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="10dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@ id/tv_name"
        android:layout_marginLeft="10dp"/>
</LinearLayout>

然后修改 MainActivity

代码语言:javascript复制
// 只需要插入这么一句就可以了  
layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);

调用 LinearLayoutManagersetOrientation() 方法来设置布局的排列方向,默认是纵向排列的。

为什么 ListView 很难实现的效果在 RecyclerView 上这么轻松就实现了呢?

主要原因是RecyclerView 出色的设计,ListView 的布局排列是又自身去管理的,而 RecyclerView 的布局排列交给了 LayoutManager ,LayoutManager 有一套可扩展布局排列接口,子类只要按照接口的规范来实现,就可以制定各种不同方式的排列布局了。

除了 LinearLayoutManger ,RecyclerView 还提供了 GridLayoutManager 和 StaggeredGrildLayoutManager 这两种内置的布局排列方式。

GridLayoutManager 可以实现网格布局

StraggeredGridLayoutManager 可以实现瀑布流布局

这里就来实现一下瀑布流

代码语言:javascript复制
        RecyclerView recyclerView = findViewById(R.id.rlv);
//        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
//        layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
        StaggeredGridLayoutManager staggeredGridLayoutManager = new StaggeredGridLayoutManager(3,StaggeredGridLayoutManager.VERTICAL);
//        recyclerView.setLayoutManager(layoutManager);
        recyclerView.setLayoutManager(staggeredGridLayoutManager);

        FruitAdapter2 fruitAdapter2 = new FruitAdapter2(list);
        recyclerView.setAdapter(fruitAdapter2);

别的地方不改变,只需要在代码中 new 一个瀑布流的布局管理器就可以了,里面穿的参数分别是 3 代表会把不会分成 3 列,第二个参数传入的是布局的排列方向,对于瀑布流来说一般就是传入 VERTICAL,水平方向没有什么意义。是不是很简单啊。来看一下效果。

你可以看到和网格布局没有什么区别啊,不要着急那是因为我们数据的原因,导致了所有的子项高度都一样看上去就和网络布局没有什么区别了。

下面我们来改变数据。

代码语言:javascript复制
    public void initData(){
        list  = new ArrayList<>();
        Random random = new Random();
        for (int i=0;i<30;i  ){
            int length =random.nextInt(20) 5;
            StringBuilder stringBuilder  = new StringBuilder();
            for (int j =0;j<length;j  ){
                stringBuilder.append("水果").append(i).append("  ").append(length);
            }
            Fruit fruit = new Fruit(stringBuilder.toString(),R.mipmap.ic_launcher);
            list.add(fruit);
        }
    }

这里我们巧妙的使用了 Random 让它随机产生数字,用来让 name 的数据变得不一样,从而出现高度不同。

需要注意的:

在使用瀑布布局管理器的时候,子项目的布局的宽度是由分的列数来决定的。也就是说如你的子项布局的宽度设置了 match_parent 的话,StraggeredGridLayoutManager 会自动给它按照比例缩小,而不是截取。比如你给它传入了 3 列,则会缩小成 1 行可以容纳 3 个子项View 的宽度。当然如果你的子项布局的宽度设置成很小,那么就不会缩小了,效果就是子View 和 子 View 之间有很大的空隙,导致不美观。

一般做法就是将子View 的宽度设置为 match_parent 然后设置 margin 来让子项之间互留一点间距。

3.6.3 RecyclerView 的点击事件

RecyclerView 并没有像 ListView 一样提供类似 setOnItemClickListener() 的注册监听的方法。需要我们自己给子项具体的 View 去注册点击事件,相比 ListView来说实现起来复杂一些。

那么你会说了,既然 RecyclerView 这个强大了,各个方面都优于 ListView,但是为什么点击事件没有处理好呢?

其实不是这样的,ListView 的点击事件上的处理并不是那么好,setOnItemClickListener() 方法注册的只是子项的点击事件,如果我想点击子线里面的某一个按钮,通过这种方式就没法直接实现了,虽然 ListView 也可以通过在适配器中做到,但是实现起来就比较麻烦了。为此 RecyclerView 干脆把子项点击事件的监听器给去除了,所有的点击事件都由具体的 View 去注册,更加灵活了。

代码语言:javascript复制
static class ViewHolder extends RecyclerView.ViewHolder{
        View view;
        ImageView iv;
        TextView tv;
        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            iv = itemView.findViewById(R.id.iv);
            tv = itemView.findViewById(R.id.tv_name);
            view = itemView;
        }
    }

    @NonNull
    @Override
    public FruitAdapter2.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fuit_item,parent,false);
        final ViewHolder viewHolder = new ViewHolder(view);
        viewHolder.view.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                int position = viewHolder.getAdapterPosition();
                Fruit fruit = listFruit.get(position);
                Toast.makeText(v.getContext(),fruit.getName(),Toast.LENGTH_SHORT).show();
            }
        });
        viewHolder.iv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(v.getContext(),"img",Toast.LENGTH_SHORT).show();
            }
        });

        return viewHolder;
    }

注意:

为了优化性能,注册点击事件的时候一定要在 onCreateViewHolder 方法中进行。通过 ViewHolder 的 getAdapaterPositon() 我们就清楚的指定我们点击的 View 在 Adapter 中的位置了。

0 人点赞