这篇之前在简书上面发布的blog,由于不打算在简书上面写东西了,就搬过来这里。
之前曾经发布过一篇blog来记录之前使用DataBinding的一些心得体会,当时对于DataBinding的双向绑定简单提了一下。现在对这个双向绑定有了进一步的了解,于是继续分享。
回顾
我们先来回顾一下之前双向绑定的做法:
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:text="@={item.price}"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{item.price}"/>
这里就只是通过使用* “@={}”*就完成了EditText
和Button
的文本属性的双向绑定。这里面隐藏了什么秘密呢?直接打开源码来看下吧;
隐藏起来的秘密
EditText
和Button
继承TextView
,我们可以在TextViewBindingAdapter
里面看到Goolge工程师对android:text
这个属性做了什么处理。打开源码,首先就被下面这段代码吸引了目光(反正我就被吸引了):
@InverseBindingAdapter(attribute = "android:text", event = "android:textAttrChanged")
public static String getTextString(TextView view) {
return view.getText().toString();
}
虽然很疑惑这个@InverseBindingAdapter
注解的作用,不过还是先把相关源码的逻辑找出来吧:
@BindingAdapter(value = {"android:beforeTextChanged", "android:onTextChanged",
"android:afterTextChanged", "android:textAttrChanged"}, requireAll = false)
public static void setTextWatcher(TextView view, final BeforeTextChanged before,
final OnTextChanged on, final AfterTextChanged after,
final InverseBindingListener textAttrChanged) {
final TextWatcher newValue;
if (before == null && after == null && on == null && textAttrChanged == null) {
newValue = null;
} else {
newValue = new TextWatcher() {
//删去部分代码,这里不是重点
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (on != null) {
on.onTextChanged(s, start, before, count);
}
if (textAttrChanged != null) {
textAttrChanged.onChange();
}
}
//删去部分代码,这里不是重点
};
}
final TextWatcher oldValue = ListenerUtil.trackListener(view, newValue, R.id.textWatcher);
if (oldValue != null) {
view.removeTextChangedListener(oldValue);
}
if (newValue != null) {
view.addTextChangedListener(newValue);
}
}
EditText
的输出监听回调方法就是上面的onTextChanged
,不过在这里貌似看到几样有趣的东西:android:textAttrChanged
、textAttrChanged参数
、ListenerUtil.trackListener
牵连起来的线索
android:textAttrChanged
这个关键词语出现了两次,第一次是出现在InverseBindingAdapter
里面。
很明显,这不是偶然。
我们继续将目光放在setTextWatcher
方法里面,发现里面还有一个textAttrChanged
参数,这是一个InverseBindingListener
对象。InverseBindingListener
是用来做什么的?直接到官网搜一下,得到这么一个解释:A listener implemented by all two-way bindings to be notified when a triggering change happens.
翻译过来的大意就是所有的双向绑定都必须实现这个监听,当事件触发时,这个监听将得到通知。
InverseBindingListener
具体是怎么实现双向监听的,我们先来看下它的源码吧:
public interface InverseBindingListener {
void onChange();
}
很简单的一个回调接口,我们再看看下DataBinding生成的Java类里的代码吧,这里面应该有这个接口的实现(生成类的路径一般都是app\build\generated\source\apt\debug\XXX):
// values
// listeners
// Inverse Binding Event Handlers
private android.databinding.InverseBindingListener mboundView1androidTextAttrChanged = new
android.databinding.InverseBindingListener() {
@Override
public void onChange() {
// Inverse of item.price
//is item.setPrice((java.lang.String) callbackArg_0)
java.lang.String callbackArg_0 = android.databinding.adapters.TextViewBindingAdapter.getTextString(mboundView1);
// localize variables for thread safety
// item
com.fritz.study.Model.InfoBean item = mItem;
// item != null
boolean itemJavaLangObjectNull = false;
// item.price
java.lang.String itemPrice = null;
itemJavaLangObjectNull = (item) != (null);
if (itemJavaLangObjectNull) {
item.setPrice(((java.lang.String) (callbackArg_0)));
}
}
};
看到这里的代码,我相信很多人都明白了这个双向绑定到底是怎么回事了。
TextViewBindingAdapter.getTextString
,这个方法是InverseBindingAdapter
的attribute
(属性)标注的方法!
到这里,可以推导出InverseBindingAdapter
里面的两个注解的作用了:
event
:这个是生成InverseBindingListener
对象的标签名字,用于在DataBinding生产类中创建InverseBindingListener
对象时来起名字用的,正如上面的mboundView1android**TextAttrChanged**参数。
同时DataBinding也是通过这个textAttrChanged
的event
标签找到setTextWatcher
方法,而setTextWatcher
通知InverseBindingListener
的onChange
方法,而onChange
方法则使用找到的get和set方法去进行检查和更新。
attribute
:这个标签很明显就是用来标注getTextString
方法了,DataBinding就是通过这个标签名字找到这个方法的
潜伏起来的陷阱
分析到这里就已经基本搞明白双向绑定的原理,不过其实我们细细思考一下这里双向绑定的逻辑:用户输入触发onChange
事件发生,更新了参数的属性,而属性改变又会刷新EditText
的文本,而EditText
的文本改变又会触发onChange
事件发生,这不是成了一个互相触发的死循环吗?
这点我们能想到,Google的工程师也肯定早就想到了,他们在TextViewBindingAdapter
源码里面的解决方法如下:
@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);
}
在上面的setText
方法可以看到,如果两次的文本没有改变,则会直接return,杜绝了无限循环调用的可能性。
如果是自己做自定义双向绑定的话,必须要注意这一点!
差点掉到一个大坑里面,不能大意再看下有无什么潜伏起来的陷阱。我们将目光回到setTextWatcher
,发现里面还有一个差点漏掉的ListenerUtil.trackListener
。我相信Google工程师不会无缘无故在这里写这么一句代码的,我们先点进去看下代码的具体实现:
原来就是通过某个id来从View里面拿出一个listener,然后将这个id作为一个新的listener键值来将这个新的listener传入View里面保存,然后返回这个取出的listener。
final TextWatcher oldValue = ListenerUtil.trackListener(view, newValue, R.id.textWatcher);
if (oldValue != null) {
view.removeTextChangedListener(oldValue);
}
if (newValue != null) {
view.addTextChangedListener(newValue);
}
那这段代码的作用就是追踪TextView
之前的listener,将其移除后再给TextView
添加新的listener。
考虑到我们可能只会绑定某一个或两个的TextWatcher
的接口方法,如果一个页面多次使用这个TextWatcher
方法来设置监听的话,可能会出现listener混乱的情况,为了避免这个问题,我们最好是在给TextView
添加listener之前检查它是否持有listener,如果有,就将旧listener移除再add新的listener。
结语
到此我们就清楚了DataBinding双向绑定要怎么玩了,双向绑定说起来很高大上,但是其实搞明白了也很简单。