Android RecyclerView詳解

cswhale發表於2018-01-09

介紹

RecyclerView用於在有限的視窗展現大量的資料,其實早已經有了類似的控制元件,如ListView、GridView,那麼相比它們,RecyclerView有什麼樣優勢呢?
RecyclerView標準化了ViewHolder,而且異常的靈活,可以輕鬆實現ListView實現不了的樣式和功能,通過佈局管理器LayoutManager可控制Item的佈局方式,通過設定Item操作動畫自定義Item新增和刪除的動畫,通過設定Item之間的間隔樣式,自定義間隔。

可實現效果

  • 設定佈局管理器以控制Item的佈局方式,橫向、豎向以及瀑布流方式。

  • 可設定Item操作的動畫(刪除或者新增等)

  • 可設定Item的間隔樣式(可繪製)

關於Item的點選和長按事件,需要使用者自己去實現

使用

  • 使用RecyclerView時候,必須指定一個介面卡Adapter和一個佈局管理器LayoutManager。
  • 介面卡繼承RecyclerView.Adapter類,具體實現類似ListView的介面卡,取決於資料資訊以及展示的UI。
  • 佈局管理器用於確定RecyclerView中Item的展示方式以及決定何時複用已經不可見的Item,避免重複建立以及執行高成本的findViewById()方法

用法

示例

mRecyclerView = (RecyclerView) findViewById(R.id.my_recycler_view);
LinearLayoutManager mLayoutManager=new LinearLayoutManager(this);
// 設定佈局管理器
mRecyclerView.setLayoutManager(mLayoutManager);
// 設定adapter
mRecyclerView.setAdapter(mAdapter);
// 設定Item新增和移除的動畫
mRecyclerView.setItemAnimator(new DefaultItemAnimator());
// 設定Item之間間隔樣式
mRecyclerView.addItemDecoration(mDividerItemDecoration);

基本使用

首先需要在在 build.gradle 檔案中引入 RecyclerView 類

  compile 'com.android.support:recyclerview-v7:23.4.0'

Fragment程式碼

package com.demo.fragment;

import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import com.demo.R;
import com.demo.adapter.VideoRecyclerViewAdapter;
import com.demo.bean.VideoBean;

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

public class ListViewFragment extends Fragment{

    public static ListViewFragment newInstance() {
        return new ListViewFragment();
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.activity_recycler_view, container, false);
        initView(view);
        return view;
    }

    private void initView(View view) {
        RecyclerView recyclerView = view.findViewById(R.id.rv);
        LinearLayoutManager layoutManager=new LinearLayoutManager(getActivity());
        layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
        recyclerView.setLayoutManager(layoutManager);// 設定佈局管理器
        DefaultItemAnimator itemAnimator = new DefaultItemAnimator();
        recyclerView .setItemAnimator(itemAnimator);// 設定Item新增和移除的動畫
        itemAnimator.setSupportsChangeAnimations(false);
        recyclerView.setAdapter(new VideoRecyclerViewAdapter(getVideoList(), getActivity()));

    }

    public List<VideoBean> getVideoList() {
        List<VideoBean> videoList = new ArrayList<>();
        //...新增資料

        return videoList;
    }
}

R.layout.activity_recycler_view

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">
    <android.support.v7.widget.RecyclerView
        android:id="@+id/rv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</LinearLayout>

RecyclerView介面卡Adapter程式碼

package com.demo.adapter;

import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;

import com.bumptech.glide.Glide;
import com.demo.R;
import com.demo.bean.VideoBean;
import com.demo.view.CustomVideoView ;

import java.util.List;

public class VideoRecyclerViewAdapter extends RecyclerView.Adapter<VideoRecyclerViewAdapter.VideoHolder> {


        private List<VideoBean> videos;
        private Context context;

        public VideoRecyclerViewAdapter(List<VideoBean> videos, Context context) {
            this.videos = videos;
            this.context = context;
        }

        @Override
        public VideoHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View itemView = LayoutInflater.from(context).inflate(R.layout.item_video_auto_play, parent, false);
            return new VideoHolder(itemView);

        }

        @Override
        public void onBindViewHolder(final VideoHolder holder, int position) {

            VideoBean videoBean = videos.get(position);
            holder.title.setText(videoBean.getTitle());
            holder.videoView .setPlayUrl(videoBean.getUrl());

        }

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

        public class VideoHolder extends RecyclerView.ViewHolder {

            private CustomVideoView videoView;
            private TextView title;

            VideoHolder(View itemView) {
                super(itemView);
                videoView= (CustomVideoView )itemView.findViewById(R.id.video_player);
                int widthPixels = context.getResources().getDisplayMetrics().widthPixels;
                videoView .setLayoutParams(new LinearLayout.LayoutParams(widthPixels, widthPixels / 16 * 9));
                title = itemView.findViewById(R.id.tv_title);
            }
        }
    }

佈局管理器:RecyclerView.LayoutManager

上述程式碼中mLayoutManager 物件是佈局管理器,RecyclerView提供了三種佈局管理器:

  • LinerLayoutManager(線性):以垂直或者水平列表方式展示Item
  • GridLayoutManager (網格):以網格方式展示Item
  • StaggeredGridLayoutManager(瀑布流): 以瀑布流方式展示Item

介面卡:RecyclerView.Adapter

RecyclerView.Adapter的使用方式和ListView的ListAdapter 類似,向RecyclerView提供顯示的資料。
但是RecyclerView.Adapter做了一件了不起的優化,那就是RecyclerView.Adapter的

 public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) 

方法能夠保證當前RecyclerView是確實需要你建立一個新的ViewHolder物件。而ListView的ListAdapter 對應的方法

    @Override
    public View getView(int i, View view, ViewGroup viewGroup)

需要在方法內部判斷是重新建立一個View還是重新整理一個View的資料,而不明所以的客戶可能每次都會返回一個新建立的View造成ListView的卡頓和資源的浪費;建立和重新整理作為兩個不同的功能本來就應該在兩個方法中實現—慶幸的是RecyclerView.Adapter解決了這個問題

檢視容器:RecyclerView.ViewHolder

RecyclerView中強制客戶使用ViewHolder,談及ListView的時候就經常說到使用ViewHolder來進行優化。使用ViewHolder其中一點好處是能夠避免重複呼叫方法findViewById(),對當前item的View中的子View進行管理。
ViewHolder 描述RecylerView中某個位置的itemView和後設資料資訊,屬於Adapter的一部分。其實該類通常用於儲存 findViewById 的結果
ViewHolder的mFlags屬性

  • FLAG_BOUND ——ViewHolder已經繫結到某個位置,mPosition、mItemId、mItemViewType都有效
  • FLAG_UPDATE ——ViewHolder繫結的View對應的資料過時需要重新繫結,mPosition、mItemId還是一致的
  • FLAG_INVALID ——ViewHolder繫結的View對應的資料無效,需要完全重新繫結不同的資料
  • FLAG_REMOVED ——ViewHolder對應的資料已經從資料集移除
  • FLAG_NOT_RECYCLABLE ——ViewHolder不能複用
  • FLAG_RETURNED_FROM_SCRAP ——這個狀態的ViewHolder會加到scrap list被複用。
  • FLAG_CHANGED ——ViewHolder內容發生變化,通常用於表明有ItemAnimator動畫
  • FLAG_IGNORE ——ViewHolder完全由LayoutManager管理,不能複用
  • FLAG_TMP_DETACHED ——ViewHolder從父RecyclerView臨時分離的標誌,便於後續移除或新增回來
  • FLAG_ADAPTER_POSITION_UNKNOWN ——ViewHolder不知道對應的Adapter的位置,直到繫結到一個新位置
  • FLAG_ADAPTER_FULLUPDATE ——方法 addChangePayload(null) 呼叫時設定

間隔樣式:RecyclerView.ItemDecoration

  • 用於繪製itemView之間的一些特殊UI,比如在itemView之前設定空白區域、畫線等。
  • RecyclerView 將itemView和裝飾UI分隔開來,裝飾UI即 ItemDecoration ,主要用於繪製item間的分割線、高亮或者margin等
  • 通過recyclerView.addItemDecoration(new DividerDecoration(this))對item新增裝飾;對RecyclerView設定多個ItemDecoration,列表展示的時候會遍歷所有的ItemDecoration並呼叫裡面的繪製方法,對Item進行裝飾。
  • public void onDraw(Canvas c, RecyclerView parent) 裝飾的繪製在Item條目繪製之前呼叫,所以這有可能被Item的內容所遮擋
  • public void onDrawOver(Canvas c, RecyclerView parent) 裝飾的繪製在Item條目繪製之後呼叫,因此裝飾將浮於Item之上
  • public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) 與padding或margin類似,LayoutManager在測量階段會呼叫該方法,計算出每一個Item的正確尺寸並設定偏移量

展示效果和ListView基本上無差別,但是Item之間並沒有分割線,在xml去找divider屬性的時候,發現RecyclerView沒有divider屬性,當然也可以在Item佈局中加上分割線,但是這樣做並不是很優雅。
其實RecyclerView是支援自定義間隔樣式的。通過

mRecyclerView.addItemDecoration()

來設定我們定義好的間隔樣式

自定義間隔樣式需要繼承RecyclerView.ItemDecoration類,該類是個抽象類,主要有三個方法

  • onDraw(Canvas c, RecyclerView parent, State state):在Item繪製之前被呼叫,該方法主要用於繪製間隔樣式
  • onDrawOver(Canvas c, RecyclerView parent, State state):在Item繪製之前被呼叫,該方法主要用於繪製間隔樣式
  • getItemOffsets(Rect outRect, View view, RecyclerView parent, State state):設定item的偏移量,偏移的部分用於填充間隔樣式,在RecyclerView的onMesure()中會呼叫該方法

onDraw()和onDrawOver()這兩個方法都是用於繪製間隔樣式,我們只需要複寫其中一個方法即可。直接來看一下自定義的間隔樣式的實現,參考官方例項

public class MyDividerItemDecoration extends RecyclerView.ItemDecoration {

    private static final int[] ATTRS = new int[]{
            android.R.attr.listDivider
    };
    public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;
    public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;
    /**
     * 用於繪製間隔樣式
     */
    private Drawable mDivider;
    /**
     * 列表的方向,水平/豎直
     */
    private int mOrientation;


    public MyDividerItemDecoration(Context context, int orientation) {
        // 獲取預設主題的屬性
        final TypedArray a = context.obtainStyledAttributes(ATTRS);
        mDivider = a.getDrawable(0);
        a.recycle();
        setOrientation(orientation);
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        // 繪製間隔
        if (mOrientation == VERTICAL_LIST) {
            drawVertical(c, parent);
        } else {
            drawHorizontal(c, parent);
        }
    }

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

    private void setOrientation(int orientation) {
        if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {
            throw new IllegalArgumentException("invalid orientation");
        }
        mOrientation = orientation;
    }

    /**
     * 繪製間隔
     */
    private void drawVertical(Canvas c, RecyclerView parent) {
        final int left = parent.getPaddingLeft();
        final int right = parent.getWidth() - parent.getPaddingRight();
        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                    .getLayoutParams();
            final int top = child.getBottom() + params.bottomMargin +
                    Math.round(ViewCompat.getTranslationY(child));
            final int bottom = top + mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }

    /**
     * 繪製間隔
     */
    private void drawHorizontal(Canvas c, RecyclerView parent) {
        final int top = parent.getPaddingTop();
        final int bottom = parent.getHeight() - parent.getPaddingBottom();
        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                    .getLayoutParams();
            final int left = child.getRight() + params.rightMargin +
                    Math.round(ViewCompat.getTranslationX(child));
            final int right = left + mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }
}

然後在程式碼中設定RecyclerView的間隔樣式

mRecyclerView.addItemDecoration(new MyDividerItemDecoration(this, LinearLayoutManager.VERTICAL));

動畫:RecyclerView.ItemAnimator

RecyclerView可以設定列表中Item刪除和新增的動畫,在v7包中給我們提供了一種預設的Item刪除和新增的動畫,如果沒有特殊的需求,預設使用這個動畫即可

// 設定Item新增和移除的動畫
mRecyclerView.setItemAnimator(new DefaultItemAnimator());

設定的動畫用於在 item 項資料變化時的動畫效果
當呼叫 Adapter 的 notifyItemChanged、notifyItemInserted、notifyItemMoved 等方法,會觸發該物件顯示相應的動畫。
RecyclerView 的 ItemAnimator 使得 item 的動畫實現變得簡單而樣式豐富,我們可以自定義 item 項不同操作(如新增,刪除)的動畫效果;
ItemAnimator 觸發於以下三種事件:

  • 某條資料被插入到資料集合中 ,對應 public final void notifyItemInserted(int position) 方法
  • 從資料集合中移除某條資料 ,對應 public final void notifyItemRemoved(int position) 方法
  • 更改資料集合中的某條資料,對應 public final void notifyItemChanged(int position) 方法

注意:notifyDataSetChanged(),會觸發列表的重繪,並不會出現任何動畫效果
使用:Animator使用到的邏輯比較多,因此最方便的就是使用第三方庫:https://github.com/wasabeef/recyclerview-animators

點選事件

RecyclerView並沒有像ListView一樣暴露出Item點選事件或者長按事件處理的api,也就是說使用RecyclerView時候,需要我們自己來實現Item的點選和長按等事件的處理。
實現方法有很多

  • 可以監聽RecyclerView的Touch事件然後判斷手勢做相應的處理
  • 可以通過在繫結ViewHolder的時候設定監聽,然後通過Apater回撥出去
  • 使用點選、長按事件支援類

第二種方法:在繫結ViewHolder的時候設定監聽,通過Apater回撥出去 Adapter 的完整程式碼

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

    /**
     * 展示資料
     */
    private ArrayList<String> mData;

    /**
     * 事件回撥監聽
     */
    private RecyclerViewAdapter.OnItemClickListener onItemClickListener;

    public RecyclerViewAdapter(ArrayList<String> data) {
        this.mData = data;
    }

    public void updateData(ArrayList<String> data) {
        this.mData = data;
        notifyDataSetChanged();
    }

    /**
     * 新增新的Item
     */
    public void addNewItem() {
        if(mData == null) {
            mData = new ArrayList<>();
        }
        mData.add(0, "new Item");
        notifyItemInserted(0);
    }

    /**
     * 刪除Item
     */
    public void deleteItem() {
        if(mData == null || mData.isEmpty()) {
            return;
        }
        mData.remove(0);
        notifyItemRemoved(0);
    }

    /**
     * 設定回撥監聽
     * 
     * @param listener
     */
    public void setOnItemClickListener(MyAdapter.OnItemClickListener listener) {
        this.onItemClickListener = listener;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        // 例項化展示的view
        View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.view_rv_item, parent, false);
        // 例項化viewholder
        ViewHolder viewHolder = new ViewHolder(v);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(final ViewHolder holder, int position) {
        // 繫結資料
        holder.mTv.setText(mData.get(position));

        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(final View v) {
                if(onItemClickListener != null) {
                    int pos = holder.getLayoutPosition();
                    onItemClickListener.onItemClick(holder.itemView, pos);
                }
            }
        });

        holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                if(onItemClickListener != null) {
                    int pos = holder.getLayoutPosition();
                    onItemClickListener.onItemLongClick(holder.itemView, pos);
                }
                //表示此事件已經消費,不會觸發單擊事件
                return true;
            }
        });
    }

    @Override
    public int getItemCount() {
        return mData == null ? 0 : mData.size();
    }

    public static class ViewHolder extends RecyclerView.ViewHolder {

        TextView mTv;

        public ViewHolder(View itemView) {
            super(itemView);
            mTv = (TextView) itemView.findViewById(R.id.item_tv);
        }
    }

    public interface OnItemClickListener {
        void onItemClick(View view, int position);
        void onItemLongClick(View view, int position);
    }
}

Activity 設定 Adapter 事件監聽

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

    @Override
    public void onItemLongClick(View view, int position) {
        Toast.makeText(MyActivity.this,"long click " + position + " item", Toast.LENGTH_SHORT).show();
    }
});

第三種方法:使用點選、長按事件支援類程式碼

參考 Hugo 的文章:Getting your clicks on RecyclerView

  1. 先要準備一份resources
    res -> values -> ids.xml ->
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <item name="item_click_support" type="id" />
</resources>
  1. 具體的支援類
public class ItemClickSupport {
    private final RecyclerView mRecyclerView;
    private OnItemClickListener mOnItemClickListener;
    private OnItemLongClickListener mOnItemLongClickListener;
    private View.OnClickListener mOnClickListener = new View.OnClickListener() {
        @Override public void onClick(View v) {
            if (mOnItemClickListener != null) {
                RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v);
                mOnItemClickListener.onItemClicked(mRecyclerView, holder.getAdapterPosition(), v);
            }
        }
    };
    private View.OnLongClickListener mOnLongClickListener = new View.OnLongClickListener() {
        @Override public boolean onLongClick(View v) {
            if (mOnItemLongClickListener != null) {
                RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v);
                return mOnItemLongClickListener.onItemLongClicked(mRecyclerView, holder.getAdapterPosition(), v);
            }
            return false;
        }
    };
    private RecyclerView.OnChildAttachStateChangeListener mAttachListener = new RecyclerView.OnChildAttachStateChangeListener() {
        @Override public void onChildViewAttachedToWindow(View view) {
            if (mOnItemClickListener != null) {
                view.setOnClickListener(mOnClickListener);
            }
            if (mOnItemLongClickListener != null) {
                view.setOnLongClickListener(mOnLongClickListener);
            }
        }

        @Override public void onChildViewDetachedFromWindow(View view) {

        }
    };

    private ItemClickSupport(RecyclerView recyclerView) {
        mRecyclerView = recyclerView;
        mRecyclerView.setTag(R.id.item_click_support, this);
        mRecyclerView.addOnChildAttachStateChangeListener(mAttachListener);
    }

    public static ItemClickSupport addTo(RecyclerView view) {
        ItemClickSupport support = (ItemClickSupport) view.getTag(R.id.item_click_support);
        if (support == null) {
            support = new ItemClickSupport(view);
        }
        return support;
    }

    public static ItemClickSupport removeFrom(RecyclerView view) {
        ItemClickSupport support = (ItemClickSupport) view.getTag(R.id.item_click_support);
        if (support != null) {
            support.detach(view);
        }
        return support;
    }

    public ItemClickSupport setOnItemClickListener(OnItemClickListener listener) {
        mOnItemClickListener = listener;
        return this;
    }

    public ItemClickSupport setOnItemLongClickListener(OnItemLongClickListener listener) {
        mOnItemLongClickListener = listener;
        return this;
    }

    private void detach(RecyclerView view) {
        view.removeOnChildAttachStateChangeListener(mAttachListener);
        view.setTag(R.id.item_click_support, null);
    }

    // 點選介面
    public interface OnItemClickListener {
        void onItemClicked(RecyclerView recyclerView, int position, View v);
    }

    // 長按介面
    public interface OnItemLongClickListener {
        boolean onItemLongClicked(RecyclerView recyclerView, int position, View v);
    }
}
  1. 在setAdapter()之後呼叫
// 點選
ItemClickSupport.addTo(rv).setOnItemClickListener(new ItemClickSupport.OnItemClickListener() {
    @Override public void onItemClicked(RecyclerView recyclerView, int position, View v) {
        Toast.makeText(MainActivity.this, mDatas.get(position), Toast.LENGTH_SHORT).show();
    }
});

// 長按
ItemClickSupport.addTo(rv).setOnItemLongClickListener(new ItemClickSupport.OnItemLongClickListener() {
    @Override public boolean onItemLongClicked(RecyclerView recyclerView, int position, View v) {

        Toast.makeText(MainActivity.this, "長按" + mDatas.get(position) + "已刪除", Toast.LENGTH_SHORT).show();

        // 需要自己處理position在集合中的位置(需考慮頭、身、腳佈局數量)
        mDatas.remove(position);
        if (lastVisible + 1 == mAdapter.getItemCount()) {
            addmore();
        }
        mAdapter.notifyItemRemoved(position);

        // 消耗事件
        return true;
    }
});

總結

目前而言,我們已經知道RecyclerView的一些功能如下

  • 水平列表展示,設定LayoutManager的方向性
  • 豎直列表展示,設定LayoutManager的方向性
  • 自定義間隔,RecyclerView.addItemDecoration()
  • Item新增和刪除動畫,RecyclerView.setItemAnimator()

所以在專案中如果再遇見列表類的佈局,就可以優先考慮使用 RecyclerView,更靈活更快捷的使用方式會給編碼帶來不一樣的體驗

相關文章