Android:RecyclerView 的使用,有这一篇就够了

谨以文章记录学习历程,如有错误还请指明。

RecyclerView 简介

首先,可以理解 RecyclerView 是 ListView 的升级版,更加灵活,同时由于封装了 ListView 的部分实现,导致其使用更简单,结构更清晰。

从名字 RecyclerView 来看,它直接提供了回收复用的功能,虽然 ListView 我们也可以自己实现 ViewHolder 以及 convertView 进行优化,但是在 RecyclerView 中,它直接封装了 ViewHolder 的回收复用,也就是说 RecyclerView 将 ViewHolder 标准化,我们不再需要面向 view ,而是直接面向 ViewHolder 编写实现我们需要的 Adapter,这样一来,逻辑结构就变得非常清晰。

当然,说到 RecyclerView 的优点,就不得不提它的 插拔式 的体验,高度解耦:

  • 布局(显示方式):可通过LayoutManager(LinearLayoutManager,GridLayoutManager,StaggeredGridLayoutManager )设置;
  • 分割线:通过 ItemDecoration 实现
  • Item 增删动画:通过 ItemAnimator
  • ViewHolder 的创建和绑定:通过实现 Adapter

除此之外,还需要额外提一下关于点击事件的问题,RecyclerView 本身是不提供点击、长按事件的,而隔壁的 ListView 稳稳支持。对此,可能刚接触 RecyclerView 的同学会疯狂吐槽,怎么作为升级版的 RecyclerView 在这一点上还不如旧版呢?

情况真的是这样么?

显然不是

ListView 中对于点击事件的处理,其实是有很大弊端的,它的 setOnItemClickListener() 方法是为子项注册点击事件,这就导致只能识别到这一整个子项,对于子项中的组件比如按钮就束手无策了。为此,RecyclerView 直接放弃了这个为子项注册点击事件的监听方法,所有点击事件都有具体 View 去注册,好处显而易见,我可以按需为组件注册点击事件,不存在点击不到的组件。

基础知识补充

如果读者从未接触过 ListView 和 RecyclerView,那么可能对前面提到的 ViewHolder、Adapter等一头雾水,为此在这里补充一些基础知识。

  • LayoutManager:RecyclerView 会根据 Layout Manager 提供的视图来填充自身,常用的布局管理器有LinearLayoutManager(线性布局管理器)、GridLayoutManager(网格布局管理器)、StaggeredGridLayoutManager (瀑布流布局管理器)等。
  • ViewHolder:列表中的视图由 ViewHolder 实例展示。 ViewHolder 用于对控件的实例进行缓存,负责显示子项。例如,如果列表显示了音乐集合,那么每个 ViewHolder 可能代表一个专辑。
  • Adapter:ViewHolder 对象由 Adapter 管理。Adapter 按需创建 ViewHolder,并为其绑定数据。绑定意味着根据 Adapter 中的位置为子视图填充对应的数据。

下面我们就介绍一些关于RecyclerView 的用法。

RecyclerView 用法

基础用法

首先,由于 RecyclerView 属于 v7-支持库,因此我们在使用之前需要先添加依赖:

dependencies {
    implementation 'com.android.support:recyclerview-v7:27.1.1'
}

接下来,RecyclerView 的核心使用流程如下:

mRecyclerView = findView(R.id.id_recycler_view);
//设置布局管理器
mRecyclerView.setLayoutManager(mLayoutManager);
//设置adapter
mRecyclerView.setAdapter(mAdapter)
//设置Item增加、移除动画
mRecyclerView.setItemAnimator(new DefaultItemAnimator());
//添加分割线
mRecyclerView.addItemDecoration(new DividerItemDecoration(
                getActivity(), DividerItemDecoration.HORIZONTAL_LIST));

可能用惯了 ListView ,第一次接触 RecyclerView 的同学会觉得,ListView 只需要一个 Adapter 就足够了,怎么 RecyclerView 的使用这么繁琐。

其实恰恰正是由于 RecyclerView 的高度解耦,本身只提供基本的回收和复用,而其他特性都允许我们自己定制,因此我们可以通过它来灵活地实现各种 复杂花哨 的效果。可以说,RecyclerView 以增加部分代码的代价来增加其灵活性和扩展性。

下面就来介绍一下 如何通过 RecyclerView 轻松实现一个普通列表:

MainActivity.java

public class MainActivity extends AppCompatActivity {

    private RecyclerView mRecyclerView;
    private MyAdapter mMyAdapter;
    private LinearLayoutManager mLayoutManager;
    private List list;

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

        mRecyclerView = findViewById(R.id.recycler_view);
        mMyAdapter = new MyAdapter(list);
        mLayoutManager = new LinearLayoutManager(this);
        mRecyclerView.setLayoutManager(mLayoutManager);
        mRecyclerView.setAdapter(mMyAdapter);
    }

    private void initData() {
        list = new ArrayList<>();
        for (int i = 0; i <= 20; i++) {
            list.add("Item " + i);
        }
    }
}

MyAdapter.java

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {

    //数据源
    private List mList;

    public MyAdapter(List list) {
        mList = list;
    }

    //返回item个数
    @Override
    public int getItemCount() {
        return mList.size() ;
    }

    //创建ViewHolder
    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    return new NormalHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item, parent, false));
    }

    //填充视图
    @Override
    public void onBindViewHolder(@NonNull final MyAdapter.ViewHolder holder, final int position) {
        holder.mView.setText(mList.get(position));
    }

    public class ViewHolder extends RecyclerView.ViewHolder {
        public TextView mView;

        public ViewHolder(View itemView) {
            super(itemView);
            mView = itemView.findViewById(R.id.text_view);
        }
    }
}

MainActivity 的布局文件,activity_main.xml

"1.0" encoding="utf-8"?>
"http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    "@+id/recycler_view"
        android:divider="#FFFF0000"
        android:dividerHeight="10dp"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

子项布局,Item.xml

"1.0" encoding="utf-8"?>
"http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    "@+id/text_view"
        android:layout_width="match_parent"
        android:layout_height="120dp"
        android:gravity="center"
        android:background="#c0c7c759"
        android:textSize="20sp"
        android:text="1" />

通过上述代码,就轻轻松松实现了一个列表,效果如下:

Android:RecyclerView 的使用,有这一篇就够了_第1张图片

可以看到,RecyclerView 的实现结构很清晰,且内部直接封装了 ViewHolder,使得我们不再需要像 ListView 一样自己实现 ViewHolder+convertView 去优化,我们只需要面向 ViewHolder 变成即可,关于复用优化的部分 RecyclerView 已经帮我们做好了。

分割线 - ItemDecoration

虽然上面我们实现了一个 列表界面,但是!!这实在是丑的没法看了,我们随便拉出来一个手机里的app,里面的列表颜值都秒杀它,拿微信界面为例:

Android:RecyclerView 的使用,有这一篇就够了_第2张图片

很明显,不同模块间有明显的分割空白,而相同模块不同功能也会有一个分割线,整体看起来结构很清晰。

为了给我们的列表添加分割线的效果,我们就需要了解一下 `RecyclerView.addItemDecoration() 方法,该方法接收一个RecyclerView.ItemDecoration 类型参数,其本身是一个抽象类,这就表明我们可以通过继承这个类,编码实现我们所需要的 复杂花哨 的分割线效果。

下面,我们先来了解一下 ItemDecoration 这个类,源码如下:

public abstract static class ItemDecoration {
        //使用此方法绘制的任何内容都将在绘制子项视图之前绘制,从而出现在视图之下。
        public void onDraw(Canvas c, RecyclerView parent, State state) {
            onDraw(c, parent);
        }

        @Deprecated
        public void onDraw(Canvas c, RecyclerView parent) {
        }

        //使用此方法绘制的任何内容都将在绘制子项视图之后绘制,从而出现在视图之上。
        public void onDrawOver(Canvas c, RecyclerView parent, State state) {
            onDrawOver(c, parent);
        }

        @Deprecated
        public void onDrawOver(Canvas c, RecyclerView parent) {
        }

        @Deprecated
        public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
            outRect.set(0, 0, 0, 0);
        }

        //为每个Item设置一定的偏移量
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
            getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(),
                    parent);
        }
    }

可以看到,这个类实际上是很简单的,可供使用的只有三个方法,而其中 onDraw() 方法和 onDrawOver() 方法实际使用中是没什么区别的(可能是我太菜,没遇到要严格区分的场景)。于是我们如果想实现自定义效果时,只需要重写 onDraw()或者onDrawOver() 方法和 getItemOffsets() 方法即可。当 RecyclerView 绘制时,会根据 getItemOffsets() 方法计算每个 Item 的位置,然后通过 onDraw() 方法绘制出来。

下面我们看官方为我们提供的默认实现类 DividerItemDecoration,源码如下:

public class DividerItemDecoration extends RecyclerView.ItemDecoration {
    public static final int HORIZONTAL = LinearLayout.HORIZONTAL;
    public static final int VERTICAL = LinearLayout.VERTICAL;

    private static final String TAG = "DividerItem";
    private static final int[] ATTRS = new int[]{ android.R.attr.listDivider };

    private Drawable mDivider;

    /**
     * Current orientation. Either {@link #HORIZONTAL} or {@link #VERTICAL}.
     */
    private int mOrientation;

    private final Rect mBounds = new Rect();

    /**
     * Creates a divider {@link RecyclerView.ItemDecoration} that can be used with a
     * {@link LinearLayoutManager}.
     *
     * @param context Current context, it will be used to access resources.
     * @param orientation Divider orientation. Should be {@link #HORIZONTAL} or {@link #VERTICAL}.
     */
    public DividerItemDecoration(Context context, int orientation) {
        final TypedArray a = context.obtainStyledAttributes(ATTRS);
        mDivider = a.getDrawable(0);
        if (mDivider == null) {
            Log.w(TAG, "@android:attr/listDivider was not set in the theme used for this "
                    + "DividerItemDecoration. Please set that attribute all call setDrawable()");
        }
        a.recycle();
        setOrientation(orientation);
    }

    /**
     * Sets the orientation for this divider. This should be called if
     * {@link RecyclerView.LayoutManager} changes orientation.
     *
     * @param orientation {@link #HORIZONTAL} or {@link #VERTICAL}
     */
    public void setOrientation(int orientation) {
        if (orientation != HORIZONTAL && orientation != VERTICAL) {
            throw new IllegalArgumentException(
                    "Invalid orientation. It should be either HORIZONTAL or VERTICAL");
        }
        mOrientation = orientation;
    }

    /**
     * Sets the {@link Drawable} for this divider.
     *
     * @param drawable Drawable that should be used as a divider.
     */
    public void setDrawable(@NonNull Drawable drawable) {
        if (drawable == null) {
            throw new IllegalArgumentException("Drawable cannot be null.");
        }
        mDivider = drawable;
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        if (parent.getLayoutManager() == null || mDivider == null) {
            return;
        }
        if (mOrientation == VERTICAL) {
            drawVertical(c, parent);
        } else {
            drawHorizontal(c, parent);
        }
    }

    private void drawVertical(Canvas canvas, RecyclerView parent) {
        canvas.save();
        final int left;
        final int right;
        //noinspection AndroidLintNewApi - NewApi lint fails to handle overrides.
        if (parent.getClipToPadding()) {
            left = parent.getPaddingLeft();
            right = parent.getWidth() - parent.getPaddingRight();
            canvas.clipRect(left, parent.getPaddingTop(), right,
                    parent.getHeight() - parent.getPaddingBottom());
        } else {
            left = 0;
            right = parent.getWidth();
        }

        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            parent.getDecoratedBoundsWithMargins(child, mBounds);
            final int bottom = mBounds.bottom + Math.round(child.getTranslationY());
            final int top = bottom - mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(canvas);
        }
        canvas.restore();
    }

    private void drawHorizontal(Canvas canvas, RecyclerView parent) {
        canvas.save();
        final int top;
        final int bottom;
        //noinspection AndroidLintNewApi - NewApi lint fails to handle overrides.
        if (parent.getClipToPadding()) {
            top = parent.getPaddingTop();
            bottom = parent.getHeight() - parent.getPaddingBottom();
            canvas.clipRect(parent.getPaddingLeft(), top,
                    parent.getWidth() - parent.getPaddingRight(), bottom);
        } else {
            top = 0;
            bottom = parent.getHeight();
        }

        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            parent.getLayoutManager().getDecoratedBoundsWithMargins(child, mBounds);
            final int right = mBounds.right + Math.round(child.getTranslationX());
            final int left = right - mDivider.getIntrinsicWidth();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(canvas);
        }
        canvas.restore();
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
            RecyclerView.State state) {
        if (mDivider == null) {
            outRect.set(0, 0, 0, 0);
            return;
        }
        if (mOrientation == VERTICAL) {
            outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
        } else {
            outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
        }
    }

实际上,该实现类会读取系统主题中的@android:attr/listDivider作为 Item 的分割线(本质是 Drawable ),同时还注意到有这样一个方法 setDrawable(),该方法允许我们为分割线指定样式。也就是说,如果我们只是单纯为了改变分割线样式,我们有两种方式来改变分割线的样式:

  • 调用 setDrawable() 直接传入一个 drawable 对象
  • 修改系统主题中的 android:listDivider 属性值

我们来实际操作一下,在原来的 MainActivity.java 中为 RecyclerView 添加默认分割线,如下:

mRecyclerView.addItemDecoration(new DividerItemDecoration(this,DividerItemDecoration.VERTICAL));

运行一下,结果如下:

Android:RecyclerView 的使用,有这一篇就够了_第3张图片

下面我们再为分割线更换一种样式:

首先在drawable/路径下创建一个资源文件divider.xml

"1.0" encoding="utf-8"?>
"http://schemas.android.com/apk/res/android"
    android:shape="rectangle">

    "#ff00ff00"
        android:endColor="#ff0000ff"
        android:startColor="#ffff0000"
        android:type="linear" />

    "4dp"/>

  • 通过 setDrawable() 方法:

    //源代码:
    mRecyclerView.addItemDecoration(new DividerItemDecoration(this,DividerItemDecoration.VERTICAL));
    
    //改成如下:
    DividerItemDecoration decoration = new DividerItemDecoration(this,DividerItemDecoration.VERTICAL);
         decoration.setDrawable(getResources().getDrawable(R.drawable.divider,null));
    mRecyclerView.addItemDecoration(decoration);
  • 或者 修改系统主题中的 android:listDivider 属性值:

二者实现的效果是一样的,如下:

Android:RecyclerView 的使用,有这一篇就够了_第4张图片

是不是有一点点酷炫?是不是对 RecyclerView 跃跃欲试了?别急,RecyclerView 的厉害之处远不止这些。

布局 - LayoutManager

前面我们提到,RecyclerView 可以通过设置不同的 LayoutManager 来达到不同的布局效果,如线性布局,网格布局等,这里我们就来介绍一下,关于 LayoutManager 的使用。

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

LayoutManager 仍然只是一个抽象类,官方为我们提供了几个实现类:

  1. LinearLayoutManager:LayoutManager 的子类,线性布局管理器,支持横向、纵向
  2. GridLayoutManager:LinearLayoutManager 的子类,网格布局管理器
  3. StaggeredGridLayoutManager:LayoutManager 的子类,瀑布流布局管理器

前面我们一直使用的都是 LinearLayoutManager,下面我们就来试一下后两个布局管理器。

首先是 GridLayoutManager,修改代码:

//mLayoutManager = new LinearLayoutManager(this);

mLayoutManager = new GridLayoutManager(this,4);
mRecyclerView.setLayoutManager(mLayoutManager);

运行一下。

Android:RecyclerView 的使用,有这一篇就够了_第5张图片

首先,我们实现了 网格布局,即类似 GridView 的效果。

但是我们也会发现,分割线绘制了一整行,显然这个效果出了些问题,这是因为在绘制的时候,水平线的计算方式是:父布局的 左边缘~右边缘,自然绘制了一整行,部分源码如下:

if (parent.getClipToPadding()) {
            left = parent.getPaddingLeft();
            right = parent.getWidth() - parent.getPaddingRight();
            canvas.clipRect(left, parent.getPaddingTop(), right,
                    parent.getHeight() - parent.getPaddingBottom());
        } else {
            left = 0;
            right = parent.getWidth();
        }

如果我们想要实现分割线仅出现在本子项的下面,那么就需要修改上述计算,代码就不贴出来了,参见https://blog.csdn.net/lmj623565791/article/details/45059587

这还不算强,真正强大的是 RecyclerView 分分钟可以实现一个 ListView 难以实现的瀑布流,也就是通过 StaggeredGridLayoutManager 来实现瀑布流布局。

//mLayoutManager = new GridLayoutManager(this,4);

mLayoutManager = new StaggeredGridLayoutManager (4,StaggeredGridLayoutManager.VERTICAL);
mRecyclerView.setLayoutManager(mLayoutManager);
//如果传入 StaggeredGridLayoutManager.VERTICAL则该布局支持纵向滑动,那么前面的 4 则指的是 4列。
//如果传入 StaggeredGridLayoutManager.HORIZONTAL,则该布局支持横向滑动,那么前面的4 则指的是 4行

效果如下

Android:RecyclerView 的使用,有这一篇就够了_第6张图片

看起来和 网格布局没什么区别嘛,不要急,我们只需要为 Item 指定一个随即高度就好了。

//MyAdapter.java
 @Override
    public void onBindViewHolder(@NonNull final MyAdapter.ViewHolder holder, final int position) {
        holder.mView.setText(mList.get(position));

        //加入以下代码
        ViewGroup.LayoutParams layoutParams = holder.mView.getLayoutParams();
        layoutParams.height = new Random().nextInt(200)+200;
        holder.mView.setLayoutParams(layoutParams);
    }
//Item.xml 为Item添加外边距
//实际上分割线也可以通过添加边距的方式“变相”实现,但是这种偷鸡的做法肯定不如去实现一个 ItemDecoration
"10dp"
        ...
       />

效果如下:

Android:RecyclerView 的使用,有这一篇就够了_第7张图片

看到此,可以说用 RecyclerView 实现 ListView、GridView、瀑布流的效果简直毫无压力。

动画 - ItemAnimator

动画也是 RecyclerView 很强大的一点,我们可以通过实现 ItemAnimator 这个抽象类来定制我们想要的动画。

官方为我们提供了一种默认的实现 DefaultItemAnimator ,借助这个类,我们就可以加入 Item 的添加和删除动画,实现起来很简单,只需要加入下面这一行代码即可:

mRecyclerView.setItemAnimator(new DefaultItemAnimator());

效果如下:

当然我们需要对现有代码做出修改

//MyAdapter.java 加入以下方法

public void addData(int position) {
    mList.add(position, "Insert One");
    notifyItemInserted(position);
}

public void removeData(int position) {
    mList.remove(position);
    notifyItemRemoved(position);
}

//添加menu.xml
"1.0" encoding="utf-8"?>
"http://schemas.android.com/apk/res/android">
    "@+id/add"
        android:icon="@drawable/ic_add"
        android:title="Add"/>
    "@+id/delete"
        android:icon="@drawable/ic_delete"
        android:title="delete"/>


//在MainActivity.java 加入以下方法
@Override
public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.menu, menu);
    return true;
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
        case R.id.add:
            mMyAdapter.addData(1);
            break;
        case R.id.delete:
            mMyAdapter.removeData(1);
            break;
    }
    return true;
}

上述只是展示了 StaggeredGridLayoutManager 的动画,另外的布局读者可以自行尝试。

当然本文只涉及到系统提供的默认动画实现,至于更多贼鸡儿好看的动画效果,就推荐一个GayHub 上 2.9K Star 的项目:RecyclerViewItemAnimators 。

点击事件 - 接口回调

关于点击事件,前面我们分析过为什么 RecyclerView 不提供点击事件,这里再重复一下:

  • ListView 中点击事件只能针对子项,无法监听内部控件
  • RecyclerView 按需对任意 View 设置点击监听,不存在点击不到的控件。

这里提供通过接口回调的方式来实现点击事件。除此之外,也可以通过手势判断,由于笔者现在对手势没有了解,读者可以自行实现。

MyAdapter.java

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {

    //...
    //定义接口 OnItemClickListener
    public interface OnItemClickListener {
        void onItemClick(View view, int position);
        void onItemLongClick(View view, int position);
    }
    private OnItemClickListener mOnItemClickListener;

    public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
        mOnItemClickListener = onItemClickListener;
}   

    //填充视图
    @Override
    public void onBindViewHolder(@NonNull final MyAdapter.ViewHolder holder, final int position) {
        holder.mView.setText(mList.get(position));
        if (mOnItemClickLitener != null)
        {
            holder.itemView.setOnClickListener(new OnClickListener()
            {
                @Override
                public void onClick(View v)
                {
                    int pos = holder.getLayoutPosition();
                    mOnItemClickLitener.onItemClick(holder.itemView, pos);
                }
            });

            holder.itemView.setOnLongClickListener(new OnLongClickListener()
            {
                @Override
                public boolean onLongClick(View v)
                {
                    int pos = holder.getLayoutPosition();
                    mOnItemClickLitener.onItemLongClick(holder.itemView, pos);
                    return false;
                }
            });
    }

    //...
}

MainActivity.java

mMyAdapter.setOnItemClickListener(new MyAdapter.OnItemClickListener() {
            @Override
            public void onItemClick(View view, int position) {
                Toast.makeText(MainActivity.this, ((TextView)view).getText() + " click", Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onItemLongClick(View view, int position) {

                Toast.makeText(MainActivity.this, position + " Long click", Toast.LENGTH_SHORT).show();
                mMyAdapter.removeData(position);
            }
        });

通过 drawable 设置点击效果

//设置点击效果
"@drawable/item_press"
    .../>


//drawable/item_perss.xml
"1.0" encoding="utf-8"?>
"http://schemas.android.com/apk/res/android" >
    "true" android:drawable="@color/colorPressed"/>
    "@color/colorNormal"/>


//colors.xml 添加如下

    ...
    "colorNormal">#c0c7c759
    "colorPressed">#00FF00

我们在 MyAdapter 中定义监听接口,在 onBindViewHolder() 中为 holder.itemView 设置点击事件,并回调 MainActivity.java 中设置的监听事件。

效果如下:

下拉更新 - SwipeRefreshLayout

下拉更新,我们借助 SwipeRefreshLayout 就可以相当容易的实现了。SwipeRefreshLayout 是 Google 在API 19.1 之后加入的用于下拉刷新的控件,通常情况下,我们只需要实现其内部的 SwipeRefreshLayout.OnRefreshListener接口即可实现下拉刷新的功能。话不多说,动手实践。

首先修改 RecyclerView 的布局,在其外部嵌套一个 SwipeRefreshLayout。

activity_main.xml

"1.0" encoding="utf-8"?>
"http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/swipe_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    "@+id/recycler_view"
        android:divider="#FFFF0000"
        android:dividerHeight="10dp"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

MainActivity.java 中实现SwipeRefreshLayout.OnRefreshListener接口:

public class MainActivity extends AppCompatActivity {   
    //.....

    @Override
    protected void onCreate(final Bundle savedInstanceState) {
        //...

        mRefreshLayout = findViewById(R.id.swipe_layout);
        mRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                //刷新动画开始后 回调此方法

                //设置可见
                mRefreshLayout.setRefreshing(true);

                //向头部插入数据
                List newDatas = new ArrayList();
                for (int i = 0; i < 5; i++) {
                    int index = i + 1;
                    newDatas.add("new item" + index);
                }
                mMyAdapter.addItem(newDatas);
                mHandler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        //模拟加载时间,设置不可见
                        mRefreshLayout.setRefreshing(false);
                    }
                }, 1000);
            }
        });

        //.....

    }

    //....
}

MyAdapter.java 中加入 addItem() 方法,供 下拉刷新 时调用

public class MyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    //...

    public void addItem(List list){
        list.addAll(mList);
        mList.clear();
        mList.addAll(list);
        notifyDataSetChanged();
    }

    //....
}

效果如图:

Android:RecyclerView 的使用,有这一篇就够了_第8张图片

是不是很简单~
(第二次刷新时由于新数据和第一次一样,导致会有第二次没刷新出来新数据的错觉)

上拉加载更多 - footerview

笔者这里的上拉加载更多,主要实现的是类似分页加载的功能,一开始只加载部分数据,这样当数据量过大或者网速过慢时,不至于影响用户体验。

具体的实现思路,引入一个 footerview,滑动的时候通过实现 OnScrollListener 接口判断 footerview 的位置,从而判断当前是否需要加载更多数据。

为了方便,笔者这里用最基本的 LinearLayoutManager 实现纵向线性布局,并取消了上面的随机高度。

具体看代码吧,注释足够详细了。

MainActivity.java

public class MainActivity extends AppCompatActivity {

    private SwipeRefreshLayout mRefreshLayout;
    private RecyclerView mRecyclerView;
    private MyAdapter mMyAdapter;
    private LinearLayoutManager mLayoutManager;
    private List list;

    //最后一个可见Item的位置,关键所在
    private int lastVisibleItem;
    private Handler mHandler = new Handler(Looper.getMainLooper());

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

        mRefreshLayout = findViewById(R.id.swipe_layout);
        mRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                //设置可见
                mRefreshLayout.setRefreshing(true);
                List newDatas = new ArrayList();
                for (int i = 0; i < 5; i++) {
                    int index = i + 1;
                    newDatas.add("new item" + index);
                }
                mMyAdapter.addItem(newDatas);
                mHandler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        //模拟加载时间,设置不可见
                        mRefreshLayout.setRefreshing(false);
                    }
                }, 1000);
            }
        });

        mRecyclerView = findViewById(R.id.recycler_view);
        mMyAdapter = new MyAdapter(list);
        mLayoutManager = new LinearLayoutManager(this);
        mRecyclerView.setLayoutManager(mLayoutManager);
        mRecyclerView.setItemAnimator(new DefaultItemAnimator());
        mMyAdapter.setOnItemClickListener(new MyAdapter.OnItemClickListener() {
            @Override
            public void onItemClick(View view, int position) {
                Toast.makeText(MainActivity.this, ((TextView)view).getText() + " click", Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onItemLongClick(View view, int position) {

                Toast.makeText(MainActivity.this, position + " Long click", Toast.LENGTH_SHORT).show();
                mMyAdapter.removeData(position);
            }
        });
        mRecyclerView.setAdapter(mMyAdapter);

        //设置滑动监听
        mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {

            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);

                //判断是否滑动到底
                if (newState == RecyclerView.SCROLL_STATE_IDLE && lastVisibleItem + 1 ==mMyAdapter.getItemCount()) {
                    //滑动到底,需要改变状态为 上拉加载更多
                    mMyAdapter.changeMoreStatus(MyAdapter.LOADING_MORE);
                    //模拟加载数据
                    mHandler.postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            List newDatas = new ArrayList();
                            for (int i = 0; i < 5; i++) {
                                int index = i + 1;
                                newDatas.add("more item" + index);
                            }
                            mMyAdapter.addMoreItem(newDatas);

                            //此时显示 正在加载中
                            mMyAdapter.changeMoreStatus(MyAdapter.PULLUP_LOAD_MORE);
                        }
                    },2500);
                }
            }

            //更新最后可见位置
            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                lastVisibleItem = mLayoutManager.findLastVisibleItemPosition();

            }
        });

    }
    private void initData() {
        list = new ArrayList<>();
        for (int i = 0; i <= 20; i++) {
            list.add("Item " + i);
        }
    }


    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.add:
                mMyAdapter.addData(1);
                break;
            case R.id.delete:
                mMyAdapter.removeData(1);
                break;
        }
        return true;
    }


}

MyAdapter.java

package com.bit.whdalive.demo;

import android.os.Handler;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;

import org.w3c.dom.Text;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class MyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    //上拉状态0:上拉加载更多
    public static final int PULLUP_LOAD_MORE = 0;
    //上拉状态1:正在加载中
    public static final int LOADING_MORE = 1;

    private static final int TYPE_FOOTER = 0;//带Footer的Item
    private static final int TYPE_NORMAL = 1;//不带Footer的Item

    //数据源
    private List mList;
    //上拉加载状态,默认为状态0-上拉加载更多
    private int load_more_status = 0;

    private Handler mHandler = new Handler(Looper.getMainLooper());

    public MyAdapter(List list) {
        mList = list;
    }
    //定义点击事件的接口
    public interface OnItemClickListener {
        void onItemClick(View view, int position);
        void onItemLongClick(View view, int position);
    }
    //点击事件的监听
    private OnItemClickListener mOnItemClickListener;

    public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
        mOnItemClickListener = onItemClickListener;
    }

    //根据Item位置返回viewType,供onCreateViewHolder方法内获取不同的Holder
    @Override
    public int getItemViewType(int position) {
        if (position == getItemCount() - 1) {
            return TYPE_FOOTER;
        } else {
            return TYPE_NORMAL;
        }
    }

    //返回item个数,由于footerview的存在,所以item个数要+1
    @Override
    public int getItemCount() {
        return mList.size() + 1;
    }

    //创建ViewHolder,如果是header或者footer直接返回响应holder即可
    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        if (viewType == TYPE_NORMAL) {
            return new NormalHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item, parent, false));
        } else {
            return new FooterHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.footer, parent, false));
        }
    }

    //填充视图
    @Override
    public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position) {
        //普通Item,直接设置
        if (holder instanceof NormalHolder) {
            //为普通Item填充数据
            ((NormalHolder) holder).mView.setText(mList.get(position));
            //设置监听
            if (mOnItemClickListener!=null){
                ((NormalHolder) holder).mView.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        mOnItemClickListener.onItemClick(((NormalHolder) holder).mView,position);
                    }
                });
                ((NormalHolder) holder).mView.setOnLongClickListener(new View.OnLongClickListener() {
                    @Override
                    public boolean onLongClick(View v) {
                        mOnItemClickListener.onItemLongClick(((NormalHolder) holder).mView,position);
                        return false;
                    }
                });
            }
        }else {
            //footerview,需要判断上拉状态

            switch (load_more_status){
                //状态:上拉加载更多
                case PULLUP_LOAD_MORE:
                    ((FooterHolder) holder).mView.setText("上拉加载更多哦");
                    break;
                //状态:正在加载中
                case LOADING_MORE:
                    ((FooterHolder) holder).mView.setText("正在加载中哦");
                    break;
            }
        }
    }

    //改变当前上拉状态
    public void changeMoreStatus(int status){
        load_more_status= status;
        notifyDataSetChanged();
    }

    //Footer对应Holder
    public class FooterHolder extends RecyclerView.ViewHolder {
        private TextView mView;

        public FooterHolder(View itemView) {
            super(itemView);
            mView = itemView.findViewById(R.id.footer);
        }
    }
    //普通Item对应的Holder
    public class NormalHolder extends RecyclerView.ViewHolder {
        public TextView mView;

        public NormalHolder(View itemView) {
            super(itemView);
            mView = itemView.findViewById(R.id.text_view);
        }
    }
    //头部添加Item,供上拉刷新时调用
    public void addItem(List list){
        list.addAll(mList);
        mList.clear();
        mList.addAll(list);
        notifyDataSetChanged();
    }
    //末尾添加Item,供上拉加载更多时调用
    public void addMoreItem(List list){
        mList.addAll(list);
        notifyDataSetChanged();
    }

    //menu添加
    public void addData(int position) {
        mList.add(position, "Insert One");
        notifyItemInserted(position);
    }

    //menu删除
    public void removeData(int position) {
        mList.remove(position);
        notifyItemRemoved(position);
    }

}

footer.xml

"1.0" encoding="utf-8"?>
"http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="100dp">

    "@+id/footer"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:textSize="20sp"
        android:textColor="@color/colorAccent"
        android:gravity="center"
        android:background="#3925e1a5"/>

效果:

需要说明的是,笔者的实现只是停留在学习层面,应该是不能直接应用到实际开发中的。对此,大家可以去GayHub 上寻找成熟的上拉/下拉刷新数据的开源框架来应用到开发中。

总结

洋洋洒洒,写的篇幅有点长,笔者尽可能的对 RecyclerView 常用的部分都介绍了一遍,当然略显浅显,想要能顺畅的按需实现效果还是需要深入学习的,比如按需实现分割线效果,再比如炫酷的动画,没有深入理解估计就只能是个API coder,而不是designer了。

一句话概括 RecyclerView:入门容易,上限很高

最后,耐心读到这里的读者,愿此文对你们有所帮助。

你可能感兴趣的:(Android,基础)