前言
本文是《 安卓 Data Binding 使用方法总结(姐姐篇)》的姊妹篇。姐姐篇写于 Google I/O 2016 之前,当时还没有双向绑定、lambda 表达式这些特性。
本文参考视频 Advanced Data Binding - Google I/O 2016 、经实战后写成,主要涉及:
- 双向绑定
- lambda 表达式
- 特殊变量
- 动画
- 自定义字体
一起来充电吧!
双向绑定
用法举例
很简单,在要使用双向绑定的地方,使用 “@={}” 即可。
<EditText android:text="@={user.firstName}" />
注意,这里的 firstName 必须是 ObservableField <T> 类型,至于原因,我们在探讨双向绑定的实现原理的时候会说明。
适用范围
双向绑定只适用于那些某个属性绑定监听事件的控件,如
- TextView/EditView/Button (android:text, TextWatcher)
- CheckBox (android:checked, OnCheckedChangeListener)
- DatePicker(android:year, android:month, android:day, OnDateChangedListener)
- TimePicker(android:hour, android:minute, OnTimeChangedListener)
- RatingBar(android:rating, OnRatingBarChangeListener)
- …
大部分控件都能满足双向绑定的需求,实在不行就自定义满足该要求的控件吧。
原理简析
双向绑定的实现原理的核心是 InverseBindingListener 这个接口:
package android.databinding;
public interface InverseBindingListener {
void onChange();
}
我们再看这个例子:
<EditText android:text="@={user.firstName}" />
此时框架生成对应的 MainActivity2WayBinding.java,摘录其中的相关代码:
private android.databinding.InverseBindingListener mboundView1androidTe = new android.databinding.InverseBindingListener() {
@Override
public void onChange() {
// some code
firstNameUser.set((java.lang.String) (callbackArg_0));
// some code
}
};
@Override
protected void executeBindings() {
if ((dirtyFlags & 0x7L) != 0) {
android.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView1, firstNameUser);
}
}
简单分析一下上述代码。
框架自动生成了一个 android.databinding.InverseBindingListener
,该 listener 的作用就是更新 firstName 的值,即 firstName.set()。所以 firstName 必须是 ObservableField<T> 类型。
然后该 listener 被绑定到 EditText 上。
firstName 值被更新时,会执行 executBindings(),调用 TextViewBindingAdapter.setText() 方法:
@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);
}
只有值发生改变时才会更新 EditText 的值,否则什么都不做,防止出现无限循环。
lambda 表达式
<Button android:onClick="@{(v) -> presenter.save(v, user)}" />
在这个 lambda 表达式中,OnClickListener.onClick(View v)
没有返回值,presenter.save(v, user)
有或没有返回值都能正常运行;但是如果事件的方法有返回值,如 OnLongClickListener.onLongClick(View v)
返回布尔值,则 android:onLongClick='@{(v) -> presenter.save(v, user)}'
中的方法 presenter.save(v, user)
必须返回对应类型的值,否则编译报错。
@{(v) -> presenter.save(v, user)}
中,-> 前面的参数要么为空,要么全部列出来并和对应的 listener 的回调方法的参数保持一致,参数可随意命名,比如 OnFocusChangeListener.onFocusChanged(View v, boolean hasFocus)
,可以写成 android:hasFocus='@{() -> presenter.refresh(fcs)}'
或 android:hasFocus='@{(v, fcs) -> presenter.refresh(fcs)}'
的形式。
除了 lambda 表达式,我们还可以使用方法引用(method reference)的形式: android:onClick='@{presenter.save}'
,这两种方式有什么区别呢?直接引用发布会上 keynote 中的截图:
和
在回调方法的参数方面也有所不同,在 lambda 表达式的参数可以是任意表达式,而方法引用的参数则必须要和 listener 的回调方法保持一致:
lambda
"... = @{()->presenter.save(user.friend)}"
"... = @{()->data.presenter.save(user.friend)}"
this.saveButton.setOnClickListener(this);
void onCick(View view) {
Presenter presenter = this.presenter;
Item item = this.item;
if (presenter != null) {
presenter.saveItem(item);
}
}
方法引用
"...onClick=@{presenter::save}"
Presenter presenter = this.presenter;
if (presenter != null) {
this.saveButton.setOnClickListener(new Listener(presenter));
} else {
this.saveButton.setOnClickListener(null);
}
class Listener implements OnClickListener {
void onClick(View view) {
mPresenter.onClick(view);
}
}
注意,方法引用中,回调方法返回值的类型(不管是 void,boolean,还是其他)都要一致,否则编译报错。如,...onClick='@{presenter::save}'
中 Presenter.save(View v) 的返回值类型要和 OnClickListener.onClick(View v) 一致。
方法引用不止能在 android:onClick=
属性中使用,在其他属性中也可以使用,而且可以使用表达式作为参数,见 特殊变量->Context 一节中的例子:
<TextView
android:id="@+id/context_demo"
android:text="@{user.load(context, @id/context_demo)}" />
public String load(Context context, int field) {
return context.getResources().getString(R.string.app_name);
}
特殊变量
带 id 的控件
可以在表达式中直接引用带 id 的 view,引用时采用驼峰命名法。
将一个控件的属性赋值给另一个属性,这样我们可以在 layout 中完成 UI 的展示逻辑,简洁而且可读性强,从而让开发者把精力集中在业务逻辑的开发。
如下面的代码,是根据 CheckBox 是否勾选而决定是否展示相应的控件。第一个 EditText 的表达式中引用了 CheckBox 的属性 checked,第二个 EditText 引用第一个 EditText 的 visibility 属性。
<CheckBox
android:id="@+id/checkbox"
android:text="填写姓名" />
<EditText
android:id="@+id/first_name"
android:text="@={user.firstName}"
android:visibility="@{checkbox.checked ? View.VISIBLE : View.GONE}" />
<EditText
android:text="@{user.lastName}"
android:visibility="@{firstName.visibility}" />
Context
现在我们可以脱离具体的 View 就能得到 Context,得到 Context 是根 view 的 Context。
注意如果有名为 context 的自定义变量存在,前者会被覆盖掉。
<TextView
android:id="@+id/context_demo"
android:text="@{user.load(context, @id/context_demo)}" />
public String load(Context context, int field) {
return context.getResources().getString(R.string.app_name);
}
动画
在 DataBinding 中,我们可以使用 Transition (适用 API >= 19,系统 >= 4.4)来实现某些动画效果。如理如下:
binding.addOnRebindCallback(new OnRebindCallback() {
@Override
public boolean onPreBind(ViewDataBinding binding) {
ViewGroup root = (ViewGroup) binding.getRoot();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
TransitionManager.beginDelayedTransition(root);
}
return true;
}
});
两个 TextView 在隐藏和显示时会有延迟效果,如下图:
但是这种方法对某些情况是失效的,如随着滚轮的滑动 TextView 的内容发生改变:
更具普遍性的方法是在 @BindingAdapter 修饰的方法中进行设置:
@BindingAdapter("adText")
public static animateTextChanges(TextView textView, String oldText, String newText) {
if (oldText == null || oldText.equals(newText)) {
return;
}
animateTextChange(textView, oldText, newText);
}
自定义字体
我们可以通过如下方式来为 TextView 自定义字体:
<TextView app:font="@{`Source-Sans-Pro-Regular.ttf`}"/>
public class AppAdapters {
@BindingAdapter({"font"})
public static void setFont(TextView textView, String fontName){
AssetManager assetManager = textView.getContext().getAssets();
String path = "fonts/" + fontName;
Typeface typeface = sCache.get(path);
if (typeface == null) {
typeface = Typeface.createFromAsset(assetManager, path);
sCache.put(path, typeface);
}
textView.setTypeface(typeface);
}
}
更多信息请参考开源项目 fontbinding。
最佳实践
通过一个登录页面的 layout 布局,来感受一下双向绑定、字符串引用等的使用方法:
<layout>
<data>
<variable name="model" type="iotalks.ForModel" />
<import type="iotalks.Validator" />
</data>
<LinearLayout>
<TextView android:text="@={model.name}"/>
<Button android:enabled="@{Validator.isValid(model)}" android:onClick="@{()->presenter.save(model)}" android:text="@{@string/welcome(model.name)}" />
</LinearLayout>
</layout>
如果还不过瘾,请参考开源项目:Android Architecture Blueprints [beta],绝对让你茅塞顿、豁然开朗、醍醐灌顶、如梦初醒。
更多资料
也许本文不值得一看,但是下面这些资料则不然。
- Data Binding Guide
- Advanced Data Binding - Google I/O 2016
- Android Architecture Blueprints [beta]
- LoginDemo4DataBinding
- Data Binding – Write Apps Faster (Android Dev Summit 2015)
- Marshmallow Brings Data Bindings to Android
- 精通 Android Data Binding
- Android Data Binding从抵触到爱不释手
- 安卓 Data Binding 使用方法总结(姐姐篇)