RecyclerView使用

简单说明

为什么有了ListView还需要RecyclerView?

主要有这几个原因:

  1. 只支持竖直方向上的列表形状排列,不支持横向、网格(GridView)、瀑布流等其它排列方式,不灵活,适用性不广。
  2. 在缓存机制不是很好,还有一些优化的空间。

RecyclerView相比于ListView的优缺点:

  1. 更灵活,适用性更广。
  2. 更方便添加Item的动画,分割线等
  3. 支持局部刷新和定向刷新
  4. 使用起来没有ListView简单
  5. 不支持Item的点击事件,需要自己处理。

一般使用

RecyclerView的一般使用和ListView在总体上差不多。区别主要有以下几点:

  1. 必须添加一个布局管理器来声明列表中的Item的排列方式
  2. 如果需要分割线,可以单独添加,且名字叫Item装饰:ItemDecoration。
  3. Adapter的创建,相对于ListView更规范化,且稍微复杂一些。
RecyclerView rv = findViewById(R.id.rv_second);
// 设置布局管理器,这里是最简单的竖直线性排列的布局
rv.setLayoutManager(new LinearLayoutManager(this));
// 设置Adapter
rv.setAdapter(adapter);
// 设置分割线
rv.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));

Adapter的使用

1. 整体结构

Adatper需要继承RecyclerView.Adapter类,且需要额外增加一个ViewHolder类继承RecyclerView.ViewHolder。然后将自定义的Holder作为Adapter的泛型类型。关于Holder的处理,放到后面再说

public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.Holder> {
    
    static class Holder extends RecyclerView.ViewHolder {
        
    }
}

2. 复写方法

Adapter需要复写父类的三个抽象方法,分别如下。

    // 直接返回数量,固定式写法
    @Override
    public int getItemCount() {
        return data == null ? 0 : data.size();
    }

    // 创建ItemView,将ItemView和Holder绑定,当然也要绑定itemView中的控件
    onCreateViewHolder()
        
    // 在这里处理数据,将position对应的JavaBean对象中的数据设置进holder.xx控件中
    onBindViewHolder()

其实写法和ListView 的Adapter在优化之后的写法是一样的。只是将ListView.Adapter中的getView()方法中的代码分开放到onCreateViewHolder(),Holder类,和onBindViewHolder()方法三部分中去。

ListView.Adapter的getView方法和RecyclerView.Adapter的onCreateViewHolder、onBindViewHolder方法的比较:

ListView的Adapter

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    // 对convertView和view中的控件的复用
    ViewHolder holder;
    if (convertView == null) {
        convertView = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.item_fruit_first, parent, false);
        holder = new ViewHolder();
        holder.iv = convertView.findViewById(R.id.iv_fruit);
        holder.tv = convertView.findViewById(R.id.tv_fruit);
        convertView.setTag(holder);
    } else {
        holder = (ViewHolder) convertView.getTag();
    }
    // 下面是将数据设置进具体的convertView的控件中
    Fruit fruit = data.get(position);
    holder.iv.setImageResource(fruit.getDrawableResId());
    holder.tv.setText(fruit.getName());
    return convertView;
}

RecyclerView的Adapter

/**
 * 创建ItemView,将ItemView和Holder绑定,当然也要绑定itemView中的控件
 *
 * @param parent   就是RecyclerView对象本身
 * @param viewType 如果有多种布局,根据这个viewType的值不同,要加载不同的布局
 * @return 在onBindViewHolder方法中使用的Holder对象
 */
@NonNull
@Override
public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    View view = LayoutInflater.from(parent.getContext())
            .inflate(R.layout.item_fruit_first, parent, false);
    // 这里将view和holder的绑定方式与ListViewAdapter不同,不再是使用Tag的方式了,而是将view作为holder的成员变量
    Holder holder = new Holder(view);
    holder.iv = view.findViewById(R.id.iv_fruit);
    holder.tv = view.findViewById(R.id.tv_fruit);
    return holder;
}

/**
 * 在这里处理数据,将position对应的JavaBean对象中的数据设置进holder.xx控件中
 *
 * @param holder   就是或新建,或复用的Holder对象
 * @param position item对应的索引
 */
@Override
public void onBindViewHolder(@NonNull Holder holder, int position) {
    Fruit fruit = data.get(position);
    holder.iv.setImageResource(fruit.getDrawableResId());
    holder.tv.setText(fruit.getName());
}

3. 不要忘记将数据设置进Adapter中

private ArrayList<Fruit> data;

public FruitAdapter(ArrayList<Fruit> data) {
    this.data = data;
}

4. ViewHolder

与ListView中的Holder不同,RecyclerViewAdapter中的Holder需要继承RecyclerView.ViewHolder,且因为RecyclerView.ViewHolder只有一个要itemView作为参数的构造方法,所以我们自定义的ViewHolder也要必须添加构造方法。

/**
 * 因为父类只有一个需要view参数的构造方法,所以Holder类必须添加一个构造方法,能够调用父类的这个构造方法
 */
static class Holder extends RecyclerView.ViewHolder {

    ImageView iv;
    TextView tv;

    /**
     * 构造方法,将itemView与holder对象绑定,并调用父类的有itemView作为参数的构造方法
     *
     * @param itemView 就是Adapter的onCreateViewHolder方法创建的View
     */
    public Holder(@NonNull View itemView) {
        super(itemView);
    }
}

5. 完整代码如下

/**
 * 1. 一般情况下我们同时需要自定义Holder类继承Rv中的ViewHolder,然后将Holder类设置为Adapter中的泛型
 * 2. 继承Rv.Adapter类之后,需要复写3个抽象方法
 */
public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.Holder> {

    private ArrayList<Fruit> data;

    public FruitAdapter(ArrayList<Fruit> data) {
        this.data = data;
    }

    /**
     * 和ListView一样,也是获取列表需要渲染加载的数据的数量
     */
    @Override
    public int getItemCount() {
        return data == null ? 0 : data.size();
    }


    /**
     * 创建ItemView,将ItemView和Holder绑定,当然也要绑定itemView中的控件
     *
     * @param parent   就是RecyclerView对象本身
     * @param viewType 如果有多种布局,根据这个viewType的值不同,要加载不同的布局
     * @return 在onBindViewHolder方法中使用的Holder对象
     */
    @NonNull
    @Override
    public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.item_fruit_first, parent, false);
        Holder holder = new Holder(view);
        holder.iv = view.findViewById(R.id.iv_fruit_first);
        holder.tv = view.findViewById(R.id.tv_fruit_first);
        return holder;
    }

    /**
     * 在这里处理数据,将position对应的JavaBean对象中的数据设置进holder.xx控件中
     *
     * @param holder   就是或新建,或复用的Holder对象
     * @param position item对应的索引
     */
    @Override
    public void onBindViewHolder(@NonNull Holder holder, int position) {
        Fruit fruit = data.get(position);
        holder.iv.setImageResource(fruit.getDrawableResId());
        holder.tv.setText(fruit.getName());
    }

    /**
     * 因为父类只有一个需要view参数的构造方法,所以Holder类必须添加一个构造方法,能够调用父类的这个构造方法
     */
    static class Holder extends RecyclerView.ViewHolder {

        ImageView iv;
        TextView tv;

        /**
         * 构造方法,将itemView与holder对象绑定,并调用父类的有itemView作为参数的构造方法
         *
         * @param itemView 就是Adapter的onCreateViewHolder方法创建的View
         */
        public Holder(@NonNull View itemView) {
            super(itemView);
        }
    }
}

其它布局:LayoutManager

常用的布局管理器有2种,分别是线性和网格。可以达到普通的列表、横向列表、网格状、瀑布流布局的效果。

LinearLayoutManager

最常用的就是LinearLayoutManager。

下面就是创建一个最普通的类似ListView的布局管理器。

LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
rv.setLayoutManager(linearLayoutManager);

LinearLayoutManager还有一个常用的构造方法。

  • 参数orientation表示排列方向
  • 参数reverseLayout表示是否倒序展示数据
/**
 * @param context       Current context, will be used to access resources.
 * @param orientation   Layout orientation. Should be {@link #HORIZONTAL} or {@link
 *                      #VERTICAL}.
 * @param reverseLayout When set to true, layouts from end to start.
 */
public LinearLayoutManager(Context context, @RecyclerView.Orientation int orientation,
        boolean reverseLayout) {
    setOrientation(orientation);
    setReverseLayout(reverseLayout);
}

因此我们如果想要显示水平方向列表,直接使用这个构造方法即可。下面的代码,再将itemView的宽度不设置为match_parent,就可以实现水平方向的排列。当然就算itemView的宽度是match_parent,也是水平排列的列表,但是每个item的宽度就会都占用屏幕的宽度了。

LinearLayoutManager linearLayoutManager =
        new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false);
rv.setLayoutManager(linearLayoutManager);

StaggeredGridLayoutManager

如果想要网格状的布局,就可以使用StaggeredGridLayoutManager来完成。

和LinearLayoutManager不同,我们常用的StaggeredGridLayoutManager构造方法是不传Context,而必须指定方向和行列数的构造方法。

/**
 * Creates a StaggeredGridLayoutManager with given parameters.
 *
 * @param spanCount   If orientation is vertical, spanCount is number of columns. If
 *                    orientation is horizontal, spanCount is number of rows.
 * @param orientation {@link #VERTICAL} or {@link #HORIZONTAL}
 */
public StaggeredGridLayoutManager(int spanCount, int orientation) {
    mOrientation = orientation;
    setSpanCount(spanCount);
    mLayoutState = new LayoutState();
    createOrientationHelpers();
}

StaggeredGridLayoutManager的使用如下,不过要注意itemView的宽高的设置。

// 创建一个竖直方向排列,一共只有2列的网格状布局管理器
StaggeredGridLayoutManager staggeredGridLayoutManager
        = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
rv.setLayoutManager(staggeredGridLayoutManager);
// 创建一个水平方向排列,一共只有2行的网格状布局管理器
StaggeredGridLayoutManager staggeredGridLayoutManager
        = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.HORIZONTAL);
rv.setLayoutManager(staggeredGridLayoutManager);

瀑布流

瀑布流就是控件的宽度或者高度不等的网格型布局。

瀑布流的实现很简单,就是在StaggeredGridLayoutManager的基础之上,更改ItemView的高度或者宽度即可。只是这里注意,要使用控件的LayoutParams来修改宽高等尺寸属性。

/**
 * 将数据设置进itemView中的控件,也就是ViewHolder中的成员变量。
 */
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
    // 不能通过view直接设置它的宽高,需要通过一个LayoutParams的成员变量来修改宽高。
    ViewGroup.LayoutParams layoutParams = holder.itemView.getLayoutParams();
    // 想办法让itemView的高度在某个范围内变化
    layoutParams.height = DensityUtil.dip2px(holder.itemView.getContext(), 60) +
                DensityUtil.dip2px(holder.itemView.getContext(), new Random().nextInt(60));
    // 不要忘记设置数据
    Fruit fruit = data.get(position);
    holder.iv.setImageResource(fruit.getDrawableRes());
    holder.tv.setText(fruit.getName());
}

dp转px的工具方法也很简单。获取系统的屏幕像素密度,乘以要dp数值,就是像素值。

public class DensityUtil {

    /**
     * 根据手机的分辨率从 dp 的单位 转成为 px(像素)
     */
    public static int dip2px(Context context, float dpValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }

    /**
     * 根据手机的分辨率从 px(像素) 的单位 转成为 dp
     */
    public static int px2dip(Context context, float pxValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (pxValue / scale + 0.5f);
    }

}

分割线/装饰:ItemDecoration

分割线

一般情况下,我们在RecyclerView中添加分割线的方式是将线画在itemView中,然后根据条件决定线是否显示。

如果想要itemView之间有间距,我们一般也是用在itemView中添加margin的方式完成。

但是如果有比较复杂的的对于itemView分割线、背景样式等的处理的时候,我们就需要使用ItemDecoration来完成了。

ItemDecoration

案例完整代码:

public class FruitDecoration extends RecyclerView.ItemDecoration {

    private final Paint mPaint;

    public FruitDecoration(Context context) {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(context.getResources().getColor(R.color.purple_200));
    }

    /**
     * 可以实现类似于Padding的效果。就是控制各个Item之间的间距等。
     *
     * @param outRect 就是ItemView的四周的边距。就是系统会根据outRect的值来扩展item的区域。
     * @param view    当前ItemView
     * @param parent  就是RecyclerView
     * @param state   存储一些RecyclerView的状态等,用的不多
     */
    @Override
    public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        /**
         * 这里有两个获取position的方法。他们一般情况下没有区别。
         * 仅在RecyclerView刷新布局后,layoutPosition会晚adapterPosition大约16ms。
         * 因为绘制完成之后,layoutPosition才有正确的值,所以我们一般用adapterPosition就好。
         *
         * 只有在使用findViewHolderForLayoutPosition获取当前item的ViewHolder时,用layoutPosition才更好,因为此时layoutPosition 和用户在屏幕上看到的一定是一样的
         */
        int position = parent.getChildAdapterPosition(view);
        int layoutPosition = parent.getChildLayoutPosition(view);
        /**
         * 这里注意,下面两个count的值的不同。
         * viewCount是当前RecyclerView中的itemView的数量。并不是数据的数量,Adapter中的getItemCount才是
         * 因为RecyclerView不会一次性创建所以的itemView,而是会进行view的复用。
         */
        int viewCount = parent.getChildCount();
        int childCount = parent.getAdapter().getItemCount();
        if (position == 0) {
            outRect.bottom = 20;
        } else if (position == childCount - 1) {
            outRect.top = 20;
        } else {
            outRect.top = 20;
            outRect.bottom = 20;
        }
    }

    /**
     * 绘制ItemView的背景。意思就是这里画出来的图像,会显示在itemView的下面。这个方法会在绘制itemView之前调用.
     * 这里的绘制区域是根据上面的getItemOffsets决定的。
     * 注意这里的canvas指的是RecyclerView的布局部分,而不是itemView的界面。如果想在每一个ItemView的相同位置画图案,需要计算对应的坐标。
     *
     * @param c      绘画的布
     * @param parent 就是RecyclerView对象本身
     * @param state  state本身
     */
    @Override
    public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.onDraw(c, parent, state);

    }

    /**
     * 绘制ItemView的前景。意思就是这里画出来的图像,会显示在itemView的上面
     *
     * @param c      绘画的布
     * @param parent 就是RecyclerView对象本身
     * @param state  state本身
     */
    @Override
    public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.onDrawOver(c, parent, state);
        int childCount = parent.getChildCount();

        for (int i = 0; i < childCount; i++) {
            View child = parent.getChildAt(i);
            int radius = child.getHeight() / 2;
            int centerX = child.getRight() - 20 - radius;
            int centerY = child.getTop() + child.getHeight() / 2;
            c.drawCircle(centerX, centerY, radius, mPaint);

        }
    }
}

动画:ItemAnimator

点击事件

主要的实现方式还是在Adapter的onBindViewHolder中设置点击事件。

多种布局

对应的Layout布局文件

getItemViewType

onCreateViewHolder

Holder类

onBindViewHolder

添加头尾

下拉刷新上拉更多

好用的第三方框架

BRVAH(BaseRecyclerViewAdapterHelper)(RecyclerView使用框架):http://www.recyclerview.org/

SmartRefrshLayout(下拉刷新框架):https://gitee.com/scwang90/SmartRefreshLayout

参考资料

Android RecyclerView 使用完全解析 体验艺术般的控件

Android 优雅的为RecyclerView添加HeaderView和FooterView

【腾讯Bugly干货分享】Android ListView 与 RecyclerView 对比浅析—缓存机制

RecyclerView之ItemDecoration由浅入深

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容