6

详解7.0带来的新工具类:DiffUtil

 3 years ago
source link: http://www.androidchina.net/5791.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
详解7.0带来的新工具类:DiffUtil – Android开发中文站
你的位置:Android开发中文站 > Android开发 > 新手入门 > 详解7.0带来的新工具类:DiffUtil

DiffUtil是support-v7:24.2.0中的新工具类,它用来比较两个数据集,寻找出旧数据集-》新数据集的最小变化量。

说到数据集,相信大家知道它是和谁相关的了,就是我的最爱,RecyclerView。

就我使用的这几天来看,它最大的用处就是在RecyclerView刷新时,不再无脑mAdapter.notifyDataSetChanged()
以前无脑mAdapter.notifyDataSetChanged()有两个缺点:

  1. 不会触发RecyclerView的动画(删除、新增、位移、change动画)
  2. 性能较低,毕竟是无脑的刷新了一遍整个RecyclerView , 极端情况下:新老数据集一模一样,效率是最低的。

使用DiffUtil后,改为如下代码:

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

它会自动计算新老数据集的差异,并根据差异情况,自动调用以下四个方法

adapter.notifyItemRangeInserted(position, count);
adapter.notifyItemRangeRemoved(position, count);
adapter.notifyItemMoved(fromPosition, toPosition);
adapter.notifyItemRangeChanged(position, count, payload);

显然,这个四个方法在执行时都是伴有RecyclerView的动画的,且都是定向刷新方法,刷新效率蹭蹭的上升了。
老规矩,先上图,

图一是无脑mAdapter.notifyDataSetChanged()的效果图,可以看到刷新交互很生硬,Item突然的出现在某个位置:
这里写图片描述

图二是使用DiffUtils的效果图,最明显的是有插入、移动Item的动画:
这里写图片描述
转成GIF有些渣,下载文末Demo运行效果更佳哦。

本文将包含且不仅包含以下内容:

1 先介绍DiffUtil的简单用法,实现刷新时的“增量更新”效果。(“增量更新”是我自己的叫法)

2 DiffUtil的高级用法,在某项Item只有内容(data)变化,位置(position)未变化时,完成部分更新(官方称之为Partial bind,部分绑定)。

3 了解到 RecyclerView.Adapter还有public void onBindViewHolder(VH holder, int position, List<Object> payloads)方法,并掌握它。

4 在子线程中计算DiffResult,在主线程中刷新RecyclerView。

5 少部分人不喜欢的notifyItemChanged()导致Item白光一闪的动画 如何去除。

6 DiffUtil部分类、方法 官方注释的汉化

二 DiffUtil的简单用法

前文也提到,DiffUtil是帮助我们在刷新RecyclerView时,计算新老数据集的差异,并自动调用RecyclerView.Adapter的刷新方法,以完成高效刷新并伴有Item动画的效果。

那么我们在学习它之前要先做一些准备工作,先写一个普通青年版,无脑notifyDataSetChanged()刷新的Demo。

1 一个普通的JavaBean,但是实现了clone方法,仅用于写Demo模拟刷新用,实际项目不需要,因为刷新时,数据都是从网络拉取的。:

class TestBean implements Cloneable {
    private String name;
    private String desc;
    ....//get set方法省略
    //仅写DEMO 用 实现克隆方法
    @Override
    public TestBean clone() throws CloneNotSupportedException {
        TestBean bean = null;
        try {
            bean = (TestBean) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return bean;
    }

2 实现一个普普通通的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", "手撕产品", 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一遍旧数据 ,模拟刷新操作
            }
            newDatas.add(new TestBean("赵子龙", "帅", R.drawable.pic6));//模拟新增数据
            newDatas.get(0).setDesc("Android+");
            newDatas.get(0).setPic(R.drawable.pic7);//模拟修改数据
            TestBean testBean = newDatas.get(1);//模拟数据位移
            newDatas.remove(testBean);
            newDatas.add(testBean);
            //别忘了将新数据给Adapter
            mDatas = newDatas;
            mAdapter.setDatas(mDatas);
            mAdapter.notifyDataSetChanged();//以前我们大多数情况下只能这样
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }

}

很简单,只不过在构建新数据源newDatas时,是遍历老数据源mDatas,调用每个data的clone()方法,确保新老数据源虽然数据一致,但是内存地址(指针不一致),这样在后面修改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,运行效果如第一节图一。
但是我们都要争做文艺青年,so

下面开始进入正题,简单使用DiffUtil,我们需要且仅需要额外编写一个类。

想成为文艺青年,我们需要实现一个继承自DiffUtil.Callback的类,实现它的四个abstract方法。

虽然这个类叫Callback,但是把它理解成:定义了一些用来比较新老Item是否相等的契约(Contract)、规则(Rule)的类, 更合适。

DiffUtil.Callback抽象类如下:

    public abstract static class Callback {
        public abstract int getOldListSize();//老数据集size

        public abstract int getNewListSize();//新数据集size

        public abstract boolean areItemsTheSame(int oldItemPosition, int newItemPosition);//新老数据集在同一个postion的Item是否是一个对象?(可能内容不同,如果这里返回true,会调用下面的方法)

        public abstract boolean areContentsTheSame(int oldItemPosition, int newItemPosition);//这个方法仅仅是上面方法返回ture才会调用,我的理解是只有notifyItemRangeChanged()才会调用,判断item的内容是否有变化

        //该方法在DiffUtil高级用法中用到 ,暂且不提
        @Nullable
        public Object getChangePayload(int oldItemPosition, int newItemPosition) {
            return null;
        }
    }

本Demo如下实现DiffUtil.Callback,核心方法配有中英双语注释(说人话就是,翻译了官方的英文注释,方便大家更好理解)。

/**
 * 介绍:核心类 用来判断 新旧Item是否相等
 * 作者:zhangxutong
 * 邮箱:[email protected]
 * 时间: 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;
    }

    //老数据集size
    @Override
    public int getOldListSize() {
        return mOldDatas != null ? mOldDatas.size() : 0;
    }

    //新数据集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调用,用来判断 两个对象是否是相同的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调用,用来检查 两个item是否含有相同的数据
     * DiffUtil uses this information to detect if the contents of an item has changed.
     * DiffUtil用返回的信息(true false)来检测当前item的内容是否发生了变化
     * 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.
     * 所以你可以根据你的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的视觉表现是否相同。
     * This method is called only if {@link #areItemsTheSame(int, int)} returns
     * {@code true} for these items.
     * 这个方法仅仅在areItemsTheSame()返回true时,才调用。
     * @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;//如果有内容不同,就返回false
        }
        if (beanOld.getPic() != beanNew.getPic()) {
            return false;//如果有内容不同,就返回false
        }
        return true; //默认两个data内容是相同的
    }

注释张写了这么详细的注释+简单的代码,相信一眼可懂。

然后在使用时,注释掉你以前写的notifyDatasetChanged()方法吧,替换成以下代码:

//文艺青年新宠
//利用DiffUtil.calculateDiff()方法,传入一个规则DiffUtil.Callback对象,和是否检测移动item的 boolean变量,得到DiffUtil.DiffResult 的对象

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

//利用DiffUtil.DiffResult对象的dispatchUpdatesTo()方法,传入RecyclerView的Adapter,轻松成为文艺青年

diffResult.dispatchUpdatesTo(mAdapter);

//别忘了将新数据给Adapter
mDatas = newDatas;
mAdapter.setDatas(mDatas);

在将newDatas 设置给Adapter之前,先调用DiffUtil.calculateDiff()方法,计算出新老数据集转化的最小更新集,就是DiffUtil.DiffResult对象。

DiffUtil.calculateDiff()方法定义如下:

第一个参数是DiffUtil.Callback对象,

第二个参数代表是否检测Item的移动,改为false算法效率更高,按需设置,我们这里是true。

public static DiffResult calculateDiff(Callback cb, boolean detectMoves)

然后利用DiffUtil.DiffResult对象的dispatchUpdatesTo()方法,传入RecyclerView的Adapter,替代普通青年才用的mAdapter.notifyDataSetChanged()方法。

查看源码可知,该方法内部,就是根据情况调用了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);
                }
            });
        }

所以说,DiffUtil不仅仅只能和RecyclerView配合,我们也可以自己实现ListUpdateCallback接口的四个方法去做一些事情。(我暂时不负责任随便一项想,想到可以配合自己项目里的九宫格控件?或者优化我上篇文章写的NestFullListView?小安利,见 ListView、RecyclerView、ScrollView里嵌套ListView 相对优雅的解决方案:http://blog.csdn.net/zxt0601/article/details/52494665

至此,我们已进化成文艺青年,运行效果和第一节图二基本一致,

唯一不同的是此时adapter.notifyItemRangeChanged()会有Item白光一闪的更新动画 (本文Demo的postion为0的item)。 这个Item一闪的动画有人喜欢有人恨,不过都不重要了,

因为当我们学会了第三节的DiffUtil搞基用法,你爱不爱这个ItemChange动画,它都将随风而去。(不知道是不是官方bug)
效果就是第一节的图二,我们的item0其实图片和文字都变化了,但是这个改变并没有伴随任何动画。

让我们迈向 文艺青年中的文艺青年 之路。

三 DiffUtil的高级用法

高级用法只涉及到两个方法,

我们需要分别实现DiffUtil.Callback
public Object getChangePayload(int oldItemPosition, int newItemPosition)方法,
返回的Object就是表示Item改变了哪些内容。

再配合RecyclerView.Adapter
public void onBindViewHolder(VH holder, int position, List<Object> payloads)方法,
完成定向刷新。(成为文青中的文青,文青青。)

敲黑板,这是一个新方法,注意它有三个参数,前两个我们熟,第三个参数就包含了我们在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);
        }

原来它内部就仅仅调用了两个参数的onBindViewHolder(holder, position) ,(题外话,哎哟喂,我的NestFullListView 的Adapter也有几分神似这种写法,看来我离Google大神又近了一步)

看到这我才明白,其实onBind的入口,就是这个方法,它才是和onCreateViewHolder对应的方法,

源码往下翻几行可以看到有个public final void bindViewHolder(VH holder, int position),它内部调用了三参的onBindViewHolder

关于RecyclerView.Adapter 也不是三言两句说的清楚的。(其实我只掌握到这里)

好了不再跑题,回到我们的三参数的onBindViewHolder(VH holder, int position, List<Object> payloads),这个方法头部有一大堆英文注释,我一直觉得阅读这些英文注释对理解方法很有用处,于是我翻译了一下,

由RecyclerView调用 用来在在指定的位置显示数据。

这个方法应该更新ViewHolder里的ItemView的内容,以反映在给定的位置 Item(的变化)。

请注意,不像ListView,如果给定位置的item的数据集变化了,RecyclerView不会再次调用这个方法,除非item本身失效了(invalidated ) 或者新的位置不能确定。

出于这个原因,在这个方法里,你应该只使用 postion参数 去获取相关的数据item,而且不应该去保持 这个数据item的副本。

如果你稍后需要这个item的position,例如设置clickListener。应该使用 ViewHolder.getAdapterPosition(),它能提供 更新后的位置。

(二笔的我看到这里发现 这是在讲解两参的onbindViewHolder方法

下面是这个三参方法的独特部分:)

**部分(partial)绑定**vs完整(full)绑定

payloads 参数 是一个从(notifyItemChanged(int, Object)或notifyItemRangeChanged(int, int, Object))里得到的合并list。
如果payloads list 不为空,那么当前绑定了旧数据的ViewHolder 和Adapter, 可以使用 payload的数据进行一次 高效的部分更新

如果payload 是空的,Adapter必须进行一次完整绑定(调用两参方法)

Adapter不应该假定(想当然的认为) 在那些notifyxxxx通知方法传递过来的payload, 一定会在 onBindViewHolder()方法里收到。(这一句翻译不好 QAQ 看举例就好)

举例来说,当View没有attached 在屏幕上时,这个来自notifyItemChange()的payload 就简单的丢掉好了。

payloads对象不会为null,但是它可能是空(empty),这时候需要完整绑定(所以我们在方法里只要判断isEmpty就好,不用重复判空)

作者语:这方法是一个高效的方法。 我是个低效的翻译者,我看了40+分钟。才终于明白,重要的部分已经加粗显示。

说了这么多话,其实用起来超级简单:

先看如何使用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会回调此方法,
     * 去得到这个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} 可以用那些信息去执行正确的动画
     * 
     * Default implementation returns {@code null}.\
     * 默认的实现是返回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的改变内容的 payload对象,
     */
    @Nullable
    @Override
    public Object getChangePayload(int oldItemPosition, int newItemPosition) {
        //实现这个方法 就能成为文艺青年中的文艺青年
        // 定向刷新中的部分更新
        // 效率最高
        //只是没有了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的变化了的那些内容。

我们这里使用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里的数据,不过data也是新的 也可以用
                        holder.tv2.setText(bean.getDesc());
                        break;
                    case "KEY_PIC":
                        holder.iv.setImageResource(payload.getInt(key));
                        break;
                    default:
                        break;
                }
            }
        }
    }

这里传递过来的payloads是一个List,由注释可知,一定不为null,所以我们判断是否是empty,

如果是empty,就调用两参的函数,进行一次Full Bind。

如果不是empty,就进行partial bind,

通过下标0取出我们在getChangePayload方法里返回的payload,然后遍历payload的key,根据key检索,如果payload里携带有相应的改变,就取出来 然后更新在ItemView上。

(这里,通过mDatas获得的也是最新数据源的数据,所以用payload的数据或者新数据的数据 进行更新都可以)

至此,我们已经掌握了刷新RecyclerView,文艺青年中最文艺的那种写法。

四 在子线程中使用DiffUtil

在DiffUtil的源码头部注释中介绍了DiffUtil的相关信息,

DiffUtil内部采用的Eugene W. Myers’s difference 算法,但该算法不能检测移动的item,所以Google在其基础上改进支持检测移动项目,但是检测移动项目,会更耗性能。

在有1000项数据,200处改动时,这个算法的耗时:

打开了移动检测时:平均值:27.07ms,中位数:26.92ms。

关闭了移动检测时:平均值:13.54ms,中位数:13.36ms。

有兴趣可以自行去源码头部阅读注释,对我们比较有用的是其中一段提到,

如果我们的list过大,这个计算出DiffResult的时间还是蛮久的,所以我们应该将获取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);
                    //别忘了将新数据给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使用,不再赘述。

五总结和其他

1 其实本文代码量很少,可下载Demo查看,一共就四个类。

但是不知不觉又被我写的这么长,主要涉及到了一些源码的注释的翻译,方便大家更好的理解。

2 DiffUtil很适合下拉刷新这种场景,更新的效率提高了,而且带动画,而且~还不用你动脑子算了。

不过若是就做个删除 点赞这种,完全不用DiffUtils。自己记好postion,判断一下postion在不在屏幕里,调用那几个定向刷新的方法即可。

3 其实DiffUtil不是只能和RecyclerView.Adapter配合使用,我们可以自己实现 ListUpdateCallback接口,利用DIffUtil帮我们找到新旧数据集的最小差异集 来做更多的事情。

4 注意 写DEMO的时候,用于比较的新老数据集,不仅ArrayList不同,里面每个data也要不同。 否则changed 无法触发。
实际项目中遇不到,因为新数据往往是网络来的。

5 今天是中秋节的最后一天,我们公司居然就开始上班了!!!气愤之余,我怒码一篇DiffUtil,我都不需要用DiffUtil,也能轻易比较出我们公司和其他公司的差异。QAQ,而且今天状态不佳,居然写了8个小时才完工。本以为这篇文章是可以入选微作文集的,没想到也是蛮长的。没有耐心的其实可以下载DEMO看看,代码量没多少,使用起来还是很轻松的。

6 关于“白光一闪”onChange动画,
public Object getChangePayload() 这个方法返回不为null的话,onChange采用Partial bind,就不会出现。 反之就有。

github传送门:好用给个star呗

https://github.com/mcxtzhang/DiffUtils

CSDN传送门:

http://download.csdn.net/detail/zxt0601/9632159

转载请注明:Android开发中文站 » 详解7.0带来的新工具类:DiffUtil


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK