天天看點

安卓開發筆記(十):更新ListView為RecylerView的使用

安卓開發筆記(十):更新ListView為RecylerView的使用

概述

RecyclerView是什麼

從Android 5.0開始,谷歌公司推出了一個用于大量資料展示的新控件RecylerView,可以用來代替傳統的ListView,更加強大和靈活。RecyclerView的官方定義如下:

A flexible view for providing a limited window into a large data set.

從定義可以看出,flexible(可擴充性)是RecyclerView的特點。

RecyclerView是support-v7包中的新元件,是一個強大的滑動元件,與經典的ListView相比,同樣擁有item回收複用的功能,這一點從它的名字Recyclerview即回收view也可以看出。

RecyclerView的優點

RecyclerView并不會完全替代ListView(這點從ListView沒有被标記為@Deprecated可以看出),兩者的使用場景不一樣。但是RecyclerView的出現會讓很多開源項目被廢棄,例如橫向滾動的ListView, 橫向滾動的GridView, 瀑布流控件,因為RecyclerView能夠實作所有這些功能。

比如:有一個需求是螢幕豎着的時候的顯示形式是ListView,螢幕橫着的時候的顯示形式是2列的GridView,此時如果用RecyclerView,則通過設定LayoutManager一行代碼實作替換。

RecylerView相對于ListView的優點羅列如下:

  • RecyclerView封裝了viewholder的回收複用,也就是說RecyclerView标準化了ViewHolder,編寫Adapter面向的是ViewHolder而不再是View了,複用的邏輯被封裝了,寫起來更加簡單。

    直接省去了listview中convertView.setTag(holder)和convertView.getTag()這些繁瑣的步驟。

  • 提供了一種插拔式的體驗,高度的解耦,異常的靈活,針對一個Item的顯示RecyclerView專門抽取出了相應的類,來控制Item的顯示,使其的擴充性非常強。
  • 設定布局管理器以控制Item的布局方式,橫向、豎向以及瀑布流方式

    例如:你想控制橫向或者縱向滑動清單效果可以通過LinearLayoutManager這個類來進行控制(與GridView效果對應的是GridLayoutManager,與瀑布流對應的還StaggeredGridLayoutManager等)。也就是說RecyclerView不再拘泥于ListView的線性展示方式,它也可以實作GridView的效果等多種效果。

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

    通過繼承RecyclerView的ItemDecoration這個類,然後針對自己的業務需求去書寫代碼。

  • 可以控制Item增删的動畫,可以通過ItemAnimator這個類進行控制,當然針對增删的動畫,RecyclerView有其自己預設的實作。

但是關于Item的點選和長按事件,需要使用者自己去實作。

基本使用

recyclerView = (RecyclerView) findViewById(R.id.recyclerView);  
LinearLayoutManager layoutManager = new LinearLayoutManager(this );  
//設定布局管理器  
recyclerView.setLayoutManager(layoutManager);  
//設定為垂直布局,這也是預設的  
layoutManager.setOrientation(OrientationHelper. VERTICAL);  
//設定Adapter  
recyclerView.setAdapter(recycleAdapter);  
 //設定分隔線  
recyclerView.addItemDecoration( new DividerGridItemDecoration(this ));  
//設定增加或删除條目的動畫  
recyclerView.setItemAnimator( new DefaultItemAnimator());  
           

在使用RecyclerView時候,必須指定一個擴充卡Adapter和一個布局管理器LayoutManager。擴充卡繼承

RecyclerView.Adapter

類,具體實作類似ListView的擴充卡,取決于資料資訊以及展示的UI。布局管理器用于确定RecyclerView中Item的展示方式以及決定何時複用已經不可見的Item,避免重複建立以及執行高成本的

findViewById()

方法。

可以看見RecyclerView相比ListView會多出許多操作,這也是RecyclerView靈活的地方,它将許多動能暴露出來,使用者可以選擇性的自定義屬性以滿足需求。

引用

在build.gradle檔案中引入該類。

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

布局

Activity布局檔案activity_rv.xml

...

Item的布局檔案item_1.xml

建立擴充卡

标準實作步驟如下:

① 建立Adapter:建立一個繼承

RecyclerView.Adapter<VH>

的Adapter類(VH是ViewHolder的類名)

② 建立ViewHolder:在Adapter中建立一個繼承

RecyclerView.ViewHolder

的靜态内部類,記為VH。ViewHolder的實作和ListView的ViewHolder實作幾乎一樣。

③ 在Adapter中實作3個方法:

  • onCreateViewHolder()

    這個方法主要生成為每個Item inflater出一個View,但是該方法傳回的是一個ViewHolder。該方法把View直接封裝在ViewHolder中,然後我們面向的是ViewHolder這個執行個體,當然這個ViewHolder需要我們自己去編寫。

需要注意的是在

onCreateViewHolder()

中,映射Layout必須為

View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_1, parent, false);
           

而不能是:

View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_1, null);
           
  • onBindViewHolder()

    這個方法主要用于适配渲染資料到View中。方法提供給你了一viewHolder而不是原來的convertView。

  • getItemCount()

    這個方法就類似于BaseAdapter的getCount方法了,即總共有多少個條目。

可以看出,RecyclerView将ListView中

getView()

的功能拆分成了

onCreateViewHolder()

onBindViewHolder()

基本的Adapter實作如下:

// ① 建立Adapter
public class NormalAdapter extends RecyclerView.Adapter<NormalAdapter.VH>{
    //② 建立ViewHolder
    public static class VH extends RecyclerView.ViewHolder{
        public final TextView title;
        public VH(View v) {
            super(v);
            title = (TextView) v.findViewById(R.id.title);
        }
    }
    
    private List<String> mDatas;
    public NormalAdapter(List<String> data) {
        this.mDatas = data;
    }

    //③ 在Adapter中實作3個方法
    @Override
    public void onBindViewHolder(VH holder, int position) {
        holder.title.setText(mDatas.get(position));
        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //item 點選事件
            }
        });
    }

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

    @Override
    public VH onCreateViewHolder(ViewGroup parent, int viewType) {
        //LayoutInflater.from指定寫法
        View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_1, parent, false);
        return new VH(v);
    }
}
           

設定RecyclerView

建立完Adapter,接着對RecyclerView進行設定,一般來說,需要為RecyclerView進行四大設定,也就是後文說的四大組成:

  • Layout Manager(必選)
  • Adapter(必選)
  • Item Decoration(可選,預設為空)
  • Item Animator(可選,預設為DefaultItemAnimator)

如果要實作ListView的效果,隻需要設定Adapter和Layout Manager,如下:

List<String> data = initData();
RecyclerView rv = (RecyclerView) findViewById(R.id.rv);
rv.setLayoutManager(new LinearLayoutManager(this));
rv.setAdapter(new NormalAdapter(data));
           

四大組成

RecyclerView的四大組成是:

  • Layout Manager:Item的布局。
  • Adapter:為Item提供資料。
  • Item Decoration:Item之間的Divider。
  • Item Animator:添加、删除Item動畫。

Layout Manager布局管理器

在最開始就提到,RecyclerView 能夠支援各種各樣的布局效果,這是 ListView 所不具有的功能,那麼這個功能如何實作的呢?其核心關鍵在于 

RecyclerView.LayoutManager

 類中。從前面的基礎使用可以看到,RecyclerView 在使用過程中要比 ListView 多一個 setLayoutManager 步驟,這個 LayoutManager 就是用于控制我們 RecyclerView 最終的展示效果的。

LayoutManager負責RecyclerView的布局,其中包含了Item View的擷取與回收。

RecyclerView提供了三種布局管理器:

  • LinerLayoutManager 以垂直或者水準清單方式展示Item
  • GridLayoutManager 以網格方式展示Item
  • StaggeredGridLayoutManager 以瀑布流方式展示Item

如果你想用 RecyclerView 來實作自己自定義效果,則應該去繼承實作自己的 LayoutManager,并重寫相應的方法,而不應該想着去改寫 RecyclerView。

LayoutManager 常見 API

關于 LayoutManager 的使用有下面一些常見的 API(有些在 LayoutManager 實作的子類中)

canScrollHorizontally();//能否橫向滾動
    canScrollVertically();//能否縱向滾動
    scrollToPosition(int position);//滾動到指定位置

    setOrientation(int orientation);//設定滾動的方向
    getOrientation();//擷取滾動方向

    findViewByPosition(int position);//擷取指定位置的Item View
    findFirstCompletelyVisibleItemPosition();//擷取第一個完全可見的Item位置
    findFirstVisibleItemPosition();//擷取第一個可見Item的位置
    findLastCompletelyVisibleItemPosition();//擷取最後一個完全可見的Item位置
    findLastVisibleItemPosition();//擷取最後一個可見Item的位置
           

上面僅僅是列出一些常用的 API 而已,更多的 API 可以檢視官方文檔,通常你想用 RecyclerView 實作某種效果,例如指定滾動到某個 Item 位置,但是你在 RecyclerView 中又找不到可以調用的 API 時,就可以跑到 LayoutManager 的文檔去看看,基本都在那裡。

另外還有一點關于瀑布流布局效果 StaggeredGridLayoutManager 想說的,看到網上有些文章寫的示例代碼,在設定了 StaggeredGridLayoutManager 後仍要去 Adapter 中動态設定 View 的高度,才能實作瀑布流,這種做法是完全錯誤的,之是以 StaggeredGridLayoutManager 的瀑布流效果出不來,基本是 item 布局的 xml 問題以及資料問題導緻。如果要在 Adapter 中設定 View 的高度,則完全違背了 LayoutManager 的設計理念了。

LinearLayoutManager源碼分析

這裡我們簡單分析LinearLayoutManager的實作。

對于LinearLayoutManager來說,比較重要的幾個方法有:

  • onLayoutChildren()

    : 對RecyclerView進行布局的入口方法。
  • fill()

    : 負責填充RecyclerView。
  • scrollVerticallyBy()

    :根據手指的移動滑動一定距離,并調用

    fill()

    填充。
  • canScrollVertically()

    canScrollHorizontally()

    : 判斷是否支援縱向滑動或橫向滑動。

onLayoutChildren()

的核心實作如下:

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    detachAndScrapAttachedViews(recycler); //将原來所有的Item View全部放到Recycler的Scrap Heap或Recycle Pool
    fill(recycler, mLayoutState, state, false); //填充現在所有的Item View
}
           

RecyclerView的回收機制有個重要的概念,即将資源回收筒分為Scrap Heap和Recycle Pool,其中Scrap Heap的元素可以被直接複用,而不需要調用

onBindViewHolder()

detachAndScrapAttachedViews()

會根據情況,将原來的Item View放入Scrap Heap或Recycle Pool,進而在複用時提升效率。

fill()

是對剩餘空間不斷地調用

layoutChunk()

,直到填充完為止。

layoutChunk()

public void layoutChunk() {
    View view = layoutState.next(recycler); //調用了getViewForPosition()
    addView(view);  //加入View
    measureChildWithMargins(view, 0, 0); //計算View的大小
    layoutDecoratedWithMargins(view, left, top, right, bottom); //布局View
}
           

其中

next()

調用了

getViewForPosition(currentPosition)

,該方法是從RecyclerView的回收機制實作類Recycler中擷取合适的View,在後文的回收機制中會介紹該方法的具體實作。

如果要自定義LayoutManager,可以參考:

Adapter擴充卡

Adapter的使用方式前面已經介紹了,功能就是為RecyclerView提供資料,這裡主要介紹萬能擴充卡的實作。其實萬能擴充卡的概念在ListView就已經存在了,即

base-adapter-helper

這裡我們隻針對RecyclerView,聊聊萬能擴充卡出現的原因。為了建立一個RecyclerView的Adapter,每次我們都需要去做重複勞動,包括重寫

onCreateViewHolder()

,

getItemCount()

、建立ViewHolder,并且實作過程大同小異,是以萬能擴充卡出現了。

萬能擴充卡

這裡講解下萬能擴充卡的實作思路。

我們通過

public abstract class QuickAdapter<T> extends RecyclerView.Adapter<QuickAdapter.VH>

定義萬能擴充卡QuickAdapter類,T是清單資料中每個元素的類型,QuickAdapter.VH是QuickAdapter的ViewHolder實作類,稱為萬能ViewHolder。

首先介紹QuickAdapter.VH的實作:

static class VH extends RecyclerView.ViewHolder{
    private SparseArray<View> mViews;
    private View mConvertView;

    private VH(View v){
        super(v);
        mConvertView = v;
        mViews = new SparseArray<>();
    }

    public static VH get(ViewGroup parent, int layoutId){
        View convertView = LayoutInflater.from(parent.getContext()).inflate(layoutId, parent, false);
        return new VH(convertView);
    }

    public <T extends View> T getView(int id){
        View v = mViews.get(id);
        if(v == null){
            v = mConvertView.findViewById(id);
            mViews.put(id, v);
        }
        return (T)v;
    }

    public void setText(int id, String value){
        TextView view = getView(id);
        view.setText(value);
    }
}
           

其中的關鍵點在于通過

SparseArray<View>

存儲item view的控件,

getView(int id)

的功能就是通過id獲得對應的View(首先在mViews中查詢是否存在,如果沒有,那麼

findViewById()

并放入mViews中,避免下次再執行

findViewById()

)。

QuickAdapter的實作如下:

public abstract class QuickAdapter<T> extends RecyclerView.Adapter<QuickAdapter.VH>{
    private List<T> mDatas;
    public QuickAdapter(List<T> datas){
        this.mDatas = datas;
    }

    public abstract int getLayoutId(int viewType);

    @Override
    public VH onCreateViewHolder(ViewGroup parent, int viewType) {
        return VH.get(parent,getLayoutId(viewType));
    }

    @Override
    public void onBindViewHolder(VH holder, int position) {
        convert(holder, mDatas.get(position), position);
    }

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

    public abstract void convert(VH holder, T data, int position);
    
    static class VH extends RecyclerView.ViewHolder{
        private SparseArray<View> mViews;
        private View mConvertView;
    
        private VH(View v){
            super(v);
            mConvertView = v;
            mViews = new SparseArray<>();
        }
    
        public static VH get(ViewGroup parent, int layoutId){
            View convertView = LayoutInflater.from(parent.getContext()).inflate(layoutId, parent, false);
            return new VH(convertView);
        }
    
        public <T extends View> T getView(int id){
            View v = mViews.get(id);
            if(v == null){
                v = mConvertView.findViewById(id);
                mViews.put(id, v);
            }
            return (T)v;
        }
    
        public void setText(int id, String value){
            TextView view = getView(id);
            view.setText(value);
        }
    }
}
           

其中:

  • getLayoutId(int viewType)

    是根據viewType傳回布局ID。
  • convert()

    做具體的bind操作。

就這樣,萬能擴充卡實作完成了。

通過萬能擴充卡能通過以下方式快捷地建立一個Adapter:

mAdapter = new QuickAdapter<String>(data) {
    @Override
    public int getLayoutId(int viewType) {
        return R.layout.item;
    }

    @Override
    public void convert(VH holder, String data, int position) {
        holder.setText(R.id.text, data);
        //holder.itemView.setOnClickListener(); 此處還可以添加點選事件
    }
};
           

是不是很友善。當然複雜情況也可以輕松解決。

mAdapter = new QuickAdapter<Model>(data) {
    @Override
    public int getLayoutId(int viewType) {
        switch(viewType){
            case TYPE_1:
                return R.layout.item_1;
            case TYPE_2:
                return R.layout.item_2;
        }
    }

    @Override
    public int getItemViewType(int position) {
        if(position % 2 == 0){
            return TYPE_1;
        } else{
            return TYPE_2;
        }
    }

    @Override
    public void convert(VH holder, Model data, int position) {
        int type = getItemViewType(position);
        switch(type){
            case TYPE_1:
                holder.setText(R.id.text, data.text);
                break;
            case TYPE_2:
                holder.setImage(R.id.image, data.image);
                break;
        }
    }
};
           

Item Decoration間隔樣式

RecyclerView通過

addItemDecoration()

方法添加item之間的分割線。Android并沒有提供實作好的Divider,是以任何分割線樣式都需要自己實作。

自定義間隔樣式需要繼承

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()

這兩個方法都是用于繪制間隔樣式,我們隻需要複寫其中一個方法即可。

Google在sample中給了一個參考的實作類:

DividerItemDecoration

,這裡我們通過分析這個例子來看如何自定義Item Decoration。

自定義的間隔樣式的實作步驟

  • ① 通過讀取系統主題中的 Android.R.attr.listDivider作為Item間的分割線,并且支援橫向和縱向。

    該分割線是系統預設的,你可以在theme.xml中找到該屬性(android:listDivider)的使用情況。

    如果要設定,則需要在value/styles.xml中設定:

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <item name="android:listDivider">@drawable/item_divider</item>
</style>
           
  • ② 擷取到listDivider以後,該屬性的值是個Drawable,在getItemOffsets中,outRect去設定了繪制的範圍。
  • ③ onDraw中實作了真正的繪制。
① 擷取listDivider

首先看構造函數,構造函數中獲得系統屬性

android:listDivider

,該屬性是一個Drawable對象。

private static final int[] ATTRS = new int[]{android.R.attr.listDivider};
private Drawable mDivider;
public DividerItemDecoration(Context context, int orientation) {
    final TypedArray a = context.obtainStyledAttributes(ATTRS);
    mDivider = a.getDrawable(0);
    a.recycle();
    setOrientation(orientation);
}
           
② getItemOffsets

接着來看

getItemOffsets()

的實作:

public void getItemOffsets(Rect outRect, int position, RecyclerView parent) {
    if (mOrientation == VERTICAL_LIST) {
        outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
    } else {
        outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
    }
}
           

這裡隻看

mOrientation == VERTICAL_LIST

的情況,outRect是目前item四周的間距,類似margin屬性,現在設定了該item下間距為

mDivider.getIntrinsicHeight()

那麼

getItemOffsets()

是怎麼被調用的呢?

RecyclerView繼承了ViewGroup,并重寫了

measureChild()

,該方法在

onMeasure()

中被調用,用來計算每個child的大小,計算每個child大小的時候就需要加上

getItemOffsets()

設定的外間距:

public void measureChild(View child, int widthUsed, int heightUsed){
    final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);//調用getItemOffsets()獲得Rect對象
    widthUsed += insets.left + insets.right;
    heightUsed += insets.top + insets.bottom;
    //...
}
           
③ onDraw

這裡我們隻考慮

mOrientation == VERTICAL_LIST

的情況,DividerItemDecoration的

onDraw()

實際上調用了

drawVertical()

public void drawVertical(Canvas c, RecyclerView parent) {
    final int left = parent.getPaddingLeft();
    final int right = parent.getWidth() - parent.getPaddingRight();
    final int childCount = parent.getChildCount();
    // 畫每個item的分割線
    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);
    }
}
           

onDraw()

是怎麼被調用的呢?還有ItemDecoration還有一個方法

onDrawOver()

,該方法也可以被重寫,那麼

onDraw()

onDrawOver()

之間有什麼關系呢?

我們來看下面的代碼:

class RecyclerView extends ViewGroup{
    public void draw(Canvas c) {
        super.draw(c); //調用View的draw(),該方法會先調用onDraw(),再調用dispatchDraw()繪制children

        final int count = mItemDecorations.size();
        for (int i = 0; i < count; i++) {
            mItemDecorations.get(i).onDrawOver(c, this, mState);
        }
        ...
    }
    public void onDraw(Canvas c) {
        super.onDraw(c);
        final int count = mItemDecorations.size();
        for (int i = 0; i < count; i++) {
            mItemDecorations.get(i).onDraw(c, this, mState);
        }
    }
}
           

根據

View的繪制流程

,首先調用RecyclerView重寫的

draw()

方法,随後

super.draw()

即調用View的

draw()

,該方法會先調用

onDraw()

(這個方法在RecyclerView重寫了),再調用

dispatchDraw()

繪制children。是以:ItemDecoration的

onDraw()

在繪制Item之前調用,ItemDecoration的

onDrawOver()

在繪制Item之後調用。

當然,如果隻需要實作Item之間相隔一定距離,那麼隻需要為Item的布局設定margin即可,沒必要自己實作ItemDecoration這麼麻煩。

Item Animator動畫

RecyclerView能夠通過

mRecyclerView.setItemAnimator(ItemAnimator animator)

設定添加、删除、移動、改變的動畫效果。

RecyclerView提供了預設的ItemAnimator實作類:DefaultItemAnimator。如果沒有特殊的需求,預設使用這個動畫即可。

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

下面就添加一下删除和添加Item的動作。在Adapter裡面添加方法。

public void addNewItem() {
    if(mData == null) {
        mData = new ArrayList<>();
    }
    mData.add(0, "new Item");
  ////更新資料集不是用adapter.notifyDataSetChanged()而是notifyItemInserted(position)與notifyItemRemoved(position) 否則沒有動畫效果。 
    notifyItemInserted(0);
}

public void deleteItem() {
    if(mData == null || mData.isEmpty()) {
        return;
    }
    mData.remove(0);
    notifyItemRemoved(0);
}
           

添加事件的處理。

public void onClick(View v) {
    int id = v.getId();
    if(id == R.id.rv_add_item_btn) {
        mAdapter.addNewItem();
        // 由于Adapter内部是直接在首個Item位置做增加操作,增加完畢後清單移動到首個Item位置
        mLayoutManager.scrollToPosition(0);
    } else if(id == R.id.rv_del_item_btn){
        mAdapter.deleteItem();
        // 由于Adapter内部是直接在首個Item位置做删除操作,删除完畢後清單移動到首個Item位置
        mLayoutManager.scrollToPosition(0);
    }
}

           

準備工作完畢後,來看一下運作的效果。

DefaultItemAnimator源碼分析

這裡我們通過分析DefaultItemAnimator的源碼來介紹如何自定義Item Animator。

DefaultItemAnimator繼承自SimpleItemAnimator,SimpleItemAnimator繼承自ItemAnimator。

首先我們介紹ItemAnimator類的幾個重要方法:

  • animateAppearance(): 當ViewHolder出現在螢幕上時被調用(可能是add或move)。
  • animateDisappearance(): 當ViewHolder消失在螢幕上時被調用(可能是remove或move)。
  • animatePersistence(): 在沒調用

    notifyItemChanged()

    notifyDataSetChanged()

    的情況下布局發生改變時被調用。
  • animateChange(): 在顯式調用

    notifyItemChanged()

    notifyDataSetChanged()

    時被調用。
  • runPendingAnimations(): RecyclerView動畫的執行方式并不是立即執行,而是每幀執行一次,比如兩幀之間添加了多個Item,則會将這些将要執行的動畫Pending住,儲存在成員變量中,等到下一幀一起執行。該方法執行的前提是前面

    animateXxx()

    傳回true。
  • isRunning(): 是否有動畫要執行或正在執行。
  • dispatchAnimationsFinished(): 當全部動畫執行完畢時被調用。

上面的方法比較難懂,不過沒關系,因為Android提供了SimpleItemAnimator類(繼承自ItemAnimator),該類提供了一系列更易懂的API,在自定義Item Animator時隻需要繼承SimpleItemAnimator即可:

  • animateAdd(ViewHolder holder): 當Item添加時被調用。
  • animateMove(ViewHolder holder, int fromX, int fromY, int toX, int toY): 當Item移動時被調用。
  • animateRemove(ViewHolder holder): 當Item删除時被調用。
  • animateChange(ViewHolder oldHolder, ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop): 當顯式調用

    notifyItemChanged()

    notifyDataSetChanged()

對于以上四個方法,注意兩點:

  • 當Xxx動畫開始執行前(在

    runPendingAnimations()

    中)需要調用

    dispatchXxxStarting(holder)

    ,執行完後需要調用

    dispatchXxxFinished(holder)

  • 這些方法的内部實際上并不是書寫執行動畫的代碼,而是将需要執行動畫的Item全部存入成員變量中,并且傳回值為true,然後在

    runPendingAnimations()

    中一并執行。

DefaultItemAnimator類是RecyclerView提供的預設動畫類。我們通過閱讀該類源碼學習如何自定義Item Animator。我們先看DefaultItemAnimator的成員變量:

private ArrayList<ViewHolder> mPendingAdditions = new ArrayList<>();//存放下一幀要執行的一系列add動畫
ArrayList<ArrayList<ViewHolder>> mAdditionsList = new ArrayList<>();//存放正在執行的一批add動畫
ArrayList<ViewHolder> mAddAnimations = new ArrayList<>(); //存放目前正在執行的add動畫

private ArrayList<ViewHolder> mPendingRemovals = new ArrayList<>();
ArrayList<ViewHolder> mRemoveAnimations = new ArrayList<>();

private ArrayList<MoveInfo> mPendingMoves = new ArrayList<>();
ArrayList<ArrayList<MoveInfo>> mMovesList = new ArrayList<>();
ArrayList<ViewHolder> mMoveAnimations = new ArrayList<>();

private ArrayList<ChangeInfo> mPendingChanges = new ArrayList<>();
ArrayList<ArrayList<ChangeInfo>> mChangesList = new ArrayList<>();
ArrayList<ViewHolder> mChangeAnimations = new ArrayList<>();
           

DefaultItemAnimator實作了SimpleItemAnimator的

animateAdd()

方法,該方法隻是将該item添加到mPendingAdditions中,等到

runPendingAnimations()

中執行。

public boolean animateAdd(final ViewHolder holder) {
    resetAnimation(holder);  //重置清空所有動畫
    ViewCompat.setAlpha(holder.itemView, 0); //将要做動畫的View先變成透明
    mPendingAdditions.add(holder);
    return true;
}
           

接着看

runPendingAnimations()

的實作,該方法是執行remove,move,change,add動畫,執行順序為:remove動畫最先執行,随後move和change并行執行,最後是add動畫。為了簡化,我們将remove,move,change動畫執行過程省略,隻看執行add動畫的過程,如下:

public void runPendingAnimations() {
    //1、判斷是否有動畫要執行,即各個動畫的成員變量裡是否有值。
    //2、執行remove動畫
    //3、執行move動畫
    //4、執行change動畫,與move動畫并行執行
    //5、執行add動畫
    if (additionsPending) {
        final ArrayList<ViewHolder> additions = new ArrayList<>();
        additions.addAll(mPendingAdditions);
        mAdditionsList.add(additions);
        mPendingAdditions.clear();
        Runnable adder = new Runnable() {
            @Override
            public void run() {
                for (ViewHolder holder : additions) {
                    animateAddImpl(holder);  //***** 執行動畫的方法 *****
                }
                additions.clear();
                mAdditionsList.remove(additions);
            }
        };
        if (removalsPending || movesPending || changesPending) {
            long removeDuration = removalsPending ? getRemoveDuration() : 0;
            long moveDuration = movesPending ? getMoveDuration() : 0;
            long changeDuration = changesPending ? getChangeDuration() : 0;
            long totalDelay = removeDuration + Math.max(moveDuration, changeDuration);
            View view = additions.get(0).itemView;
            ViewCompat.postOnAnimationDelayed(view, adder, totalDelay); //等remove,move,change動畫全部做完後,開始執行add動畫
        }
    }
}
           

為了防止在執行add動畫時外面有新的add動畫添加到mPendingAdditions中,進而導緻執行add動畫錯亂,這裡将mPendingAdditions的内容移動到局部變量additions中,然後周遊additions執行動畫。

runPendingAnimations()

中,

animateAddImpl()

是執行add動畫的具體方法,其實就是将itemView的透明度從0變到1(在

animateAdd()

中已經将view的透明度變為0),實作如下:

void animateAddImpl(final ViewHolder holder) {
    final View view = holder.itemView;
    final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
    mAddAnimations.add(holder);
    animation.alpha(1).setDuration(getAddDuration()).
            setListener(new VpaListenerAdapter() {
                @Override
                public void onAnimationStart(View view) {
                    dispatchAddStarting(holder);  //在開始add動畫前調用
                }
                @Override
                public void onAnimationCancel(View view) {
                    ViewCompat.setAlpha(view, 1);
                }

                @Override
                public void onAnimationEnd(View view) {
                    animation.setListener(null);
                    dispatchAddFinished(holder); //在結束add動畫後調用
                    mAddAnimations.remove(holder);
                    if (!isRunning()) {
                        dispatchAnimationsFinished(); //結束所有動畫後調用
                    }
                }
            }).start();
}
           

開源動畫recyclerview-animators

從DefaultItemAnimator類的實作來看,發現自定義Item Animator好麻煩,需要繼承SimpleItemAnimator類,然後實作一堆方法。

别急,

recyclerview-animators

解救你,原因如下:

  • 首先, 提供了一系列的Animator,比如FadeInAnimator,ScaleInAnimator。
  • 其次,如果該庫中沒有你滿意的動畫,該庫提供了BaseItemAnimator類,該類繼承自SimpleItemAnimator,進一步封裝了自定義Item Animator的代碼,使得自定義Item Animator更友善,你隻需要關注動畫本身。如果要實作DefaultItemAnimator的代碼,隻需要以下實作:
public class DefaultItemAnimator extends BaseItemAnimator {

  public DefaultItemAnimator() {
  }

  public DefaultItemAnimator(Interpolator interpolator) {
    mInterpolator = interpolator;
  }

  @Override protected void animateRemoveImpl(final RecyclerView.ViewHolder holder) {
    ViewCompat.animate(holder.itemView)
        .alpha(0)
        .setDuration(getRemoveDuration())
        .setListener(new DefaultRemoveVpaListener(holder))
        .setStartDelay(getRemoveDelay(holder))
        .start();
  }

  @Override protected void preAnimateAddImpl(RecyclerView.ViewHolder holder) {
    ViewCompat.setAlpha(holder.itemView, 0); //透明度先變為0
  }

  @Override protected void animateAddImpl(final RecyclerView.ViewHolder holder) {
    ViewCompat.animate(holder.itemView)
        .alpha(1)
        .setDuration(getAddDuration())
        .setListener(new DefaultAddVpaListener(holder))
        .setStartDelay(getAddDelay(holder))
        .start();
  }
}
           

是不是比繼承SimpleItemAnimator友善多了。

局部重新整理閃屏問題解決

對于RecyclerView的Item Animator,有一個常見的坑就是“閃屏問題”。

這個問題的描述是:當Item視圖中有圖檔和文字,當更新文字并調用

notifyItemChanged()

時,文字改變的同時圖檔會閃一下。這個問題的原因是當調用

notifyItemChanged()

時,會調用DefaultItemAnimator的

animateChangeImpl()

執行change動畫,該動畫會使得Item的透明度從0變為1,進而造成閃屏。

解決辦法很簡單,在

rv.setAdapter()

之前調用

((SimpleItemAnimator)rv.getItemAnimator()).setSupportsChangeAnimations(false)

禁用change動畫。

點選事件

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

實作方法有很多:

  • 可以監聽RecyclerView的Touch事件然後判斷手勢做相應的處理,
  • 也可以通過在綁定ViewHolder的時候設定監聽,然後通過Apater回調出去

我們選擇第二種方法,更加直覺和簡單。

看一下Adapter的完整代碼。

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder>{
    // 展示資料
    private ArrayList<String> mData;
    // 事件回調監聽
    private MyAdapter.OnItemClickListener onItemClickListener;
    public MyAdapter(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);
    }

    // ① 定義點選回調接口
    public interface OnItemClickListener {
        void onItemClick(View view, int position);
        void onItemLongClick(View view, int position);
    }
    
    // ② 定義一個設定點選監聽器的方法
    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));
        //③ 對RecyclerView的每一個itemView設定點選事件
        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);
        }
    }
}
           

設定Adapter的事件監聽。

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

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

最後的實作效果。

原文位址

https://www.cnblogs.com/geeksongs/p/10518528.html

繼續閱讀