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

来源:https://www.cnblogs.com/geeksongs/archive/2019/03/12/10518528.html
-Advertisement-
Play Games

概述 RecyclerView是什麼 從Android 5.0開始,谷歌公司推出了一個用於大量數據展示的新控制項RecylerView,可以用來代替傳統的ListView,更加強大和靈活。RecyclerView的官方定義如下: A flexible view for providing a limi ...


概述

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解救你,原因如下:

  • 首先,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);
        <

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • [20190312]視圖v$datafile欄位OFFLINE_CHANGE#, ONLINE_CHANGE#.txt--//視圖v$datafile存在2個欄位OFFLINE_CHANGE#, ONLINE_CHANGE#,想當然會認為數據文件offline時記錄scn號的改變.--//真的嗎?通 ...
  • create function GetPY(@str varchar(500))returns varchar(500)asbegin declare @cyc int,@length int,@str1 varchar(100),@charcate varbinary(20) set @cyc=1 ...
  • 最近測試了一下阿裡雲RDS for SQL Server,有些設計簡直就是反人類,讓人不得不吐槽一番。 1:控制台創建資料庫時,資料庫名不能包含大小字母。 如上截圖所示,資料庫名稱不能包含大寫字母,好吧,這個限制我認了。 但是使用“高許可權賬號”通過SSMS客戶端連接資料庫,可以創建包含大寫字母的數據... ...
  • MySQL預設只允許root帳戶在本地登錄,如果要在其它機器上連接mysql,必須修改root允許遠程連接。 其操作簡單,如下所示: 1. 進入mysql: 2. 使用mysql庫: 3. 查看用戶表: 4. 更新用戶表:(其中%的意思是允許所有的ip遠程訪問,如果需要指定具體的某個ip就寫上具體的 ...
  • 一、通過一個開始時間、結束時間計算出一個工作日天數(不包含工作日與節假日); 1、函數 2、存儲過程 二、通過一個開始時間、天數計算出一個結束時間(不包含工作日與節假日); 使用迴圈來實現; ...
  • 1、環境配置 環境配置過程中,需要關註軟體版本是否一致,主要包括:oracle客戶端版本、cx_oracle版本、python版本; 2、操作記錄 (1)驗證環境是否正常;(無報錯即為正常) import cx_Oracle (2)創建資料庫連接,方式大致三種; db1=cx_Oracle.conn ...
  • 一.利用SharedPrefences將數據儲存於data.txt當中 二.將數據從data.txt當中讀取併進行更新 ...
  • 1、XCode項目中創建一個.strings 擴展名的文件:打開File > New > File,選擇Resource中Strings Fils,如圖:點擊下一步,為文件命名為(強烈建議這樣命名) InfoPlist.strings ,然後點擊save。 2、創建完成後,你可以看到工程目錄結構文件... ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...