RecyclerView 的使用

RecyclerView 的使用

ListView 的缺点很明显,只能实现数据纵向滚动效果,拓展性也不够好。为此,Android 提供了一个更强大的滚动控件 —— RecyclerView。ListView 能做到的,RecyclerView 也能做到,而且还能做得更好,所以更推荐使用 RecyclerView。

使用步骤:

  1. 与 ListView 一样,使用 RecyclerView 也需要在布局文件中添加相应的标签,注意它是在 androidx.recyclerview.widget 包下的:

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
    
  2. 根据实际需求自定义一个 Item 布局(以联系人列表为例)。

    <LinearLayout ... >
        <ImageView
            android:id="@+id/iv_contact_item_avatar"
            ... />
    
        <TextView
            android:id="@+id/tv_contact_item_name"
            ... />
    </LinearLayout>
    
  3. 定义数据源实体类 ContactEntity。

  4. 创建自定义适配器类 ContactAdapter,在 ContactAdapter 中定义的一个静态内部类 ViewHolder 并继承自 RecyclerView.ViewHolder,然后创建与父类匹配的构造函数。

    public class ContactAdapter {
    
        static class ViewHolder extends RecyclerView.ViewHolder{
            public ViewHolder(@NonNull View itemView) {
                super(itemView);
            }
        }
    }
    
  5. 让 ContactAdapter 类继承自 RecyclerView.Adapter 并指定泛型为 ContactAdapter.ViewHolder,然后实现其方法。

    public class ContactAdapter extends RecyclerView.Adapter<ContactAdapter.ViewHolder> {
    
        static class ViewHolder extends RecyclerView.ViewHolder {
            // 从名字就可以知道传入参数是子项的 Item 视图
            public ViewHolder(@NonNull View itemView) {
                super(itemView);
            }
        }
    
        /**
        * 用于创建 ViewHolder 实例
        * @param parent 新视图绑定到适配器后将被添加到的视图组,这里指的其实就是 RecyclerView
        * @param viewType 新视图的类型,默认为 0。如果 Item 存在多种表现形式,应重写 getItemViewType() 方法给每种形式指定唯一值
        * @return 一个新的持有给定视图类型视图的 ViewHolder
        */
        @NonNull
        @Override
        public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            return null;
        }
    
        /**
        * 用于对 RecyclerView 子项的数据进行赋值,在每个子项需要显示到屏幕时执行
        * @param holder 需要在对应位置更新显示 Item 内容的 ViewHolder
        * @param position Item 所需数据在适配器的数据中的对应位置
        */
        @Override
        public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
    
        }
    
        /**
        * RecyclerView 子项的数量,返回多少就显示多少
        */
        @Override
        public int getItemCount() {
            return 0;
        }
    }
    

    这是创建好这个自定义适配器类的最初状态。看上去比 ListView 更复杂,但实际比 ListView 更好理解。另外,适配器中还需要定义用于存放数据源的对象和 RecyclerView 所在的上下文 Context,同时也要添加对应的构造函数。

    public class ContactAdapter extends RecyclerView.Adapter<ContactAdapter.ViewHolder> {
    
        private final List<ContactEntity> data;  // 对应数据源 List
        private final LayoutInflater inflater;    // 对应 Context 的布局加载器
    
        public ContactAdapter(Context context, List<ContactEntity> data) {
            this.data = data;
            this.inflater = LayoutInflater.from(context);
        }
    
        // ...
    }
    
  6. 给内部静态类 ViewHolder 添加对应的控件属性,并在构造函数中对其实例化。

    public class ContactAdapter extends RecyclerView.Adapter<ContactAdapter.ViewHolder> {
    
        // ...
    
        static class ViewHolder extends RecyclerView.ViewHolder {
            private ImageView avatar;
            private TextView name;
    
            public ViewHolder(@NonNull View itemView) {
                super(itemView); // 调用父类的构造函数
                avatar = (ImageView) itemView.findViewById(R.id.iv_contact_item_avatar);
                name = (TextView) itemView.findViewById(R.id.tv_list_item_contact);
            }
        }
    }
    
  7. 编写 RecyclerView.Adapter 要求实现的方法。

    public class ContactAdapter extends RecyclerView.Adapter<ContactAdapter.ViewHolder> {
    
        // ...
    
        @NonNull
        @Override
        public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            // 使用布局加载器的 inflate 方法加载 Item 项的布局文件并转化为 View 对象
            // 第一个参数为 Item 布局文件的资源 ID
            // 第二个参数可以给 Item 项指定一个父视图,使用参数中的 parent 即可
            // 第三个参数用来确认是否将 View 添加到父视图中,false 代表只让父视图中的 layout 属性生效,但不会将 View 添加到父视图中
            // 这里不能设置为 true,因为 ItemView 有父视图那将不能再添加到 RecyclerView 中,该页面也会打不开
            View view = inflater.inflate(R.layout.contact_item, parent, false);
            // 使用 ItemView 构造一个 ViewHolder 对象并返回
            return new ViewHolder(view);
        }
    
        @Override
        public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
            ContactEntity entity = data.get(position);
            // 使用 ViewHolder 绑定数据
            holder.avatar.setImageResource(entity.getAvatar());
            holder.name.setText(entity.getName());
        }
    
        @Override
        public int getItemCount() {
            return data.size(); // 直接返回数据源 List 的大小即可
        }
    }
    
  8. 在 Avtivity 中创建一个访问指定 Context 资源的 LinearLayoutManager 布局管理器对象,使用setLayoutManager() 方法将布局管理器设置到 RecyclerView 中。

    // 获取 RecyclerView 对象
    RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
    
    // 创建一个访问指定 Context 资源的布局管理器
    LinearLayoutManager layoutManager = new LinearLayoutManager(this);
    // 将布局管理器对象与 RecyclerView 绑定
    recyclerView.setLayoutManager(layoutManager);
    

    LinearLayoutManager 可以用来指定 RecyclerView 的布局方式为线性布局,同时也可以对该线性布局进行一些设置。

    LinearLayoutManager layoutManager = new LinearLayoutManager(this);
    
    // 设置布局排列方式为水平方向,默认为纵向排列 VERTICAL
    layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
    // 设置布局内 Item 顺序为反向,默认 false。
    layoutManager.setReverseLayout(true);
    
    // 这两个设置也可以直接使用构造函数指定
    LinearLayoutManager layoutManager = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, true);
    
  9. 在 Avtivity 中创建自定义适配器对象,绑定数据源。使用 setAdapter() 方法将适配器设置到 RecyclerView 中。

    // 假装是从网络上获取到的数据
    List<ContactEntity> data = new ArrayList<>();
    for (int i = 0; i < 50; i++) {
        ContactEntity entity = new ContactEntity();
        if (i % 2 == 0) {
            entity.setAvatar(R.mipmap.ic_launcher);
            entity.setName("ic_launcher" + i);
        } else {
            entity.setAvatar(R.mipmap.ic_launcher_round);
            entity.setName("ic_launcher_round" + i);
        }
        data.add(entity);
    }
    // 创建自定义的适配器对象
    ContactAdapter adapter = new ContactAdapter(this, data);
    // 将适配器对象与 RecyclerView 绑定
    recyclerView.setAdapter(adapter);
    
  10. 在 ViewHolder 中设置行为响应事件。

    /**
    * 请勿在 onBindViewHolder 方法中设置事件监听
    * 这里采用让 ViewHolder 去实现 View.OnClickListener 接口的方式设置监听事件
    */
    static class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
        private ImageView avatar;
        private TextView name;
    
        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            avatar = (ImageView) itemView.findViewById(R.id.iv_contact_item_avatar);
            name = (TextView) itemView.findViewById(R.id.tv_list_item_contact);
            // 在 ViewHolder 中对 Item 或其内部控件设置事件监听
            itemView.setOnClickListener(this);
            avatar.setOnClickListener(this);
            name.setOnClickListener(this);
        }
    
        @Override
        public void onClick(View v) {
            switch (v.getId()) {
                case R.id.contact_item:
                    // TODO
                    break;
                case R.id.iv_contact_item_avatar: break;
                case R.id.tv_list_item_contact: break;
            }
        }
    }
    

为什么 RecyclerView 中的 ItemView 没有使用 setTag() 方法?

创建 RecyclerView.ViewHolder 对象时需要传入一个 View,这个 View 就是 Item 视图。从 RecyclerView.ViewHolder 的源码可以看到,其实 ViewHolder 对象不仅缓存了 ItemView 中的控件 View 实例,还缓存了这个 ItemView。这样就没有必要使用 setTag() 方法了,也不需要像 ListView 那样回收复用 ItemView,直接回收复用 ViewHolder 会更好,事实上 RecyclerView 缓存机制也的确是这样。

RecyclerView.LayoutManager

LayoutManager 负责测量和定位 RecyclerView 中的 Item 视图,并确定何时回收用户不再可见的 Item 视图的策略。 通过更改 LayoutManager,可以使用 RecyclerView 来实现标准的垂直滚动列表、统一网格、交错网格、水平滚动集合等。

如果 LayoutManager 指定了默认构造函数或带有签名(Context, AttributeSet, int, int)的构造函数,RecyclerView 将在布局加载时实例化并设置 LayoutManager。 然后可以从 getProperties(Context, AttributeSet, int, int) 获得大多数使用的属性。 如果 LayoutManager 指定了两个构造函数,则非默认构造函数将优先。

除了 LinearLayoutManager,RecyclerView 还提供了两种内置的布局管理器,GridLayoutManager 和 StaggeredGridLayoutManager。GridLayoutManager 可用于实现网格布局,而 StaggeredGridLayoutManager 可用于实现瀑布流布局。

// 第一个参数为当前 Context
// 第二个参数为网格的行数或列数
// 第三个参数为布局排列方向
// 第四个参数为是否反转布局
GridLayoutManager layoutManager = new GridLayoutManager(this, 3, RecyclerView.HORIZONTAL, true);

// 第一个参数为瀑布流的行数或列数
// 第二个参数为布局排列方向
StaggeredGridLayoutManager layoutManager = new StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL);

RecyclerView 设置分割线 —— addItemDecoration() 方法

RecyclerView 成功显示出来了,但现在的显示效果看上去怪怪的。对比一下 ListView 就会发现 RecyclerView 子项之间是没有分割线的,而 ListView 自带分割线,还有两个属性 android:dividerandroid:dividerHeight 可以用来自定义分割线。

那 RecyclerView 中有这两个或类似的属性吗?答案是没有。当然,可以专门在 Item 布局中的适当地方添加一个高度/宽度为 1 的带背景的 View 作为分割线,如果 Item 有明显背景也可以通过给 Item 的最外层布局设置一个 margin 值。这两种方法是可以实现效果的,但是不够优雅。

事实上,RecyclerView 提供了一个添加装饰的方法——addItemDecoration(RecyclerView.ItemDecoration),该方法传入的参数类型是一个抽象类。

public abstract static class ItemDecoration {
    // 调用此方法绘制的任何内容都会在 ItemView 绘制之前进行绘制,因此将会显示在 ItemView 的背后
    public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull State state) {
    }

    // 调用此方法绘制的任何内容都会在 ItemView 绘制之前进行绘制,因此将会显示在 ItemView 的表面
    public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent,
            @NonNull State state) {
    }

    // 通过 outRect.set() 为每个 Item 设置一定的偏移量,可以实现类似 padding 和 margin 的效果,主要用于绘制 Decorator
    public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
            @NonNull RecyclerView parent, @NonNull State state) {
    }
}

官方有一个自带的实现类——DividerItemDecoration,但只能用于 LinearLayoutManager 的布局方式。使用起来很简单,指定上下文对象和分割线的方向构造其实例,再作为参数传入 addItemDecoration() 方法中即可实现分割线。

// 第一个参数是上下文,第二个参数是分割线对应 RecyclerView 的方向
DividerItemDecoration decoration = new DividerItemDecoration(this, DividerItemDecoration.VERTICAL);

// 默认使用的是系统自带分割线样式 android.R.attr.listDivider
// 可以使用 setDrawable() 修改样式
Drawable drawable = AppCompatResources.getDrawable(this, R.drawable.divider_diy);
if (drawable != null) {
    decoration.setDrawable(drawable);
}

recyclerView.addItemDecoration(decoration);

// 也可以 setOrientation() 修改分割线方向
// decoration.setOrientation(DividerItemDecoration.HORIZONTAL);
// divider_diy.xml
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="line">
    <stroke
        android:width="2dp"
        android:color="@android:color/darker_gray" />
    <size android:height="2dp" />
</shape>

当然也可以选择自己实现 RecyclerView.ItemDecoration 抽象类。

RecyclerView 设置事件监听 —— addOnItemTouchListener() 方法

该方法的官方文档说明:添加一个 RecyclerView.OnItemTouchListener 以在触摸事件被分派到子视图或此视图的标准滚动行为之前拦截它们。

前面通过让 ViewHolder 去实现 View.OnClickListener 已经实现了在 RecyclerView 中设置行为响应事件,但其实 RecyclerView 本身并没有实现 OnItemClick 等事件监听的功能。不过 RecyclerView 提供了一个基本事件监听接口 OnItemTouchListener,同时官方也给了一个基本实现 SimpleOnItemTouchListener,但实际上只是空实现:

public static class SimpleOnItemTouchListener implements RecyclerView.OnItemTouchListener {

    /** 当返回 true 时,MotionEvent 将被拦截在这一层,不会将 MotionEvent 传递给下一层 View。 */
    @Override
    public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
        return false;
    }
    /** 收到了onInterceptTouchEvent 的处理后,无论其返回什么,都会调用 onTouchEvent 进行处理。 */
    @Override
    public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
    }

    /** 当有子 View 反对拦截 MotionEvent 的时候,会调用该方法。 */
    @Override
    public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    }
}

官方推荐实现 RecyclerView.OnItemTouchListener 去自定义触摸事件,这个接口实质是利用了 View 的事件分发与拦截机制。大体思路:在 onInterceptTouchEvent() 中通过 rv.findChildViewUnder(e.getX(), e.getY()) 获取当前触摸位置对应的 ItemView,根据触摸状态决定是否要把事件拦截,在拦截的时候同时添加一个回调方法,让自定义的监听器接口在这里得到回调。

public class RecyclerViewTouchListener implements RecyclerView.OnItemTouchListener {
    // 定义内部接口属性
    private final onItemClickListener onItemClickListener;
    // 构造函数
    public RecyclerViewTouchListener(RecyclerViewTouchListener.onItemClickListener onItemClickListener) {
        this.onItemClickListener = onItemClickListener;
    }

    @Override
    public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
        // 获取当前触摸位置下的 ItemView
        View child = rv.findChildViewUnder(e.getX(), e.getY());
        if (child != null) {
            //  获取子视图在适配器数据中的位置
            int position = rv.getChildAdapterPosition(child);
            // 获取子视图在 RecyclerView 中位置
            int id = rv.getChildLayoutPosition(child);
            // 调用内部接口的点击方法,通过接口让外部实现具体行为
            onItemClickListener.onItemClick(rv, child, position, id);
            // 也可以获取与子视图对应的 ViewHolder
            // RecyclerView.ViewHolder viewHolder = rv.getChildViewHolder(child);
            return true; // 返回 true 对触摸事件进行拦截
        }
        return false;
    }

    @Override
    public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
    }

    @Override
    public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    }

    // 定义内部接口
    public interface onItemClickListener {
        // 定义响应点击事件方法,传入所需参数
        // 这里模仿 ListView 传入父视图,子视图,子视图在适配器数据源中的位置,子视图在父视图中的位置 ID
        void onItemClick(RecyclerView recyclerView, View itemView, int position, long id);
    }
}

定义好 RecyclerView.OnItemTouchListener 实现类后,就可以在对应 Activity 中使用 addOnItemTouchListener() 给 RecyclerView 添加触摸事件监听器。

recyclerView.addOnItemTouchListener(new RecyclerViewTouchListener(new RecyclerViewTouchListener.onItemClickListener() {
    @Override
    public void onItemClick(RecyclerView recyclerView, View itemView, int position, long id) {
        // TODO
    }
}));

使用上面的方式可以达到给 RecyclerView 子项添加触摸事件,但并没有动画,同时还不能处理子项中某一个控件的响应事件。

### 使用RecyclerView实现列表视图 #### 创建并配置RecyclerView实例 为了在Android应用程序中使用`RecyclerView`显示列表视图,首先需要获取布局文件中的`RecyclerView`对象。这可以通过调用`findViewById()`方法完成: ```java RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view); ``` 接着要为`RecyclerView`指定一个适配器(Adapter),该适配器负责提供数据给各个子项(ItemView)[^1]。 #### 设置LayoutManager 为了让`RecyclerView`知道如何排列它的孩子视图(Child View), 需要设置一个`LayoutManager`. 常见的选择有线性布局(`LinearLayoutManager`)、网格布局(`GridLayoutManager`)以及瀑布流布局(`StaggeredGridLayoutManager`). 下面是一个简单的例子展示了如何应用线性布局管理器: ```java recyclerView.setLayoutManager(new LinearLayoutManager(this)); ``` #### 定义ViewHolder与Adapter 对于每一个条目来说, `RecyclerView`利用了一个叫做`ViewHolder`的设计模式来提高性能. 这里定义了持有单个item UI元素引用的静态内部类. 之后还需要编写自定义的`Adapter`, 继承自`RecyclerView.Adapter<MyAdapter.MyViewHolder>`, 并重写几个抽象的方法如`onCreateViewHolder()`,`onBindViewHolder()`, 和`getItemCount()` 来填充实际的数据到viewholder 中去[^3]. #### 数据绑定 当设置了adapter 后就可以准备向其中添加数据源了。一般情况下我们会有一个List 或者Array 类型的数据集作为输入传递给adapter 的构造函数,在每次更新界面的时候只需要刷新这个集合再通知adapter 即可。 ```java myDataset.add("New Item"); mAdapter.notifyItemInserted(myDataset.size()-1); ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿咩AmieVastness

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值