- 浏览: 2215734 次
- 性别:
- 来自: 北京
-
文章分类
- 全部博客 (1240)
- mac/IOS (287)
- flutter (1)
- J2EE (115)
- android基础知识 (582)
- android中级知识 (55)
- android组件(Widget)开发 (18)
- android 错误 (21)
- javascript (18)
- linux (70)
- 树莓派 (18)
- gwt/gxt (1)
- 工具(IDE)/包(jar) (18)
- web前端 (17)
- java 算法 (8)
- 其它 (5)
- chrome (7)
- 数据库 (8)
- 经济/金融 (0)
- english (2)
- HTML5 (7)
- 网络安全 (14)
- 设计欣赏/设计窗 (8)
- 汇编/C (8)
- 工具类 (4)
- 游戏 (5)
- 开发频道 (5)
- Android OpenGL (1)
- 科学 (4)
- 运维 (0)
- 好东西 (6)
- 美食 (1)
最新评论
-
liangzai_cool:
请教一下,文中,shell、C、Python三种方式控制led ...
树莓派 - MAX7219 -
jiazimo:
...
Kafka源码分析-序列5 -Producer -RecordAccumulator队列分析 -
hp321:
Windows该命令是不是需要安装什么软件才可以?我试过不行( ...
ImageIO读jpg的时候出现javax.imageio.IIOException: Unsupported Image Type -
hp321:
Chenzh_758 写道其实直接用一下代码就可以解决了:JP ...
ImageIO读jpg的时候出现javax.imageio.IIOException: Unsupported Image Type -
huanghonhpeng:
大哥你真强什么都会,研究研究。。。。小弟在这里学到了很多知识。 ...
android 浏览器
一 概述
DiffUtil是support-v7:24.2.0中的新工具类,它用来比较两个数据集,寻找出旧数据集-》新数据集的最小变化量。
说到数据集,相信大家知道它是和谁相关的了,就是我的最爱,RecyclerView。
就我使用的这几天来看,它最大的用处就是在RecyclerView刷新时,不再无脑mAdapter.notifyDataSetChanged()。
以前无脑mAdapter.notifyDataSetChanged()有两个缺点:
使用DiffUtil后,改为如下代码:
它会自动计算新老数据集的差异,并根据差异情况,自动调用以下四个方法
显然,这个四个方法在执行时都是伴有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模拟刷新用,实际项目不需要,因为刷新时,数据都是从网络拉取的。:
2 实现一个普普通通的RecyclerView.Adapter。
3 Activity代码:
很简单,只不过在构建新数据源newDatas时,是遍历老数据源mDatas,调用每个data的clone()方法,确保新老数据源虽然数据一致,但是内存地址(指针不一致),这样在后面修改newDatas里的值时,不会牵连mDatas里的值被一起改了。
4 activity_main.xml 删掉了一些宽高代码,就是一个RecyclerView和一个Button用于模拟刷新。:
以上是一个普通青年很容易写出的,无脑notifyDataSetChanged()的demo,运行效果如第一节图一。
但是我们都要争做文艺青年,so
下面开始进入正题,简单使用DiffUtil,我们需要且仅需要额外编写一个类。
想成为文艺青年,我们需要实现一个继承自DiffUtil.Callback的类,实现它的四个abstract方法。
虽然这个类叫Callback,但是把它理解成:定义了一些用来比较新老Item是否相等的契约(Contract)、规则(Rule)的类, 更合适。
DiffUtil.Callback抽象类如下:
本Demo如下实现DiffUtil.Callback,核心方法配有中英双语注释(说人话就是,翻译了官方的英文注释,方便大家更好理解)。
注释张写了这么详细的注释+简单的代码,相信一眼可懂。
然后在使用时,注释掉你以前写的notifyDatasetChanged()方法吧,替换成以下代码:
讲解:
步骤一
在将newDatas 设置给Adapter之前,先调用DiffUtil.calculateDiff()方法,计算出新老数据集转化的最小更新集,就是DiffUtil.DiffResult对象。
DiffUtil.calculateDiff()方法定义如下:
第一个参数是DiffUtil.Callback对象,
第二个参数代表是否检测Item的移动,改为false算法效率更高,按需设置,我们这里是true。
步骤二
然后利用DiffUtil.DiffResult对象的dispatchUpdatesTo()方法,传入RecyclerView的Adapter,替代普通青年才用的mAdapter.notifyDataSetChanged()方法。
查看源码可知,该方法内部,就是根据情况调用了adapter的四大定向刷新方法。
小结:
所以说,DiffUtil不仅仅只能和RecyclerView配合,我们也可以自己实现ListUpdateCallback接口的四个方法去做一些事情。
至此,运行效果和第一节图二基本一致,
唯一不同的是此时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的源码里,它长这个样子:
原来它内部就仅仅调用了两个参数的onBindViewHolder(holder, position) ,看到这我才明白,其实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()方法,又附带了中英双语注释
简单的说,这个方法返回一个Object类型的payload,它包含了某个item的变化了的那些内容。
我们这里使用Bundle保存这些变化。
在Adapter里如下重写三参的onBindViewHolder:
这里传递过来的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使用:
代码如下:
就是简单的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
DiffUtil是support-v7:24.2.0中的新工具类,它用来比较两个数据集,寻找出旧数据集-》新数据集的最小变化量。
说到数据集,相信大家知道它是和谁相关的了,就是我的最爱,RecyclerView。
就我使用的这几天来看,它最大的用处就是在RecyclerView刷新时,不再无脑mAdapter.notifyDataSetChanged()。
以前无脑mAdapter.notifyDataSetChanged()有两个缺点:
- 不会触发RecyclerView的动画(删除、新增、位移、change动画)
- 性能较低,毕竟是无脑的刷新了一遍整个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是否相等 * 作者:iaiai * 邮箱:176291935@qq.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; } //老数据集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接口的四个方法去做一些事情。
至此,运行效果和第一节图二基本一致,
唯一不同的是此时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) ,看到这我才明白,其实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
发表评论
-
带你深入理解 FLUTTER 中的字体“冷”知识
2020-08-10 23:40 656本篇将带你深入理解 Flutter 开发过程中关于字体和文 ... -
Flutter -自定义日历组件
2020-03-01 17:56 1133颜色文件和屏幕适配的文件 可以自己给定 import ... -
Dart高级(一)——泛型与Json To Bean
2020-02-23 19:13 1024从 Flutter 发布到现在, 越来越多人开始尝试使用 Da ... -
flutter loading、Progress进度条
2020-02-21 17:03 1201Flutter Progress 1 条形无固定值进度条 ... -
Flutter使用Https加载图片
2020-02-21 01:39 1045Flutter使用Https加载图片 使用http加载图片出 ... -
flutter shared_preferences 异步变同步
2020-02-21 00:55 863前言 引用 在开发原生iOS或Native应用时,一般有判断上 ... -
Flutter TextField边框颜色
2020-02-19 21:31 956监听要销毁 myController.dispose(); T ... -
flutter Future的正确用法
2020-02-18 21:55 816在flutter中经常会用到异步任务,dart中异步任务异步处 ... -
记一次Flutter简单粗暴处理HTTPS证书检验方法
2020-02-18 14:13 1007最近在做Flutter项目到了遇到一个无解的事情,当使用Ima ... -
flutter 获取屏幕宽度高度 通知栏高度等屏幕信息
2019-07-27 08:39 1368##MediaQuery MediaQuery.of(con ... -
关于flutter RefreshIndicator扩展listview下拉刷新的问题
2019-07-10 19:40 1158当条目过少时listview某些嵌套情况下可能不会滚动(条目 ... -
flutter listview 改变状态的时候一直无限添加
2019-07-10 16:01 817setstate的时候会一直无限的调用listview.bui ... -
Flutter Android端启动白屏问题的解决
2019-07-09 00:51 1548问题描述 Flutter 应用在 Android 端上启动时 ... -
Flutter中SnackBar使用
2019-07-08 23:43 800底部弹出,然后在指定时间后消失。 注意: build(Bui ... -
Flutter 之点击空白区域收起键盘
2019-07-08 18:43 1806点击空白处取消TextField焦点这个需求是非常简单的,在学 ... -
Flutter 弹窗 Dialog ,AlertDialog,IOS风格
2019-07-08 18:04 1403import 'package:flutter/mate ... -
flutter ---TextField 之 输入类型、长度限制
2019-07-08 14:30 2362TextField想要实现输入类型、长度限制需要先引入impo ... -
【flutter 溢出BUG】键盘上显示bottom overflowed by 104 PIXELS
2019-07-08 11:13 1601一开始直接使用Scaffold布局,body:new Colu ... -
解决Flutter项目卡在Initializing gradle...界面的问题
2019-07-07 12:53 907Flutter最近很火,我抽出了一点时间对Flutter进行了 ... -
关于android O 上 NotificationChannel 的一些注意事项
2019-07-04 11:47 954最近在适配android O,遇到个问题,应用中原本有设置界面 ...
相关推荐
Android 7.0引入了一个强大的工具类——DiffUtil,它为开发者提供了一种优雅的方式来计算旧数据集和新数据集之间的差异,从而有效地更新列表。本文将深入探讨DiffUtil的工作原理、使用方法以及实际应用。 DiffUtil...
探索Apollo 7.0版本的CyberRT调度框架:思维导图及模块注释代码详解,深入解析Apollo 7.0 CyberRT调度框架:思维导图与模块注释代码的完整指南,Apollo7.0_CyberRT调度框架思维导图及该模块注释代码 , Apollo 7.0为...
FLAC3D 6.0&7.0自定义云图解决方案:径向应力、位移及切向应力、位移的代码详解与视频教程,《FLAC3D 6.0 & 7.0 自定义云图出图神器:解析代码、应用教程及视频讲解》,代码适用于FLAC3D6.0&7.0的自定义云图,包括径向...
"FLAC3D 6.0&7.0自定义云图解决方案:径向应力、位移及切向应力、位移的代码详解与视频教程",代码适用于FLAC3D6.0&7.0的自定义云图,包括径向应力、径向位移、切向应力、切向位移。 【代码具有解释,还有视频讲解...
DiffUtil示例
UDEC 7.0单轴压缩案例解析:全应力应变曲线及代码详解,UDEC 7.0单轴压缩案例解析:全应力应变曲线代码详解,UDEC 7.0单轴压缩案例代码,含全应力应变曲线 ,UDEC 7.0; 单轴压缩; 案例代码; 全应力应变曲线,UDEC 7.0...
Android安装apk文件并适配Android 7.0详解 Android安装apk文件并适配Android 7.0详解是指在Android系统中安装apk文件并使其适配Android 7.0系统的过程。这是一个重要的知识点,涉及到Android系统的安装机制和适配...
Android 7.0,昵称为"牛轧糖",是Google推出的一款重大更新,它带来了多项显著的新特性,旨在提高系统的性能、用户便利性和开发者工具的功能。以下是Android 7.0的一些关键特性详解: 1. **分屏多任务**: 分屏...
1. **Android 7.0公测版**:Android 7.0,又称为Nougat(牛轧糖),是Google发布的一个重要的Android操作系统版本。这个版本带来了多项改进,包括分屏多任务处理、更好的通知管理、更高效的内存使用等。一加3手机的...
《rtl8821cs在全志H6芯片上的Android 7.0系统实现与调试详解》 在当今的移动设备领域,无线连接技术扮演着至关重要的角色,其中Wi-Fi和蓝牙是不可或缺的部分。本文将深入探讨rtl8821cs无线网卡驱动在全志H6芯片上...
Apollo 7.0 CyberRT调度框架详解:思维导图与模块注释代码的全面解析,Apollo7.0_CyberRT调度框架思维导图及该模块注释代码 , Apollo 7.0为Apollo最新版本,自Apollo3.5版本开始摒弃原来的ROS调度方式改用自研的...
"Android 应用适配 Android 7.0 权限要求详解" Android 7.0 权限要求详解是指在 Android 7.0 及以上版本中,对应用权限的管理和获取的机制。该机制的主要目的,是为了保护用户的隐私和安全,防止恶意应用滥用用户...
Android 7.0 Settings顶部多了一个建议选项,多了个侧边栏,操作更加便捷了。 原生7.0主界面 原生7.0侧边栏 Android 6.0 之前做Android 6.0开发的,都会了解到6.0的Settings加载选项是通过加载dashboard_...
- **多窗口支持**:Android 7.0引入了多窗口模式,用户可以在屏幕上同时看到并操作两个应用,这对开发者的应用设计提出了新的挑战和机遇。 - **快速回复**:在通知栏可以直接回复消息,无需离开当前应用,这对消息...
西门子Wincc 7.0水处理项目工程实例:涵盖渗透、反渗透等多种工艺与S7程序配套,深入解析结构变量、脚本及报警记录与PLC的联接技术。...,西门子WinCC 7.0水处理工程项目实例:结构变量与报警记录详解
Android多媒体应用开发实战详解:图像、音频、视频、2D和3D
《Android底层开发技术实战详解:内核移植与驱动》是一本深入探讨Android系统底层开发的专著,旨在帮助读者理解并掌握Android系统的核心技术。本书着重讲解了Linux内核的基本原理,以及如何将Linux内核移植到Android...
1.Android系统源码定制和编译方法(Android7.0至Android12) 2.Android.mk和Android.bp语法大全及使用方法详解 3.Android.mk或Android.bp中引用Android的第三方(jar、aar、so)、Androidx的类库等的使用方法 4.Android....