数据对象
任何的POJO 对象都可以用作数据绑定,但是修改一个 POJO 对象不会更新 UI。 数据绑定的威力在于,赋予数据对象在数据改变的时候通知其他组件的能力。有三种数据改变通知机制:Observable 对象、ObservableFields 和 observable 集合。
如果这三种类型中的任意一种类型的数据绑定到 UI 中,当数据改变的时候, UI 的数据也会自动更新。
实现了 android.databinding.Observable 接口的对象,可以设置一个监听器来监听所有值域变化的事件。
为了方便开发者使用,BaseObservable 类包含了添加和删除监听对象的接口,但是通知数据变化需要开发者自己来做。 和 ListView 的 Adapter 类似。
private static class User extends BaseObservable { private String firstName; private String lastName; @Bindable public String getFirstName() { return this.firstName; } @Bindable public String getFirstName() { return this.lastName; } public void setFirstName(String firstName) { this.firstName = firstName; notifyPropertyChanged(BR.firstName); } public void setLastName(String lastName) { this.lastName = lastName; notifyPropertyChanged(BR.lastName); } }
Bindable 注解在编译的时候会生成一个 BR 类中的实体,BR 位于模块的包中。如果您的数据类无法修改,则可以使用 PropertyChangeRegistry 来保存和通知改变事件。
继承 Observable 可能有点麻烦,如果你像简单一点或者只有少量几个绑定的属性,则可以使用 ObservableFields。 ObservableFields 为字包含的 observable 对象。 ObservableFields 包含了所有基本类型和一个引用类型。 使用方式如下:
private static class User extends BaseObservable { public final ObservableField<String> firstName = new ObservableField<>(); public final ObservableField<String> lastName = new ObservableField<>(); public final ObservableInt age = new ObservableInt(); }
很简单,这些变量会自动触发值改变事件,使用 get 和 set 来访问:
user.firstName.set(“Google”);int age = user.age.get();
Observable 集合如果引用的 key 为对象,则可以使用 ObservableArrayMap :
ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put(“firstName”, “Google”);
user.put(“lastName”, “Inc.”);
user.put(“age”, 17);
在布局文件中,可以通过 String key 来引用map 里面的对象:
<data>
<import type=”android.databinding.ObservableMap”/>
<variable name=”user” type=”ObservableMap<String, Object>”/>
</data>
…
<TextView
android:text=’@{user["lastName"]}’
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”/>
<TextView
android:text=’@{String.valueOf(1 + (Integer)user["age"])}’
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”/>
如果集合的 key 为整数,则使用 ObservableArrayList :
ObservableArrayList<Object> user = new ObservableArrayList<>();
user.add(“Google”);
user.add(“Inc.”);
user.add(17);
在 布局文件中可以使用索引来引用这些对象:
<data>
<import type=”android.databinding.ObservableList”/>
<import type=”com.example.my.app.Fields”/>
<variable name=”user” type=”ObservableList<Object>”/>
</data>
…
<TextView
android:text=’@{user[Fields.LAST_NAME]}’
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”/>
<TextView
android:text=’@{String.valueOf(1 + (Integer)user[Fields.AGE])}’
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”/>
生成的绑定类自动处理的 布局文件中的 View 和 变量的值,并把他们关联起来。 所有生成的绑定类都继承自 android.databinding.ViewDataBinding。
创建绑定类
绑定类应该在解析完布局后立刻创建,这样可以避免其他数据干扰布局文件中表达式的解析。获取绑定类最常用的方式是通过生成类的静态函数 inflate 。inflate 函数同时解析 View 和完成数据绑定。
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater); MyLayoutBinding binding = MyLayoutBinding.inflate(LayoutInflater, viewGroup, false);
如果布局文件解析的机制有变化,则还可以分开绑定:
MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);
有时候,绑定对象需要运行时创建,则可以通过如下方式:
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId, parent, attachToParent); ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId);
(带 ID 的 View)Views With IDs
对于 布局文件中的每个带 ID 的 View 都会生成一个 final 变量。 绑定类只解析一次布局文件,并创建每个 View。 这种方式比多次调用 findViewById 要高效一些。
例如:
<layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <variable name="user" type="com.example.User"/> </data> <LinearLayout android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user.firstName}" android:id="@+id/firstName"/> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user.lastName}" android:id="@+id/lastName"/> </LinearLayout> </layout>
生成的 绑定类会包含如下变量:
public final TextView firstName;
public final TextView lastName;
没有 ID 也可以使用数据绑定,但是为了以后引用这些 View, 添加个 id 会更加方便。
每个变量都会生成 get 和 set 函数:
<data>
<import type=”android.graphics.drawable.Drawable”/>
<variable name=”user” type=”com.example.User”/>
<variable name=”image” type=”Drawable”/>
<variable name=”note” type=”String”/>
</data>
会生成如下代码:
public abstract com.example.User getUser();
public abstract void setUser(com.example.User user);
public abstract Drawable getImage();
public abstract void setImage(Drawable image);
public abstract String getNote();
public abstract void setNote(String note);
ViewStubs 和普通的 view 不太一样。 一开始这些 view 是不可见的, 并且没有解析到 界面中,当显示 ViewStub 或者显示的解析他们的时候才会加载到界面中,替代之前的 View。
由于 ViewStub 最终会从 View 层级中消失, 所以对应的绑定对象也应该消失以便回收资源。由于 View 是 final 的,这里会使用一个 ViewStubProxy 对象来替代 ViewStub, 这样开发者就可以访问 ViewStub 了,并且当 ViewStub 被加载到 View 层级中的时候,开发者也可以访问加载的 View。
当解析另外一个布局文件的时候, 绑定对象也应该和新的布局关联起来。因此,ViewStubProxy 需要监听 ViewStub 的 OnInflateListener 回调接口来建立绑定关系。开发者可以在 ViewStubProxy 上设置一个 OnInflateListener ,当绑定建立的时候,开发者可以收到回调 函数。
高级绑定
动态变量
有时候具体绑定的类还不知道是哪个。例如,一个 RecyclerView Adapter 使用一些布局文件,只有在 onBindViewHolder 中才知道 layout 使用的是哪个变量。
下面的例子中,RecyclerView 的每个 View 都包含一个 item 变量, 通过 BindingHolder 的 getBinding 函数来访问 ViewDataBinding 。然后把 item 变量设置进去。
public void onBindViewHolder(BindingHolder holder, int position) {
final T item = mItems.get(position);
holder.getBinding().setVariable(BR.item, item);
holder.getBinding().executePendingBindings();
}
立即绑定
当变量的值更新的时候,binding 对象将在下个更新周期中更新。这样就会有一点时间间隔,如果你像立刻更新,则可以使用 executePendingBindings 函数。
只要不是集合变量,则可以在后台线程中更新数据。数据绑定将会保存每个变量的值到本地以避免多线程问题。
当绑定的值改变的时候,生成的绑定对象会调用一个 setter 函数来更新 View 的值。绑定框架可以自定义调用哪个函数来设置值。
对于一个属性,绑定框架会自动查找 setAttribute 函数。例如 TextView 的属性 android:text 上的表达式,绑定框架将会调用 TextView 的 setText(String) 函数,如果表达式返回值为 int, 则会调用 setText(int) 函数。所以,要小心表达式的返回值,如果必要可以使用 cast 来转换为需要的类型。
需要注意的是, 数据绑定框架查找的是一个 set 函数,而不是该属性是否存在。 例如 support 库中的 DrawerLayout 没有任何属性,但是有很多 set 函数,所以可以把这些函数当做属性来在 绑定布局文件中使用,只需要把函数名字的 set 去掉,并把后面的单词首字符修改为小写即可。例如:
<android.support.v4.widget.DrawerLayout
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
app:scrimColor=”@{@color/scrim}”
app:drawerListener=”@{fragment.drawerListener}”/>
DrawerLayout 有个 setScrimColor 函数,但是没有 scrimColor 这个变量。
还可以通过 BindingMethods 来重命名对应的 set 函数。 例如 android:tint 属性的 set 函数被重命名为 setImageTintList 而不是 setTint.
@BindingMethods({
@BindingMethod(type = “android.widget.ImageView”,
attribute = “android:tint”,
method = “setImageTintList”),
})
开发者一般不需要重命名 setter, android 框架已经重命名了对应的实现。
自定义 Setters
有些属性需要自定义绑定逻辑。例如, android:paddingLeft 属性并没有对应的函数, View 只有一个 setPadding(left, top, right, bottom)。通过 BindingAdapter 注解来创建一个静态的自定义 setter 函数。 android 系统已经创建了这些 BindingAdapter 函数了,例如下面是 paddingLeft 属性对应的函数:
@BindingAdapter(“android:paddingLeft”)
public static void setPaddingLeft(View view, int padding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
绑定适配器(Binding adapter)对于其他类型的定制是非常有用的。例如一个自定义的 loader 可以在其他线程中加载图片。
如果绑定适配器有冲突,则开发者自定义的将会替代系统自定义的。
一个 适配器还可以有多个参数:
@BindingAdapter({“bind:imageUrl”, “bind:error”})
public static void loadImage(ImageView view, String url, Drawable error) {
Picasso.with(view.getContext()).load(url).error(error).into(view);
}
<ImageView app:imageUrl=“@{venue.imageUrl}”app:error=“@{@drawable/venueError}”/>
如果用于 ImageView 的 imageUrl和 error 参数都存在并且 imageUrl 是 string 类型、error 是 drawable 类型 则就会调用上面定义的适配器。
在匹配适配器的时候, 会忽略自定义的命名空间你也可以为 android 命名空间的属性自定义适配器
对象转换
当绑定表达式返回一个对象时候,将会自动调用 set 函数、重命名的函数、或者自定义的 setter 中的一个。表达式返回的对象将会转换为该函数的参数类型。
使用 ObservableMaps 来保存数据会比较简单。例如:
<TextView
android:text=’@{userMap["lastName"]}’
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”/>
这里的 userMap 返回的对象将制动转换为 setText(CharSequence) 的参数。 如果参数不明确,则开发者需要强制转换为需要的类型。
自定义转换规则
有时候参数应该可以自动转换,例如
<View
android:background=”@{isError ? @color/red : @color/white}”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”/>
上面的背景需要一个 Drawable 对象,但是表达式的返回值为整数对象的颜色值。这种情况下,颜色值需要转换为 ColorDrawable。 这种转换通过一个静态函数完成,该函数带有一个 BindingConversion 注解。
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
return new ColorDrawable(color);
}
需要注意的是,转换是在 setter 层面上完成的, 所以不能混合使用不同的类型:
<View
android:background=”@{isError ? @drawable/error : @color/white}”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”/>
上面混合使用 drawable 和 int 是不可以的。
由于数据绑定框架还处于beta 测试阶段, 上面所介绍的方法可能随时出现更改,最新的信息,请参考这里: https://developer.android.com/tools/data-binding/guide.html
两个数据绑定的示例项目: