RecyclerView使用方法举例(1)

  RecyclerView是在Android L中新加入的ViewGroup,可以通过Support Library在更早的版本的Android上使用。文档中对RecyclerView的概述只有一行“A flexible view for providing a limited window into a large data set”,Google希望用RecyclerView来替代之前的ListView和GridView。

  在ListView和GridView中,数据的获取和显示等功能是紧耦合的,所以用了两个类来分别实现列表和网格的显示。而RecyclerView只关心“Recycle”,至于里面的各个item如何摆放、列表如何显示,统统委托给了其他类来负责。这使得RecyclerView在带来更加灵活丰富的功能的同时,使用上也变得更加复杂,很多ListView和GridView中集成的功能,都需要手动实现。使用RecyclerView,通常离不开一下几个类:

  • Adapter:和ListView和GridView类似,用于包装数据,并负责单个item的创建。Adapter通过ViewHolder获得当前item中需要更新的View。
  • ViewHolder:持有当前item的所有子View,RecyclerView的Adapter强制使用View Holder的模式。
  • LayoutManager:负责摆放item。
  • ItemDecoration:在item周围绘制装饰,如分隔线。
  • ItemAnimator:在对item进行添加、移除、重新排列操作时添加动画效果。

  下面通过一个例子介绍RecyclerView的简单使用方法。这个例子从TMDb上获取若干流行电影的信息,使用RecyclerView以网格的形式显示出来。

1. 原始数据获取

  例子中使用的数据来自TMDb,详情请参考Retrofit使用方法举例。后续内容可以接着Retrofit使用方法举例中提供的例子继续进行。

2. 添加Layout

2.1. 添加RecyclerView

  使用Support Library版本的RecyclerView可以获得向前兼容。

<FrameLayout
    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:id="@+id/movie_grid"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</FrameLayout>

  完整文件在这里

2.2. 添加Item

  下面定义RecyclerView中每一个item的布局,包含一个用于显示海报的ImageView和一个用于显示电影名称的TextView。

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:layout_width="200dp"
    tools:layout_height="300dp" >

    <ImageView
        android:id="@+id/movie_poster"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="centerCrop"
        android:adjustViewBounds="true"
        tools:src="@drawable/placeholder_poster" />

    <TextView
        android:id="@+id/movie_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="16sp"
        android:singleLine="true"
        tools:text="Jurassic World" />

</LinearLayout>

  完整文件在这里

3. 实现Adapter

  前面定义类item的布局,但RecyclerView还不知道如何把数据填到item里面去,这是Adapter的工作。RecyclerView的Adapter强制使用ViewHolder的模式,由ViewHolder持有item中的所有子View。定义MovieAdapter 如下,它继承了RecyclerView.Adapter<MovieAdapter.ViewHolder> ,包含一个Movie的List作为数据集:

public class MovieAdapter extends RecyclerView.Adapter<MovieAdapter.ViewHolder> {
    private List<Movie> mMovies;

    public MovieAdapter(List<Movie> movies) {
        mMovies = movies;
    }
}

  要实现MovieAdapter ,需要实现MovieAdapter.ViewHolder ,并重写onCreateViewHolder() 、onBindViewHolder() 和getItemCount() 三个方法。

3.1. 实现ViewHolder

  下面来实现MovieAdapter.ViewHolder ,这里以内部类的形式实现:

public class MovieAdapter extends RecyclerView.Adapter<MovieAdapter.ViewHolder> {
    ...
    public static class ViewHolder extends RecyclerView.ViewHolder {
        public ImageView mPoster;
        public TextView mTitle;

        public ViewHolder(View itemView) {
            super(itemView);

            mPoster = (ImageView) itemView.findViewById(R.id.movie_poster);
            mTitle = (TextView) itemView.findViewById(R.id.movie_title);
        }
    }
}

ViewHolder 构造器中,在itemView 下面找到用于显示海报的movie_poster 和movie_title ,保存为成员变量。

3.2. 重写onCreateViewHolder()

  onCreateViewHolder() 用于创建单个item,获取LayoutInflater后,inflate之前定义的item的布局item_movie ,并以此实例化ViewHolder。

@Override
public MovieAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    Context context = parent.getContext();
    LayoutInflater inflater = LayoutInflater.from(context);

    View contactView = inflater.inflate(R.layout.item_movie, parent, false);

    ViewHolder viewHolder = new ViewHolder(contactView);
    return viewHolder;
}

inflater.inflate(R.layout.item_movie, parent, false) 中的最后一个参数false 为”attachToRoot“,如果为true,由此inflate得到的View会自动加到parent中,作为parent的子View。

3.3. 重写onBindViewHolder()

  onBindViewHolder() 负责将数据填入item。

@Override
public void onBindViewHolder(MovieAdapter.ViewHolder holder, int position) {
    Movie movie = mMovies.get(position);

    holder.mTitle.setText(movie.getTitle());

    String key = movie.getPosterPath();
    String url = ImageUtility.getImageUrl(key);
    Picasso.with(holder.itemView.getContext())
            .load(url)
            .error(R.drawable.placeholder_poster_white)
            .placeholder(R.drawable.placeholder_poster_white)
            .into(holder.mPoster);
}

position 从数据集中取出对应元素,填入ViewHolder的对应View中。这里将电影名称填入mTitle ,并使用Picasso将海报图片下载并加载到mPoster 。Picasso是一个Android上的图片下载和缓存库,使用方法十分简单,这里从url 下载图片并显示在holder.mPoster 中,在下载完成之前,使用R.drawable.placeholder_poster_white 显示在holder.mPoster 里作为占位符,如果下载失败,依然显示R.drawable.placeholder_poster_white 。注意从TMDb得到的海报链接是一个key,需要经过处理得到完整链接,详情可以参考这里的“Image Languages”一节。

3.4. 重写getItemCount()

  getItemCount() 用于返回当前数据集中数据元素的个数。这里使用的是List,获取其长度十分简单。

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

  以上完整文件可以参考这里

4. 设置RecyclerView

  首先找到RecyclerView:

mRecyclerView = (RecyclerView) rootView.findViewById(R.id.movie_grid);

  然后进行设置:
private MovieAdapter mMovieAdapter;
private List<Movie> mMovies = new ArrayList<>();
...
private void setupRecyclerView(@NonNull RecyclerView recyclerView) {
    mMovieAdapter = new MovieAdapter(mMovies);
    recyclerView.setAdapter(mMovieAdapter);

    GridLayoutManager layoutManager = new GridLayoutManager(getContext(), 2);
    recyclerView.setLayoutManager(layoutManager);
    recyclerView.setHasFixedSize(true);
}

首先将mMovies 传入MovieAdapter作为数据集,并使用recyclerView.setAdapter() 将mMovieAdapter 作为RecyclerView的Adapter。然后设置布局,这里使用GridLayoutManager 获得网格的布局,new GridLayoutManager(getContext(), 2) 中的2 表示有两列,使用recyclerView.setLayoutManager() 配置到RecyclerView。最后recyclerView.setHasFixedSize(true) 表示RecyclerView中的每一个item都有固定的尺寸(RecyclerView支持显示各种不同尺寸的item),如果每一个item大小都一样,可借此来优化显示效率。

  完整文件在这里

5. 通知数据更新

  前面mMovieAdapter = new MovieAdapter(mMovies); 的时候,mMovies 中可能还是空的,在从TMDb获得电影数据,填充mMovies 后,需要通知MovieAdapter有数据更新。

mMovies.addAll(movies);
mMovieAdapter.notifyItemInserted(0);

mMovies 初始是空的,这里将新数据加入,最后使用notifyItemInserted(0) 通知mMovieAdapter 在位置0添加了数据。RecyclerView.Adapter还提供了很多其他的方法用于通知数据更新,可以参考这里,其中notifyDataSetChanged() 最为万能,不需要任何参数,但效率也最低,应作为最后手段。

  完整文件在这里

6. 完整代码

  完整代码可以在这里找到。运行效果如图1所示。

 图1

图1