本文主要探讨了BindingAdapter,力求深入透彻,让你读完之后不再有疑惑。
一、为什么需要BindingAdapter?
让我们接着上文的例子来讲。在上文的例子中,activity_main.xml中的TextView有如下binding表达式:
android:text = "@{user.name}"
我们来看看DataBinding是怎么处理上面这行代码的。
首先,DataBinding框架会对binding表达式进行求值,具体怎么求值,在我的文章DataBinding实现原理探析中有详细的论述,这里不再重复。
假设现在DataBinding框架已经对binding表达式求完值,值为CharSequence “milter”。
现在DataBinding框架面对的情况是这样的:要把值为的“milter”的字符串赋值给TextView的命名空间为android的text属性。
上面这句话读起来比较绕,我们把它做成表格来看:
类型 | 值 |
---|---|
CharSequence | “milter” |
TextView | username |
命名空间 | android |
属性 | text |
DataBinding如何使用些信息?我们先从最简单的情况说起,假如没有BindingAdapter机制,DataBinding框架会这样做:
- 忽略命名空间android,也就是说不管属性前面是什么命名空间,android也好,自定义(如app)的也罢,DataBinding框架都不care,它根本不需要这个信息。
- 根据属性值text和binding表达式的值CharSequence “milter”在TextView中寻找有如下签名的方法:
setText(CharSequence text)
- 在id为username的TextView上调用:
setText("milter")
- 结束!
上面的过程看起来非常理想对不对?毕竟我们自己也经常调用setText方法。但是由于databinding表达式的存在,事情开始变得复杂。
在我们的例子中,databinding表达式最后算出来的值是“milter”,可在实际情况中,这个表达式的值有很多变数,比如它可能是null,可能与TextView已有的text一样等,这些情况下,我们完全没有必要调用TextView的setText方法。
要知道,setText方法是一个复杂耗时间的操作,尤其是如果它的参数是一个Spanned类型,操作会更复杂。
怎么解决这个问题?最好的方法就是当DataBinding框架算出binding表达式的值之后,能够让我们介入,让我们根据求出的值的情形来决定是否调用TextView的setText。
DataBinding框架确实给我们提供了这样的介入机制,这就是BindingAdapter。
二、使用BindingAdapter
继续我们上面的例子,为了实现我们上面的介入目的,我们按照DataBinding框架的要求,定义出下面的BindingAdapter:
@BindingAdapter("android:text")
public static void setText(TextView view, CharSequence text) {
final CharSequence oldText = view.getText();
if (text == oldText || (text == null && oldText.length() == 0)) {
return;
}
if (text instanceof Spanned) {
if (text.equals(oldText)) {
return; // No change in the spans, so don't set anything.
}
} else if (!haveContentsChanged(text, oldText)) {
return; // No content changes, so don't set anything.
}
view.setText(text);
}
要理解这个BindingAdapter,有三大关键点:
- 注解@BindingAdapter的参数“android:text”
- 方法第一个参数TextView
- 方法第二个参数CharSequence
DataBinding框架会汇总以上三个信息,进而得出结论:
当在TextView上设置text属性,且设置的值的类型是CharSequence时,就不要直接调用TextView相应的setText方法,而是调用用户定义的这个BindingAdapter方法。
Note:DataBinding框架根本不关心text属性前面的命名空间是什么,也不关心这个BindingAdapter的方法名字是什么,我们把它定义成setText,纯属巧合 :)。
在这个BindingAdapter中,我们先是判断是否有必要调用TextView的setText方法,确认有必要后,我们才调用TextView的setText方法,对于没有必要的情况,我们选择直接返回。关于这个BindingAdapter放在哪里,答案是:看你心情,随意放!
好了,现在我们来总结一下:
当在任意一个View的任意一个属性上使用binding表达式时,DataBinding框架的处理过程分成三步:
1、对binding表达式求值
2、寻找合适的BindingAdapter,如果找到,就调用它的方法
3、如果没有找到合适的BindingAdapter,就在View上寻找合适的方法调用
现在问题来了,UI控件那么多,它们的属性就更多了,难道对每个需要使用binding表达式的属性,我们都要像上面那样写一个BindingAdapter?有木有想死的冲动?
不用担心,DataBinding框架已经帮我们写好了许许多多的BindingAdapter,覆盖了Android提供的所有控件的绝大多数属性!
告诉你一个秘密,其实上面的BindingAdapter代码,根本不是我写的,而是DataBinding框架已经提供好的。
2016.12.18补充
有的朋友问,DataBinding框架写好的setText方法在哪里,我怎么找不着呢?下面用一张图告诉你!
该图是在Project视图模式下截取的。图左侧那一群BindingAdapter中就有本文中使用的TextViewBindingAdapter,其内容显示在图中右侧,可以看到setText方法了吧 :)
三、自定义BindingAdapter
假如我们觉得上面那个系统提供的BindingAdapter不能满足我们的需求,我们想要这个BindingAdapter能够将CharSequence全部变成大写,然后再调用TextView的setText方法,这时,我们就需要自定义BindingAdapter。
DataBinding框架很开明,它承诺:在寻找合适的BindingAdapter时,会优先使用用户定义的BindingAdapter。
现在我们自定义一个我们的BindingAdapter:
@BindingAdapter("android:text")
public static void setText(TextView view, CharSequence text) {
final CharSequence oldText = view.getText();
if (text == oldText || (text == null && oldText.length() == 0)) {
return;
}
if (text instanceof Spanned) {
if (text.equals(oldText)) {
return; // No change in the spans, so don't set anything.
}
} else if (!haveContentsChanged(text, oldText)) {
return; // No content changes, so don't set anything.
}
//下面这句代码,就是我们加进去的
CharSequence upperText = text.toUpperCase();
view.setText(upperText);
}
这个BindingAdapter其实就比系统提供的BindingAdapter多一句将字符串变成大写的代码,其他完全一样。
有了这个BindingAdapter,任何时候,任何情况下,只要我们在TextView的text属性上使用binding表达式,并且这个表达式的值是CharSequence,那么,我们自定义的BindingAdapter就会被DataBinding框架调用,它会把binding表达式的值变成大写后设置给TextView。如下图所示:
总结:以上,我们深入探讨了DataBinding框架的BindingAdapter机制,从为什么需要它,怎么使用它,怎么自定义它三个方面进行了分析。重点在于理解它的原理。关于BindingAdapter各种好玩的使用方法,请参考官方文档:https://developer.android.com/topic/libraries/data-binding/index.html(要科学上网哈!)