最新公告
  • 欢迎您光临AA分享网,一个高级程序员的学习、分享的分享平台!立即加入我们
  • 详解7.0带来的新工具类:DiffUtil

    原文出自张旭童的CSDN博客:http://blog.csdn.net/zxt0601/article/details/52562770 

    一 概述

    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突然的出现在某个位置: 

    20160917133116779.gif

    图二是使用DiffUtils的效果图,最明显的是有插入、移动Item的动画: 

    20160917133139138.gif 
    转成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 [email protected] 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
         * [email protected] 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 [email protected] #areItemsTheSame(int, int)} returns
         * [email protected] 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 [email protected] ViewHolder#itemView} to reflect the item at
             * the given position.
             * <p>
             * Note that unlike [email protected] 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 [email protected] ViewHolder#getAdapterPosition()} which will
             * have the updated adapter position.
             * <p>
             * Partial bind vs full bind:
             * <p>
             * The payloads parameter is a merge list from [email protected] #notifyItemChanged(int, Object)} or
             * [email protected] #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 [email protected] #areItemsTheSame(int, int)} returns [email protected] true} for two items and
         * [email protected] #areContentsTheSame(int, int)} returns false for them, DiffUtil
         * calls this method to get a payload about the change.
         * 
         * [email protected] #areItemsTheSame(int, int)} 返回true,[email protected] #areContentsTheSame(int, int)} 返回false时,DiffUtils会回调此方法,
         * 去得到这个Item(有哪些)改变的payload。
         * 
         * For example, if you are using DiffUtil with [email protected] RecyclerView}, you can return the
         * particular field that changed in the item and your
         * [email protected] android.support.v7.widget.RecyclerView.ItemAnimator ItemAnimator} can use that
         * information to run the correct animation.
         * 
         * 例如,如果你用RecyclerView配合DiffUtils,你可以返回  这个Item改变的那些字段,
         * [email protected] android.support.v7.widget.RecyclerView.ItemAnimator ItemAnimator} 可以用那些信息去执行正确的动画
         * 
         * Default implementation returns [email protected] 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

    AA分享网一个高级程序员的学习、分享的IT资源分享平台
    AA分享网-企业网站源码-PHP源码-网站模板-视频教程-IT技术教程 » 详解7.0带来的新工具类:DiffUtil
    • 257会员总数(位)
    • 5897资源总数(个)
    • 8本周发布(个)
    • 0 今日发布(个)
    • 536稳定运行(天)

    提供最优质的资源集合

    立即查看 了解详情