1. 程式人生 > >RecyclerView 全面使用及分析 - 基礎篇(一)

RecyclerView 全面使用及分析 - 基礎篇(一)

一、RecyclerView 介紹

RecyclerView—Recycling

在 RecyclerView 出來之前,大家都在使用 ListView、GridView,當然 RecyclerView 出來之後,基本上都轉向了 RecyclerView,從名字上可以看出,它能夠實現view 的複用,同樣 ListView 在使用時我們自己也可以通過 converView 來實現複用,但是 RecyclerView 已經幫我們做好了,我們只需要給出需要裝載 view 的 ViewHolder 就行,同時允許我們新增 ItemDecoration,ItemAnimator,設定多樣的LayoutManager。

有這樣一段英文:

the RecyclerView itself doesn’t care about visuals at all. It doesn’t care about placing the elements at the right place, it doesn’t care about separating any items and not about the look of each individual item either. To exaggerate a bit: All RecyclerView does, is recycle stuff. Hence the name.

Anything that has to do with layout, drawing and so on, that is anything that has to do with how your data set is presented, is delegated to pluggable classes. That makes the new RecyclerView API extremely flexible.

大概意思是:RecyclerView 不關心檢視,不關心元素位置,只關心複用,也就是說 RecyclerView 幫我們做好了複用的實現, 其他方面我們自己來配置,這樣更加靈活,達到所謂的“插拔式”效果,即可新增 分割線,可新增動畫效果,可設定佈局效果等等。

其實複用,就是多建立一個或者幾個檢視,當滑動到底部時,載入更多的檢視時,複用已經不可見的檢視。至於多建立檢視是幾個,每一屏上的檢視是幾個,暫時我也不知道,後面進行原始碼分析時,再來揭曉,先知道它大概的原理。

RecyclerView—Recycling

二、基本使用

RecyclerView 使用很頻繁,其實主要場景就是要展示的檢視數量很多,無法在一個螢幕之下展示出來,當然你想到 ScrollView,ScrollView 展示的檢視也是有限的,也不會很多,否則會造成卡頓,甚至 OOM。RecyclerView 一般的用法就是做分頁載入,上拉載入,下拉重新整理,然後將資料鋪到介面上,複雜一些時,介面上有幾種不同的檢視,通過不同的 ViewHolder 來裝載。

下面就先開看看最基本的用法:

Steps

首先在 gradle 中引入 RecyclerView ,這裡同時引入 cardview,不是必須的,只是為了在新增每個Item 檢視時使用 cardview ,更加美觀一些。

    implementation 'com.android.support:cardview-v7:27.1.1'
    implementation 'com.android.support:recyclerview-v7:27.1.1'

1. XML 佈局設定

引用 recyclerview

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:toolbar="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.design.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <!--使用 toolbar 設定選單-->
        <include layout="@layout/view_toolbar" />

        <!--用於重新整理-->
        <com.ralf.www.recyclerviewtest.widget.MultiSwipeRefreshLayout
            android:id="@+id/swipe_refresh_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_behavior="@string/appbar_scrolling_view_behavior">

            <!--重點部分,使用RecyclerView,高度設定,-->
            <!--如果是垂直佈局,使用match_parent-->
            <!--如果是水平佈局,可使用wrap_content -->
            <android.support.v7.widget.RecyclerView
                android:id="@+id/rv_meizhi"
                android:layout_width="match_parent"
                android:layout_height="match_parent" />

        </com.ralf.www.recyclerviewtest.widget.MultiSwipeRefreshLayout>

        <android.support.design.widget.FloatingActionButton
            android:id="@+id/main_fab"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="right|bottom"
            android:layout_marginBottom="24dp"
            android:layout_marginRight="16dp"
            android:clickable="true"
            android:onClick="onFab"
            android:src="@mipmap/ic_refresh_white_24dp"
            app:borderWidth="0dp"
            app:elevation="4dp"
            app:layout_anchor="@id/swipe_refresh_layout"
            app:layout_anchorGravity="right|bottom"
            app:layout_behavior="com.ralf.www.recyclerviewtest.widget.FABAutoHideBehavior" />

    </android.support.design.widget.CoordinatorLayout>

</FrameLayout>

子項的 XML 佈局

item_main_activity_rv.xml

子項佈局在重寫 Adapter 時使用,外面包了一層 CardView,更加美觀一些

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
    android:id="@+id/meizhi_card"
    xmlns:android="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="wrap_content"
    android:layout_margin="5dp"
    android:clickable="true"
    android:foreground="?attr/selectableItemBackground"
    app:cardCornerRadius="2dp"
    app:cardElevation="4dp"
    tools:minWidth="160dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <ImageView
            android:id="@+id/iv_meizhi"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:adjustViewBounds="true"
            android:scaleType="fitXY"
            tools:src="@mipmap/ic_launcher"/>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:paddingBottom="10dp"
            android:paddingLeft="10dp"
            android:paddingRight="10dp"
            android:paddingTop="10dp">

            <TextView
                android:id="@+id/tv_title"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textAppearance="?android:attr/textAppearanceSmall"
                tools:text="Title"/>

        </LinearLayout>

    </LinearLayout>

</android.support.v7.widget.CardView>

2. LayoutManager 設定

RecyclerView 設定 LayoutManager,系統提供了3種 LayoutManager:LinearLayoutManager(線性佈局)、GridLayoutManager(網格佈局)、StaggeredGridLayoutManager(瀑布佈局),根據場景需要自己設定,也可以自己實現 LayoutManager。
這裡先給出簡單的使用,對於詳細的分析以及自己實現 後面的文章(加粗下,提醒自己。。)再給出分析。


//        final StaggeredGridLayoutManager layoutManager = new StaggeredGridLayoutManager(
//                2, StaggeredGridLayoutManager.VERTICAL
//        );
        final GridLayoutManager layoutManager = new GridLayoutManager(this, 2
                , GridLayoutManager.VERTICAL, false);

//        final LinearLayoutManager layoutManager = new LinearLayoutManager(this,
//                LinearLayoutManager.VERTICAL, false);
                
       // 設定佈局管理器        
       mRecyclerView.setLayoutManager(layoutManager);

3. 設定資料來源

資料來源一般就用 List 傳遞給 Adapter,作為其建構函式中的一個引數給出。

private List<GanHuo> mPicList = new ArrayList<>();

直接在 成員變數宣告時給出 List 物件,一般資料來源 都是通過網路求來的,在 demo 中我集成了 Retrofit + Rxjava 進行網路請求,載入圖片,對這部分沒接觸的童鞋可以看看 Retrofit + Rxjava,或者自己替換成本地的圖片也行。

有一點,需要注意下:對於初始化時,設定佈局管理器,設定 Adapter,設定資料來源,沒有嚴格的先後順序。開始沒有在 List 中加入資料,初始化時候,載入網路資料或者本地資料,然後通過重新整理列表即可。

4. 實現 Adapter 並設定

PictureAdapter 需要整合 RecyclerView.Adapter,並需要宣告泛型型別 ViewHolder,否則是 Object 型別,並實現 三個方法:

生成用於持有每個 View 的 ViewHolder,實現複用
onCreateViewHolder

將ViewHolder繫結,即將資料繫結到檢視上
onBindViewHolder

獲取子 View 的數量,即傳過來的 List 的大小
getItemCount


public class PictureAdapter extends RecyclerView.Adapter<PictureAdapter.PictureViewHolder> {

    private List<GanHuo> picList;
    private ItemClickListener mClickListener;
    private Context mContext;

    public PictureAdapter(Context context, List<GanHuo> picList) {
        this.picList = picList;
        mContext = context;
    }

    @NonNull
    @Override
    public PictureViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        // 載入佈局 item_main_activity_rv.xml
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_main_activity_rv,
                parent, false);
        return new PictureViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull final PictureViewHolder holder, final int position) {
        if (picList != null) {
            GanHuo ganHuo = picList.get(position);
            String url = ganHuo.getUrl();
            String who = ganHuo.getWho();
            
            // Glide圖片載入
            RequestOptions requestOptions = RequestOptions.placeholderOf(R.mipmap.ic_launcher)
                    .error(R.mipmap.ic_refresh_white_24dp)
                    .useAnimationPool(true);
            Glide.with(mContext)
                    .load(url)
                    .apply(requestOptions)
                    .into(holder.imageView);
            holder.textView.setText(who);
            // 點選事件
            if (mClickListener != null) {
                holder.textView.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        mClickListener.onItemClick(PictureAdapter.this, v, position);
                    }
                });
                holder.textView.setOnLongClickListener(new View.OnLongClickListener() {
                    @Override
                    public boolean onLongClick(View v) {
                        mClickListener.onItemLongClick(PictureAdapter.this, v, position);
                        return true;
                    }
                });

                holder.imageView.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        mClickListener.onItemClick(PictureAdapter.this, v, position);
                    }
                });
                holder.imageView.setOnLongClickListener(new View.OnLongClickListener() {
                    @Override
                    public boolean onLongClick(View v) {
                        mClickListener.onItemLongClick(PictureAdapter.this, v, position);
                        return true;
                    }
                });
            }
        } else {
            // default url
            RequestOptions requestOptions = RequestOptions.placeholderOf(R.mipmap.ic_launcher)
                    .error(R.mipmap.ic_refresh_white_24dp);
            Glide.with(mContext)
                    .load(R.mipmap.ic_launcher)
                    .apply(requestOptions)
                    .into(holder.imageView);
            holder.textView.setText("看不到的妹紙");
        }
    }

    @Override
    public int getItemCount() {
        if (picList == null || picList.size() < 1) {
            return 0;
        }
        return picList.size();
    }

    // ViewHolder 用於存放 View,這個View也就是 前面設定的 .xml,RecyclerView 通過 ViewHolder進行復用
    static class PictureViewHolder extends RecyclerView.ViewHolder {

        private View mView;
        private ImageView imageView;
        private TextView textView;

        public PictureViewHolder(View itemView) {
            super(itemView);
            mView = itemView;
            initView();
        }

        private void initView() {
            imageView = mView.findViewById(R.id.iv_meizhi);
            textView = mView.findViewById(R.id.tv_title);
        }
    }

    public ItemClickListener getClickListener() {
        return mClickListener;
    }

    public void setClickListener(ItemClickListener clickListener) {
        mClickListener = clickListener;
    }

    // 點選事件介面
    public interface ItemClickListener {

        void onItemClick(RecyclerView.Adapter adapter,
                         View view, int position);

        void onItemLongClick(RecyclerView.Adapter adapter,
                             View view, int position);
    }
}

實現 Adapter 之後,建立例項,然後用 RecyclerView setAdapter。


 // mPicList 就是用於存放資料的
 mAdapter = new PictureAdapter(this, mPicList);
 mRecyclerView.setAdapter(mAdapter);

請求資料,通過網路請求資料 Retrofit + Rxjava

private void requestData(int index) {
        // 開始重新整理
        setRefreshing(true);
        RetrofitClient.getRetrofitClientInstance()
                .requestNetForData(NetApi.BASE_URL, RequestDataService.class)
                .getGanHuoData("福利", 10, index)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeOn(Schedulers.io())
                .subscribe(new DefaultObserver<BaseEntity<GanHuo>>() {
                    @Override
                    public void onNext(BaseEntity<GanHuo> ganHuo) {
                        if (ganHuo != null && !ganHuo.isError()) {
                            // 重新整理列表
                            refreshRecyclerView(ganHuo.getResults());
                        }
                    }

                    @Override
                    public void onError(Throwable e) {
                        setRefreshing(false);
                    }

                    @Override
                    public void onComplete() {
                        // 重新整理結束,如果是下拉重新整理,滾動到最頂部
                        setRefreshing(false);
                        if (mIndex == 1) {
                            mRecyclerView.smoothScrollToPosition(0);
                        }
                    }
                });
    }

重新整理 RecyclerView,有上拉載入和下拉重新整理,對於不同的情況做了簡單的處理,防止更新資料時,有閃爍現象。

private void refreshRecyclerView(List<GanHuo> ganHuoList) {
        if (ganHuoList == null || ganHuoList.size() < 1) {
            return;
        }
        if (mIndex == 1) {
            mPicList.clear();
            mPicList.addAll(ganHuoList);
            mAdapter.notifyDataSetChanged();
        } else {
            mPicList.addAll(ganHuoList);
            mAdapter.notifyItemRangeInserted(mAdapter.getItemCount()
                    , ganHuoList.size());
        }
    }

此時,就可以執行,能夠看到載入的圖片列表了。

load_refresh

這裡面有 RecyclerView的滑動事件監聽,用來滑動到底部時,做上拉載入更多資料。


mRecyclerView.addOnScrollListener(
                new RecyclerView.OnScrollListener() {
                    @Override
                    public void onScrolled(RecyclerView rv, int dx, int dy) {
                    // 判斷是否滑動到最底部
                        if (!mSwipeRefreshLayout.isRefreshing() &&
                                layoutManager.findLastCompletelyVisibleItemPosition() >= mAdapter.getItemCount() - 1) {

                            mIndex += 1;
                            requestData(mIndex);
                        }
                    }
                }
        );

5. 設定分割線

在圖中看出,已經設定了分割線。設定分割線相對簡單,主要有兩種方法:

  • 在 xml 佈局中新增 0.5 dp的View
  • 使用 ItemDecoration

第一種就不介紹了,自己嘗試弄一下。第二種實現ItemDecoration,系統預設只給出來了一種實現 DividerItemDecoration,
一般情況下,也夠用了,除了改一下顏色之類的。


// 新增分割線
// 第二個引數設定方向,垂直佈局設定垂直分割線,水平佈局設定水平分割線
mRecyclerView.addItemDecoration(new DividerItemDecoration(
        this, DividerItemDecoration.VERTICAL));

分割線的顏色怎麼修改的?
DividerItemDecoration 的顏色採用的應是主題的顏色,所以要改變顏色,需要自己定義一個顏色,然後加到應用的主題中。

在 drawable 中定義一個 shape

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

    // 漸變顏色
    <gradient
        android:centerColor="#ff00ff00"
        android:endColor="#ff0000ff"
        android:startColor="#ffff0000"
        android:type="linear" />
        
        // 分割線寬度
    <size android:height="4dp"/>

</shape>

加到應用主題中

<!-- Application theme. -->
    <style name="AppTheme1" parent="Theme.AppCompat.NoActionBar">
        <item name="colorPrimary">@color/theme_primary</item>
        <item name="colorPrimaryDark">@color/theme_primary_dark</item>
        <item name="colorAccent">@color/md_red_400</item>
        // 分割線主題顏色設定
        <item name="android:listDivider">@drawable/divider_shape</item>
        <!-- 加入toolbar溢位【彈出】選單的風格 -->
        <item name="actionOverflowMenuStyle">@style/OverflowMenuStyle</item>

   </style>

這樣就能改變分割線顏色,同時寬度也可以 shape 中修改

DividerItemDecoration

6. 設定動畫

設定動畫相對比較簡單,呼叫 setItemAnimator 即可,動畫可以使用提供的預設的動畫,或者自己實現 RecyclerView.ItemAnimator,達到自己想要的結果。


    // 新增動畫
    mRecyclerView.setItemAnimator(new DefaultItemAnimator());
    

動畫用於資料改變時顯示的動畫效果,這裡在 Adapter 中新增兩個方法,測試一下


/**
     * 新增測試,用於展現動畫
     * @param position
     */
    public void indertTest(int position){
        picList.add(1,new GanHuo());
        // 這裡更新資料集不是用adapter.notifyDataSetChanged()而是
        // notifyItemInserted(position)與notifyItemRemoved(position)
        // 否則沒有動畫效果。
        notifyItemInserted(position);
    }

    /**
     * 刪除測試,用於展現動畫
     * @param position
     */
    public void removeTest(int position){
        picList.remove(picList.get(position));
        notifyItemRemoved(position);
    }

animatirs

想看看到更多的動畫效果,可以參考 github 上的開源專案 RecyclerViewItemAnimators

7. 設定點選事件

對於 RecyclerView 的點選事件,系統沒有提供介面 ClickListener和 LongClickListener,需要自己實現。常用的方式一般有兩種:

  • mRecyclerView.addOnItemTouchListener(listener);根據手勢動作判斷
  • 第二種是自己在 Adapter 中設定介面,然後將實現傳遞進去

這裡給出第二種的方式,看一下效果:

在 onBindViewHolder 方法中程式碼有點多,主要看setOnClickListener 部分,實際上還是給普通的控制元件設定點選事件,在 onClick 中回撥我們設定的介面,這樣執行的方法就是我們想要的動作了。

public class PictureAdapter extends RecyclerView.Adapter<PictureAdapter.PictureViewHolder> {

    ...
    @Override
    public void onBindViewHolder(@NonNull final PictureViewHolder holder, final int position) {
        if (picList != null) {
            GanHuo ganHuo = picList.get(position);
            String url = ganHuo.getUrl();
            String who = ganHuo.getWho();
            RequestOptions requestOptions = RequestOptions.placeholderOf(R.mipmap.ic_launcher)
                    .error(R.mipmap.ic_refresh_white_24dp)
                    .useAnimationPool(true);
            Glide.with(mContext)
                    .load(url)
                    .apply(requestOptions)
                    .into(holder.imageView);
            holder.textView.setText(who);
            // 點選事件
            if (mClickListener != null) {
                holder.textView.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        mClickListener.onItemClick(PictureAdapter.this, v, position);
                    }
                });
                holder.textView.setOnLongClickListener(new View.OnLongClickListener() {
                    @Override
                    public boolean onLongClick(View v) {
                        mClickListener.onItemLongClick(PictureAdapter.this, v, position);
                        return true;
                    }
                });

                holder.imageView.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        mClickListener.onItemClick(PictureAdapter.this, v, position);
                    }
                });
                holder.imageView.setOnLongClickListener(new View.OnLongClickListener() {
                    @Override
                    public boolean onLongClick(View v) {
                        mClickListener.onItemLongClick(PictureAdapter.this, v, position);
                        return true;
                    }
                })