Android之RecyclerView的好伴侶:詳解DiffUtil

轉(zhuǎn)自:https://blog.csdn.net/zxt0601/article/details/52562770

一 概述

DiffUtil是support-v7:24.2.0中的新工具類趟径,它用來比較兩個數(shù)據(jù)集尊流,尋找出舊數(shù)據(jù)集->新數(shù)據(jù)集的最小變化量。
說到數(shù)據(jù)集二驰,相信大家知道它是和誰相關(guān)的了瑞妇,就是我的最愛:RecyclerView稿静。
就我使用的這幾天來看,它最大的用處就是在RecyclerView刷新時辕狰,不再無腦mAdapter.notifyDataSetChanged()改备。
以前無腦mAdapter.notifyDataSetChanged()有兩個缺點:

不會觸發(fā)RecyclerView的動畫(刪除、新增蔓倍、位移悬钳、change動畫)
性能較低,畢竟是無腦的刷新了一遍整個RecyclerView , 極端情況下:新老數(shù)據(jù)集一模一樣偶翅,效率是最低的默勾。

使用DiffUtil后,改為如下代碼:

DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallBack(mDatas, newDatas), true);
diffResult.dispatchUpdatesTo(mAdapter);

它會自動計算新老數(shù)據(jù)集的差異聚谁,并根據(jù)差異情況母剥,自動調(diào)用以下四個方法

1.adapter.notifyItemRangeInserted(position, count);
2.adapter.notifyItemRangeRemoved(position, count);
3.adapter.notifyItemMoved(fromPosition, toPosition);
4.adapter.notifyItemRangeChanged(position, count, payload);

顯然,這個四個方法在執(zhí)行時都是伴有RecyclerView的動畫的形导,且都是定向刷新方法环疼,刷新效率蹭蹭的上升了。
老規(guī)矩骤宣,先上圖秦爆,

圖一是無腦mAdapter.notifyDataSetChanged()的效果圖序愚,可以看到刷新交互很生硬憔披,Item突然的出現(xiàn)在某個位置:
1.gif

圖二是使用DiffUtils的效果圖,最明顯的是有插入爸吮、移動Item的動畫:
2.gif

本文將包含且不僅包含以下內(nèi)容:

1 先介紹DiffUtil的簡單用法芬膝,實現(xiàn)刷新時的“增量更新”效果。(“增量更新”是我自己的叫法)
2 DiffUtil的高級用法形娇,在某項Item只有內(nèi)容(data)變化锰霜,位置(position)未變化時,完成部分更新(官方稱之為Partial bind桐早,部分綁定)癣缅。
3 了解到 RecyclerView.Adapter還有public void onBindViewHolder(VH holder, int position, List<Object> payloads)方法厨剪,并掌握它。
4 在子線程中計算DiffResult友存,在主線程中刷新RecyclerView祷膳。
5 少部分人不喜歡的notifyItemChanged()導(dǎo)致Item白光一閃的動畫 如何去除。
6 DiffUtil部分類屡立、方法 官方注釋的漢化

二 DiffUtil的簡單用法

前文也提到直晨,DiffUtil是幫助我們在刷新RecyclerView時,計算新老數(shù)據(jù)集的差異膨俐,并自動調(diào)用RecyclerView.Adapter的刷新方法勇皇,以完成高效刷新并伴有Item動畫的效果。
那么我們在學(xué)習它之前要先做一些準備工作焚刺,先寫一個普通青年版敛摘,無腦notifyDataSetChanged()刷新的Demo。

1 一個普通的JavaBean乳愉,但是實現(xiàn)了clone方法着撩,僅用于寫Demo模擬刷新用,實際項目不需要匾委,因為刷新時拖叙,數(shù)據(jù)都是從網(wǎng)絡(luò)拉取的。:

class TestBean implements Cloneable {
    private String name;
    private String desc;
    ....//get set方法省略
    //僅寫DEMO 用 實現(xiàn)克隆方法
    @Override
    public TestBean clone() throws CloneNotSupportedException {
        TestBean bean = null;
        try {
            bean = (TestBean) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return bean;
    }
}

2 實現(xiàn)一個普普通通的RecyclerView.Adapter赂乐。

public class DiffAdapter extends RecyclerView.Adapter<DiffAdapter.DiffVH> {
    private final static String TAG = "zxt";
    private List<TestBean> mDatas;
    private Context mContext;
    private LayoutInflater mInflater;

    public DiffAdapter(Context mContext, List<TestBean> mDatas) {
        this.mContext = mContext;
        this.mDatas = mDatas;
        mInflater = LayoutInflater.from(mContext);
    }

    public void setDatas(List<TestBean> mDatas) {
        this.mDatas = mDatas;
    }

    @Override
    public DiffVH onCreateViewHolder(ViewGroup parent, int viewType) {
        return new DiffVH(mInflater.inflate(R.layout.item_diff, parent, false));
    }

    @Override
    public void onBindViewHolder(final DiffVH holder, final int position) {
        TestBean bean = mDatas.get(position);
        holder.tv1.setText(bean.getName());
        holder.tv2.setText(bean.getDesc());
        holder.iv.setImageResource(bean.getPic());
    }

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

    class DiffVH extends RecyclerView.ViewHolder {
        TextView tv1, tv2;
        ImageView iv;

        public DiffVH(View itemView) {
            super(itemView);
            tv1 = (TextView) itemView.findViewById(R.id.tv1);
            tv2 = (TextView) itemView.findViewById(R.id.tv2);
            iv = (ImageView) itemView.findViewById(R.id.iv);
        }
    }
}

3 Activity代碼:

public class MainActivity extends AppCompatActivity {
    private List<TestBean> mDatas;
    private RecyclerView mRv;
    private DiffAdapter mAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initData();
        mRv = (RecyclerView) findViewById(R.id.rv);
        mRv.setLayoutManager(new LinearLayoutManager(this));
        mAdapter = new DiffAdapter(this, mDatas);
        mRv.setAdapter(mAdapter);
    }

    private void initData() {
        mDatas = new ArrayList<>();
        mDatas.add(new TestBean("張旭童1", "Android", R.drawable.pic1));
        mDatas.add(new TestBean("張旭童2", "Java", R.drawable.pic2));
        mDatas.add(new TestBean("張旭童3", "背鍋", R.drawable.pic3));
        mDatas.add(new TestBean("張旭童4", "手撕產(chǎn)品", R.drawable.pic4));
        mDatas.add(new TestBean("張旭童5", "手撕測試", R.drawable.pic5));
    }

    /**
     * 模擬刷新操作
     *
     * @param view
     */
    public void onRefresh(View view) {
        try {
            List<TestBean> newDatas = new ArrayList<>();
            for (TestBean bean : mDatas) {
                newDatas.add(bean.clone());//clone一遍舊數(shù)據(jù) 薯鳍,模擬刷新操作
            }
            newDatas.add(new TestBean("趙子龍", "帥", R.drawable.pic6));//模擬新增數(shù)據(jù)
            newDatas.get(0).setDesc("Android+");
            newDatas.get(0).setPic(R.drawable.pic7);//模擬修改數(shù)據(jù)
            TestBean testBean = newDatas.get(1);//模擬數(shù)據(jù)位移
            newDatas.remove(testBean);
            newDatas.add(testBean);
            //別忘了將新數(shù)據(jù)給Adapter
            mDatas = newDatas;
            mAdapter.setDatas(mDatas);
            mAdapter.notifyDataSetChanged();//以前我們大多數(shù)情況下只能這樣
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }
}

很簡單,只不過在構(gòu)建新數(shù)據(jù)源newDatas時挨措,是遍歷老數(shù)據(jù)源mDatas挖滤,調(diào)用每個data的clone()方法,確保新老數(shù)據(jù)源雖然數(shù)據(jù)一致浅役,但是內(nèi)存地址(指針不一致)斩松,這樣在后面修改newDatas里的值時,不會牽連mDatas里的值被一起改了觉既。

4 activity_main.xml 刪掉了一些寬高代碼惧盹,就是一個RecyclerView和一個Button用于模擬刷新。:

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

    <android.support.v7.widget.RecyclerView
        android:id="@+id/rv" />

    <Button
        android:id="@+id/btnRefresh"
        android:layout_alignParentRight="true"
        android:onClick="onRefresh"
        android:text="模擬刷新" />
</RelativeLayout>

以上是一個普通青年很容易寫出的瞪讼,無腦notifyDataSetChanged()的demo钧椰,運行效果如第一節(jié)圖一。但是我們都要爭做文藝青年符欠,so下面開始進入正題嫡霞,簡單使用DiffUtil,我們需要且僅需要額外編寫一個類希柿。
想成為文藝青年诊沪,我們需要實現(xiàn)一個繼承自DiffUtil.Callback的類养筒,實現(xiàn)它的四個abstract方法。
雖然這個類叫Callback端姚,但是把它理解成:定義了一些用來比較新老Item是否相等的契約(Contract)闽颇、規(guī)則(Rule)的類, 更合適寄锐。

DiffUtil.Callback抽象類如下:

    public abstract static class Callback {
        public abstract int getOldListSize();//老數(shù)據(jù)集size

        public abstract int getNewListSize();//新數(shù)據(jù)集size

        public abstract boolean areItemsTheSame(int oldItemPosition, int newItemPosition);//新老數(shù)據(jù)集在同一個postion的Item是否是一個對象兵多?(可能內(nèi)容不同,如果這里返回true橄仆,會調(diào)用下面的方法)

        public abstract boolean areContentsTheSame(int oldItemPosition, int newItemPosition);//這個方法僅僅是上面方法返回ture才會調(diào)用剩膘,我的理解是只有notifyItemRangeChanged()才會調(diào)用,判斷item的內(nèi)容是否有變化

        //該方法在DiffUtil高級用法中用到 盆顾,暫且不提
        @Nullable
        public Object getChangePayload(int oldItemPosition, int newItemPosition) {
            return null;
        }
    }

本Demo如下實現(xiàn)DiffUtil.Callback怠褐,核心方法配有中英雙語注釋(說人話就是,翻譯了官方的英文注釋您宪,方便大家更好理解)奈懒。

/**
 * 介紹:核心類 用來判斷 新舊Item是否相等
 * 作者:zhangxutong
 * 郵箱:zhangxutong@imcoming.com
 * 時間: 2016/9/12.
 */

public class DiffCallBack extends DiffUtil.Callback {
    private List<TestBean> mOldDatas, mNewDatas;//看名字

    public DiffCallBack(List<TestBean> mOldDatas, List<TestBean> mNewDatas) {
        this.mOldDatas = mOldDatas;
        this.mNewDatas = mNewDatas;
    }

    //老數(shù)據(jù)集size
    @Override
    public int getOldListSize() {
        return mOldDatas != null ? mOldDatas.size() : 0;
    }

    //新數(shù)據(jù)集size
    @Override
    public int getNewListSize() {
        return mNewDatas != null ? mNewDatas.size() : 0;
    }

    /**
     * Called by the DiffUtil to decide whether two object represent the same Item.
     * 被DiffUtil調(diào)用,用來判斷 兩個對象是否是相同的Item宪巨。
     * For example, if your items have unique ids, this method should check their id equality.
     * 例如磷杏,如果你的Item有唯一的id字段,這個方法就 判斷id是否相等捏卓。
     * 本例判斷name字段是否一致
     *
     * @param oldItemPosition The position of the item in the old list
     * @param newItemPosition The position of the item in the new list
     * @return True if the two items represent the same object or false if they are different.
     */
    @Override
    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
        return mOldDatas.get(oldItemPosition).getName().equals(mNewDatas.get(newItemPosition).getName());
    }

    /**
     * Called by the DiffUtil when it wants to check whether two items have the same data.
     * 被DiffUtil調(diào)用极祸,用來檢查 兩個item是否含有相同的數(shù)據(jù)
     * DiffUtil uses this information to detect if the contents of an item has changed.
     * DiffUtil用返回的信息(true false)來檢測當前item的內(nèi)容是否發(fā)生了變化
     * DiffUtil uses this method to check equality instead of {@link Object#equals(Object)}
     * DiffUtil 用這個方法替代equals方法去檢查是否相等。
     * so that you can change its behavior depending on your UI.
     * 所以你可以根據(jù)你的UI去改變它的返回值
     * For example, if you are using DiffUtil with a
     * {@link android.support.v7.widget.RecyclerView.Adapter RecyclerView.Adapter}, you should
     * return whether the items' visual representations are the same.
     * 例如怠晴,如果你用RecyclerView.Adapter 配合DiffUtil使用遥金,你需要返回Item的視覺表現(xiàn)是否相同。
     * This method is called only if {@link #areItemsTheSame(int, int)} returns
     * {@code true} for these items.
     * 這個方法僅僅在areItemsTheSame()返回true時蒜田,才調(diào)用稿械。
     * @param oldItemPosition The position of the item in the old list
     * @param newItemPosition The position of the item in the new list which replaces the
     *                        oldItem
     * @return True if the contents of the items are the same or false if they are different.
     */
    @Override
    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
        TestBean beanOld = mOldDatas.get(oldItemPosition);
        TestBean beanNew = mNewDatas.get(newItemPosition);
        if (!beanOld.getDesc().equals(beanNew.getDesc())) {
            return false;//如果有內(nèi)容不同,就返回false
        }
        if (beanOld.getPic() != beanNew.getPic()) {
            return false;//如果有內(nèi)容不同冲粤,就返回false
        }
        return true; //默認兩個data內(nèi)容是相同的
    }

注釋張寫了這么詳細的注釋+簡單的代碼美莫,相信一眼可懂。
然后在使用時色解,注釋掉你以前寫的notifyDatasetChanged()方法吧茂嗓,替換成以下代碼:

//文藝青年新寵
//利用DiffUtil.calculateDiff()方法,傳入一個規(guī)則DiffUtil.Callback對象科阎,和是否檢測移動item的 boolean變量,得到DiffUtil.DiffResult 的對象

DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallBack(mDatas, newDatas), true);

//利用DiffUtil.DiffResult對象的dispatchUpdatesTo()方法忿族,傳入RecyclerView的Adapter锣笨,輕松成為文藝青年

diffResult.dispatchUpdatesTo(mAdapter);

//別忘了將新數(shù)據(jù)給Adapter
mDatas = newDatas;
mAdapter.setDatas(mDatas);

講解:

步驟一

在將newDatas 設(shè)置給Adapter之前蝌矛,先調(diào)用DiffUtil.calculateDiff()方法,計算出新老數(shù)據(jù)集轉(zhuǎn)化的最小更新集错英,就是DiffUtil.DiffResult對象入撒。
DiffUtil.calculateDiff()方法定義如下:
第一個參數(shù)是DiffUtil.Callback對象,
第二個參數(shù)代表是否檢測Item的移動椭岩,改為false算法效率更高茅逮,按需設(shè)置,我們這里是true判哥。

public static DiffResult calculateDiff(Callback cb, boolean detectMoves)

步驟二

然后利用DiffUtil.DiffResult對象的dispatchUpdatesTo()方法献雅,傳入RecyclerView的Adapter,替代普通青年才用的mAdapter.notifyDataSetChanged()方法塌计。
查看源碼可知挺身,該方法內(nèi)部,就是根據(jù)情況調(diào)用了adapter的四大定向刷新方法锌仅。

        public void dispatchUpdatesTo(final RecyclerView.Adapter adapter) {
            dispatchUpdatesTo(new ListUpdateCallback() {
                @Override
                public void onInserted(int position, int count) {
                    adapter.notifyItemRangeInserted(position, count);
                }

                @Override
                public void onRemoved(int position, int count) {
                    adapter.notifyItemRangeRemoved(position, count);
                }

                @Override
                public void onMoved(int fromPosition, int toPosition) {
                    adapter.notifyItemMoved(fromPosition, toPosition);
                }

                @Override
                public void onChanged(int position, int count, Object payload) {
                    adapter.notifyItemRangeChanged(position, count, payload);
                }
            });
        }

小結(jié):

所以說章钾,DiffUtil不僅僅只能和RecyclerView配合,我們也可以自己實現(xiàn)ListUpdateCallback接口的四個方法去做一些事情热芹。(我暫時不負責任隨便一項想贱傀,想到可以配合自己項目里的九宮格控件?或者優(yōu)化我上篇文章寫的NestFullListView伊脓?小安利窍箍,見 ListView、RecyclerView丽旅、ScrollView里嵌套ListView 相對優(yōu)雅的解決方案:http://blog.csdn.net/zxt0601/article/details/52494665

至此椰棘,我們已進化成文藝青年,運行效果和第一節(jié)圖二基本一致榄笙,唯一不同的是此時adapter.notifyItemRangeChanged()會有Item白光一閃的更新動畫 (本文Demo的postion為0的item)邪狞。 這個Item一閃的動畫有人喜歡有人恨,不過都不重要了茅撞,因為當我們學(xué)會了第三節(jié)的DiffUtil搞基用法帆卓,你愛不愛這個ItemChange動畫,它都將隨風而去米丘。(不知道是不是官方bug)效果就是第一節(jié)的圖二剑令,我們的item0其實圖片和文字都變化了,但是這個改變并沒有伴隨任何動畫拄查。讓我們邁向 文藝青年中的文藝青年 之路吁津。

三 DiffUtil的高級用法

理論:

高級用法只涉及到兩個方法,
我們需要分別實現(xiàn)DiffUtil.Callback的public Object getChangePayload(int oldItemPosition, int newItemPosition)方法堕扶,返回的Object就是表示Item改變了哪些內(nèi)容碍脏。再配合RecyclerView.Adapter的public void onBindViewHolder(VH holder, int position, List<Object> payloads)方法梭依,
完成定向刷新。(成為文青中的文青典尾,文青青役拴。)敲黑板,這是一個新方法钾埂,注意它有三個參數(shù)河闰,前兩個我們熟,第三個參數(shù)就包含了我們在getChangePayload()返回的Object褥紫。
好吧姜性,那我們就先看看這個方法是何方神圣:
在v7-24.2.0的源碼里,它長這個樣子:

        /**
         * Called by RecyclerView to display the data at the specified position. This method
         * should update the contents of the {@link ViewHolder#itemView} to reflect the item at
         * the given position.
         * <p>
         * Note that unlike {@link android.widget.ListView}, RecyclerView will not call this method
         * again if the position of the item changes in the data set unless the item itself is
         * invalidated or the new position cannot be determined. For this reason, you should only
         * use the <code>position</code> parameter while acquiring the related data item inside
         * this method and should not keep a copy of it. If you need the position of an item later
         * on (e.g. in a click listener), use {@link ViewHolder#getAdapterPosition()} which will
         * have the updated adapter position.
         * <p>
         * Partial bind vs full bind:
         * <p>
         * The payloads parameter is a merge list from {@link #notifyItemChanged(int, Object)} or
         * {@link #notifyItemRangeChanged(int, int, Object)}.  If the payloads list is not empty,
         * the ViewHolder is currently bound to old data and Adapter may run an efficient partial
         * update using the payload info.  If the payload is empty,  Adapter must run a full bind.
         * Adapter should not assume that the payload passed in notify methods will be received by
         * onBindViewHolder().  For example when the view is not attached to the screen, the
         * payload in notifyItemChange() will be simply dropped.
         *
         * @param holder The ViewHolder which should be updated to represent the contents of the
         *               item at the given position in the data set.
         * @param position The position of the item within the adapter's data set.
         * @param payloads A non-null list of merged payloads. Can be empty list if requires full
         *                 update.
         */
        public void onBindViewHolder(VH holder, int position, List<Object> payloads) {
            onBindViewHolder(holder, position);
        }

原來它內(nèi)部就僅僅調(diào)用了兩個參數(shù)的onBindViewHolder(holder, position) 故源,(題外話污抬,哎喲喂,我的NestFullListView 的Adapter也有幾分神似這種寫法绳军,看來我離Google大神又近了一步)看到這我才明白印机,其實onBind的入口,就是這個方法门驾,它才是和onCreateViewHolder對應(yīng)的方法射赛,源碼往下翻幾行可以看到有個public final void bindViewHolder(VH holder, int position),它內(nèi)部調(diào)用了三參的onBindViewHolder奶是。關(guān)于RecyclerView.Adapter 也不是三言兩句說的清楚的楣责。(其實我只掌握到這里),好了不再跑題,回到我們的三參數(shù)的onBindViewHolder(VH holder, int position, List<Object> payloads)聂沙,這個方法頭部有一大堆英文注釋秆麸,我一直覺得閱讀這些英文注釋對理解方法很有用處,于是我翻譯了一下及汉,

翻譯:

由RecyclerView調(diào)用 用來在在指定的位置顯示數(shù)據(jù)沮趣。
這個方法應(yīng)該更新ViewHolder里的ItemView的內(nèi)容,以反映在給定的位置 Item(的變化)坷随。請注意房铭,不像ListView,如果給定位置的item的數(shù)據(jù)集變化了温眉,RecyclerView不會再次調(diào)用這個方法缸匪,除非item本身失效(invalidated ) 或者新的位置不能確定。出于這個原因,在這個方法里类溢,你應(yīng)該只使用 postion參數(shù) 去獲取相關(guān)的數(shù)據(jù)item凌蔬,而且不應(yīng)該去保持 這個數(shù)據(jù)item的副本。如果你稍后需要這個item的position,例如設(shè)置clickListener龟梦。應(yīng)該使用 ViewHolder.getAdapterPosition()隐锭,它能提供 更新后的位置窃躲。(二筆的我看到這里發(fā)現(xiàn) 這是在講解兩參的onbindViewHolder方法
下面是這個三參方法的獨特部分:)
部分(partial)綁定vs完整(full)綁定
payloads 參數(shù) 是一個從(notifyItemChanged(int, Object)或notifyItemRangeChanged(int, int, Object))里得到的合并list计贰。
如果payloads list 不為空,那么當前綁定了舊數(shù)據(jù)的ViewHolder 和Adapter蒂窒, 可以使用 payload的數(shù)據(jù)進行一次 高效的部分更新躁倒。
如果payload 是空的,Adapter必須進行一次完整綁定(調(diào)用兩參方法)洒琢。
Adapter不應(yīng)該假定(想當然的認為) 在那些notifyxxxx通知方法傳遞過來的payload秧秉, 一定會在 onBindViewHolder()方法里收到。(這一句翻譯不好 QAQ 看舉例就好)
舉例來說衰抑,當View沒有attached 在屏幕上時象迎,這個來自notifyItemChange()的payload 就簡單的丟掉好了。
payloads對象不會為null呛踊,但是它可能是空(empty)砾淌,這時候需要完整綁定(所以我們在方法里只要判斷isEmpty就好,不用重復(fù)判空)谭网。
作者語:這方法是一個高效的方法汪厨。 我是個低效的翻譯者,我看了40+分鐘愉择。才終于明白劫乱,重要的部分已經(jīng)加粗顯示。
實戰(zhàn):

說了這么多話锥涕,其實用起來超級簡單:
先看如何使用getChangePayload()方法衷戈,又附帶了中英雙語注釋

   /**
     * When {@link #areItemsTheSame(int, int)} returns {@code true} for two items and
     * {@link #areContentsTheSame(int, int)} returns false for them, DiffUtil
     * calls this method to get a payload about the change.
     * 
     * 當{@link #areItemsTheSame(int, int)} 返回true,且{@link #areContentsTheSame(int, int)} 返回false時层坠,DiffUtils會回調(diào)此方法殖妇,
     * 去得到這個Item(有哪些)改變的payload。
     * 
     * For example, if you are using DiffUtil with {@link RecyclerView}, you can return the
     * particular field that changed in the item and your
     * {@link android.support.v7.widget.RecyclerView.ItemAnimator ItemAnimator} can use that
     * information to run the correct animation.
     * 
     * 例如窿春,如果你用RecyclerView配合DiffUtils拉一,你可以返回  這個Item改變的那些字段,
     * {@link android.support.v7.widget.RecyclerView.ItemAnimator ItemAnimator} 可以用那些信息去執(zhí)行正確的動畫
     * 
     * Default implementation returns {@code null}.\
     * 默認的實現(xiàn)是返回null
     *
     * @param oldItemPosition The position of the item in the old list
     * @param newItemPosition The position of the item in the new list
     * @return A payload object that represents the change between the two items.
     * 返回 一個 代表著新老item的改變內(nèi)容的 payload對象旧乞,
     */
    @Nullable
    @Override
    public Object getChangePayload(int oldItemPosition, int newItemPosition) {
        //實現(xiàn)這個方法 就能成為文藝青年中的文藝青年
        // 定向刷新中的部分更新
        // 效率最高
        //只是沒有了ItemChange的白光一閃動畫蔚润,(反正我也覺得不太重要)
        TestBean oldBean = mOldDatas.get(oldItemPosition);
        TestBean newBean = mNewDatas.get(newItemPosition);

        //這里就不用比較核心字段了,一定相等
        Bundle payload = new Bundle();
        if (!oldBean.getDesc().equals(newBean.getDesc())) {
            payload.putString("KEY_DESC", newBean.getDesc());
        }
        if (oldBean.getPic() != newBean.getPic()) {
            payload.putInt("KEY_PIC", newBean.getPic());
        }

        if (payload.size() == 0)//如果沒有變化 就傳空
            return null;
        return payload;//
    }

簡單的說,這個方法返回一個Object類型的payload尺栖,它包含了某個item的變化了的那些內(nèi)容嫡纠。
我們這里使用Bundle保存這些變化。

在Adapter里如下重寫三參的onBindViewHolder:

    @Override
    public void onBindViewHolder(DiffVH holder, int position, List<Object> payloads) {
        if (payloads.isEmpty()) {
            onBindViewHolder(holder, position);
        } else {
            //文藝青年中的文青
            Bundle payload = (Bundle) payloads.get(0);
            TestBean bean = mDatas.get(position);
            for (String key : payload.keySet()) {
                switch (key) {
                    case "KEY_DESC":
                        //這里可以用payload里的數(shù)據(jù),不過data也是新的 也可以用
                        holder.tv2.setText(bean.getDesc());
                        break;
                    case "KEY_PIC":
                        holder.iv.setImageResource(payload.getInt(key));
                        break;
                    default:
                        break;
                }
            }
        }
    }

這里傳遞過來的payloads是一個List除盏,由注釋可知叉橱,一定不為null,所以我們判斷是否是empty者蠕,
如果是empty窃祝,就調(diào)用兩參的函數(shù),進行一次Full Bind踱侣。
如果不是empty粪小,就進行partial bind,
通過下標0取出我們在getChangePayload方法里返回的payload抡句,然后遍歷payload的key探膊,根據(jù)key檢索,如果payload里攜帶有相應(yīng)的改變待榔,就取出來 然后更新在ItemView上逞壁。
(這里,通過mDatas獲得的也是最新數(shù)據(jù)源的數(shù)據(jù)锐锣,所以用payload的數(shù)據(jù)或者新數(shù)據(jù)的數(shù)據(jù) 進行更新都可以)
至此腌闯,我們已經(jīng)掌握了刷新RecyclerView,文藝青年中最文藝的那種寫法刺下。
四 在子線程中使用DiffUtil

在DiffUtil的源碼頭部注釋中介紹了DiffUtil的相關(guān)信息绑嘹,
DiffUtil內(nèi)部采用的Eugene W. Myers’s difference 算法,但該算法不能檢測移動的item橘茉,所以Google在其基礎(chǔ)上改進支持檢測移動項目工腋,但是檢測移動項目,會更耗性能畅卓。
在有1000項數(shù)據(jù)擅腰,200處改動時,這個算法的耗時:
打開了移動檢測時:平均值:27.07ms翁潘,中位數(shù):26.92ms趁冈。
關(guān)閉了移動檢測時:平均值:13.54ms,中位數(shù):13.36ms拜马。
有興趣可以自行去源碼頭部閱讀注釋渗勘,對我們比較有用的是其中一段提到,
如果我們的list過大俩莽,這個計算出DiffResult的時間還是蠻久的旺坠,所以我們應(yīng)該將獲取DiffResult的過程放到子線程中,并在主線程中更新RecyclerView扮超。
這里我采用Handler配合DiffUtil使用:
代碼如下:

    private static final int H_CODE_UPDATE = 1;
    private List<TestBean> mNewDatas;//增加一個變量暫存newList
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case H_CODE_UPDATE:
                    //取出Result
                    DiffUtil.DiffResult diffResult = (DiffUtil.DiffResult) msg.obj;
                    diffResult.dispatchUpdatesTo(mAdapter);
                    //別忘了將新數(shù)據(jù)給Adapter
                    mDatas = mNewDatas;
                    mAdapter.setDatas(mDatas);
                    break;
            }
        }
    };
            new Thread(new Runnable() {
                @Override
                public void run() {
                    //放在子線程中計算DiffResult
                    DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallBack(mDatas, mNewDatas), true);
                    Message message = mHandler.obtainMessage(H_CODE_UPDATE);
                    message.obj = diffResult;//obj存放DiffResult
                    message.sendToTarget();
                }
            }).start();

就是簡單的Handler使用取刃,不再贅述蹋肮。

五總結(jié)和其他

1 其實本文代碼量很少,可下載Demo查看璧疗,一共就四個類坯辩。但是不知不覺又被我寫的這么長,主要涉及到了一些源碼的注釋的翻譯崩侠,方便大家更好的理解漆魔。

2 DiffUtil很適合下拉刷新這種場景,更新的效率提高了啦膜,而且?guī)赢嬘兴停襼還不用你動腦子算了淌喻。不過若是就做個刪除 點贊這種僧家,完全不用DiffUtils。自己記好postion裸删,判斷一下postion在不在屏幕里八拱,調(diào)用那幾個定向刷新的方法即可。

3 其實DiffUtil不是只能和RecyclerView.Adapter配合使用涯塔,我們可以自己實現(xiàn) ListUpdateCallback接口肌稻,利用DIffUtil幫我們找到新舊數(shù)據(jù)集的最小差異集 來做更多的事情。

4 注意 寫DEMO的時候匕荸,用于比較的新老數(shù)據(jù)集爹谭,不僅ArrayList不同,里面每個data也要不同榛搔。 否則changed 無法觸發(fā)诺凡。實際項目中遇不到,因為新數(shù)據(jù)往往是網(wǎng)絡(luò)來的践惑。

5 關(guān)于“白光一閃”onChange動畫腹泌,
public Object getChangePayload() 這個方法返回不為null的話,onChange采用Partial bind尔觉,就不會出現(xiàn)凉袱。 反之就有。

github傳送門:好用給個star唄
https://github.com/mcxtzhang/DiffUtils

CSDN傳送門:
http://download.csdn.net/detail/zxt0601/9632159

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末侦铜,一起剝皮案震驚了整個濱河市专甩,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌钉稍,老刑警劉巖涤躲,帶你破解...
    沈念sama閱讀 216,651評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異嫁盲,居然都是意外死亡篓叶,警方通過查閱死者的電腦和手機烈掠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來缸托,“玉大人左敌,你說我怎么就攤上這事±洌” “怎么了矫限?”我有些...
    開封第一講書人閱讀 162,931評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長佩抹。 經(jīng)常有香客問我叼风,道長,這世上最難降的妖魔是什么棍苹? 我笑而不...
    開封第一講書人閱讀 58,218評論 1 292
  • 正文 為了忘掉前任无宿,我火速辦了婚禮,結(jié)果婚禮上枢里,老公的妹妹穿的比我還像新娘孽鸡。我一直安慰自己,他們只是感情好栏豺,可當我...
    茶點故事閱讀 67,234評論 6 388
  • 文/花漫 我一把揭開白布彬碱。 她就那樣靜靜地躺著,像睡著了一般奥洼。 火紅的嫁衣襯著肌膚如雪巷疼。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,198評論 1 299
  • 那天灵奖,我揣著相機與錄音嚼沿,去河邊找鬼。 笑死桑寨,一個胖子當著我的面吹牛伏尼,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播尉尾,決...
    沈念sama閱讀 40,084評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼爆阶,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了沙咏?” 一聲冷哼從身側(cè)響起辨图,我...
    開封第一講書人閱讀 38,926評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎肢藐,沒想到半個月后故河,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,341評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡吆豹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,563評論 2 333
  • 正文 我和宋清朗相戀三年鱼的,在試婚紗的時候發(fā)現(xiàn)自己被綠了理盆。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,731評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡凑阶,死狀恐怖猿规,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情宙橱,我是刑警寧澤姨俩,帶...
    沈念sama閱讀 35,430評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站师郑,受9級特大地震影響环葵,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜宝冕,卻給世界環(huán)境...
    茶點故事閱讀 41,036評論 3 326
  • 文/蒙蒙 一张遭、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧猬仁,春花似錦帝璧、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽褐耳。三九已至诈闺,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間铃芦,已是汗流浹背雅镊。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留刃滓,地道東北人仁烹。 一個月前我還...
    沈念sama閱讀 47,743評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像咧虎,于是被迫代替她去往敵國和親卓缰。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,629評論 2 354

推薦閱讀更多精彩內(nèi)容