本文介绍了Data Binding表达式语言、观察者模式的高级应用、在RecylerView中的应用、属性Setters的详细使用等。
表达式语言
常见特性
表达式语言看起来和Java表达式语言很像,这些是相同的:
- 算术运算 + - / * %
- 字符串连接 +
- 逻辑运算 && ||
- 位运算 & | ^
- 一元运算 + - ! ~
- 位移 >> >>> <<
- 比较运算 == > < >= <=
- instanceof
- 分组 ()
- 字面 character, String, numeric, null
- Cast(强转)
- 方法调用
- 域访问
- 数组访问 []
- 三元运算 ?:
例如:1
2
3android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
不支持的操作
- this
- super
- new
- Explicit generic invocation
空合并操作符
空合并操作符和三元操作符有类似功能,当左边不为null时取左边的值否则取右边的值,例如:1
android:text="@{user.displayName ?? user.lastName}"
这和下面的代码功能一样:1
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
集合
常见的集合:arrays, lists, sparse lists, and maps,可以使用[]操作符方便的访问。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<data>
<import type="android.util.SparseArray"/>
<import type="java.util.Map"/>
<import type="java.util.List"/>
<variable name="list" type="List<String>"/>
<variable name="sparse" type="SparseArray<String>"/>
<variable name="map" type="Map<String, String>"/>
<variable name="index" type="int"/>
<variable name="key" type="String"/>
</data>
…
android:text="@{list[index]}"
…
android:text="@{sparse[index]}"
…
android:text="@{map[key]}"
资源
使用正常的语法也可以访问资源作为表达式的部分:
1 | android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}" |
格式化字符串和多元化也可以通过提供的参数计算:1
2android:text="@{@string/nameFormat(firstName, lastName)}"
android:text="@{@plurals/banana(bananaCount)}"
String Literals
当属性值使用单引号时,可以很方便地在表达式中使用双引号:
1 | android:text='@{map["name"]}' |
当属性值使用双引号时,表达式中需要使用后引号(`)或(")。
1 | android:text="@{map[`name`]}" |
数据对象
Data Binding真正强大的地方在于观察者模式的应用。这有三个不同的数据类型可以使用:Observable objects, observable fields, 和 observable collections。
当这些可观察的对象中的一个绑定到UI时,数据对象的属性发生改变时,UI会被自动更新。
Observable Objects
实现可观察的对象只需要继承BaseObservable类,给getter添加Bindable注解,在setter中进行通知就可以了。
1 | public class Score extends BaseObservable { |
在编译期间Bindable注解会在BR类中生成一个入口。BR类会在module包中生成。
ObservableFields
假如我们只需要监听bean里其中几个变量值的改变,我们可以使用ObservableFields来实现。在上一篇文章中我们使用了ObservableBoolean创建了一个变量用于控制view的显示。它的子类有:
- ObservableBoolean
- ObservableByte
- ObservableChar
- ObservableShort
- ObservableInt
- ObservableLong
- ObservableFloat
- ObservableDouble
- ObservableParcelable
在User类中添加一个电话字段:1
2
3
4public final ObservableField<String> phone = new ObservableField<>();
this.phone.set(“12344444444”); // set the value
this.phone.get();//get the value
Observable Collections
Observable collections允许通过key访问数据对象。当key是一个引用类型(如:String)时ObservableArrayMap是非常有用的。1
2
3
4ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);
在布局文件中,通过String的key可以访问map:1
2
3
4
5
6
7
8
9
10
11
12
13<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是有用的:1
2
3
4ObservableArrayList<Object> user = new ObservableArrayList<>();
user.add("Google");
user.add("Inc.");
user.add(17);
在布局中通过索引访问list:1
2
3
4
5
6
7
8
9
10
11
12
13
14<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"/>
RecylerView中的应用
DataBinding在RecylerView中的应用,我们在ViewHolder类添加getBinding方法并返回ViewDataBinding,在onBindViewHolder方法中通过setVariable设置变量的值,通过使用executePendingBindings方法可以立即执行绑定。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public static class ViewHolder extends RecyclerView.ViewHolder {
private ItemUserBinding binding;
public ViewHolder(ViewDataBinding binding) {
super(binding.getRoot());
this.binding = (ItemUserBinding) binding;
}
public ViewDataBinding getBinding() {
return binding;
}
}
@Override
public UserAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new ViewHolder(ItemUserBinding.inflate(mLayoutInflater, parent, false));
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
final User user = users.get(position);
holder.getBinding().setVariable(BR.user, user);
holder.getBinding().executePendingBindings();
}
属性Setters
无论绑定的值何时改变,生成绑定的类必须使用绑定表达式在View上调用setter方法。Data Binding框架支持自定义设置值的方法。
自动Setters
对于一个属性,data binding会尝试找到setAttribute方法。跟属性的命名空间没关系,只和属性名称相关。例如,一个表达式和TextView的android:text属性相关,它会寻找setText(String)。如果表达式返回int,data binding会搜索setText(int)方法。注意表达式需要返回正确的类型,必要时可以强转。注意如果给的属性名称不存在Data Binding仍会起作用。你可以通过使用data binding很方便地创建任何setter属性。例如,support包中DrawerLayout没有任何属性,但有大量的setter。你可以通过自动setters使用这些。
1 | <android.support.v4.widget.DrawerLayout |
重命名Setters
有一些setter的属性和名称不匹配。对于这些方法,属性可以通过BindingMethod注解和setter进行关联。这必须和一个类关联并且包含BindingMethod注解,对于每个重命名的方法。例如,android:tint实际上和setImageTintList(ColorStateList)关联,而不是setTint。1
2
3
4
5@BindingMethods({
@BindingMethod(type = "android.widget.ImageView",
attribute = "android:tint",
method = "setImageTintList"),
})
开发者一般用不到这个功能,这里只是让大家了解Data Binding有这个功能。
自定义Setters
一些属性需要自定义绑定逻辑。例如,对于android:paddingLeft属性没有相关的setter。而是有一个setPadding(left, top, right, bottom)存在。开发者可以使用BindingAdapter注解在静态的方法上来自定义setter。
例如,这有一个设置paddingLeft属性:1
2
3
4
5
6
7@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
Binding adapters对于其它自定义类型是有用的。例如,自定义一个loader可以在子线程加载图片。
当有冲突的时候,开发者创建的binding adapters会重写data binding默认的adapters。
BindingAdapter也可以接收多个参数:1
2
3
4@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);
}
1 | <ImageView app:imageUrl="@{venue.imageUrl}" |
如果ImageView的属性imageUrl和error都存在,并且imageUrl是string和error是drawable,那这个adapter将会被调用。
- 自定义的命名空间会被忽略。
- 你也可以为android命名空间写adapters。
- 如果是为android的命名空间定义的BindingAdapter,在使用时也必须使用android命名空间。
BindingAdapter处理的时候可能需要原先的值。1
2
3
4
5
6
7
8
9@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int oldPadding, int newPadding) {
if (oldPadding != newPadding) {
view.setPadding(newPadding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
}
事件handlers可能只需要一个接口或一个抽象方法的抽象类。例如:1
2
3
4
5
6
7
8
9
10
11
12@BindingAdapter("android:onLayoutChange")
public static void setOnLayoutChangeListener(View view, View.OnLayoutChangeListener oldValue,
View.OnLayoutChangeListener newValue) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
if (oldValue != null) {
view.removeOnLayoutChangeListener(oldValue);
}
if (newValue != null) {
view.addOnLayoutChangeListener(newValue);
}
}
}
当listener有多个方法,必须被分为多个listener。例如: View.OnAttachStateChangeListener有两个方法onViewAttachedToWindow() 和 onViewDetachedFromWindow()。我们必须创建两个接口区分属性并处理它们。
1 | @TargetApi(VERSION_CODES.HONEYCOMB_MR1) |
因为改变listener也会影响到其它的,我们必须定义三个不同的binding adapters,每个属性对应一个并且一个对应所有的,它们都应该设置。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44@BindingAdapter("android:onViewAttachedToWindow")
public static void setListener(View view, OnViewAttachedToWindow attached) {
setListener(view, null, attached);
}
@BindingAdapter("android:onViewDetachedFromWindow")
public static void setListener(View view, OnViewDetachedFromWindow detached) {
setListener(view, detached, null);
}
@BindingAdapter({"android:onViewDetachedFromWindow", "android:onViewAttachedToWindow"})
public static void setListener(View view, final OnViewDetachedFromWindow detach,
final OnViewAttachedToWindow attach) {
if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1) {
final OnAttachStateChangeListener newListener;
if (detach == null && attach == null) {
newListener = null;
} else {
newListener = new OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
if (attach != null) {
attach.onViewAttachedToWindow(v);
}
}
@Override
public void onViewDetachedFromWindow(View v) {
if (detach != null) {
detach.onViewDetachedFromWindow(v);
}
}
};
}
final OnAttachStateChangeListener oldListener = ListenerUtil.trackListener(view,
newListener, R.id.onAttachStateChangeListener);
if (oldListener != null) {
view.removeOnAttachStateChangeListener(oldListener);
}
if (newListener != null) {
view.addOnAttachStateChangeListener(newListener);
}
}
}
上面的例子比正常情况稍微有点复杂,因为View对于listener使用添加和移除而不是View.OnAttachStateChangeListener设置方法。android.databinding.adapters.ListenerUtil类可以帮助保持之前监听listeners的追踪,因此在Binding Adaper中它们可以被移除。
通过在接口 OnViewDetachedFromWindow 和 OnViewAttachedToWindow 使用@TargetApi(VERSION_CODES.HONEYCOMB_MR1)注解,data binding的代码生成器只有在运行在Honeycomb MR1及以后的版本中才会生成listener,通过addOnAttachStateChangeListener(View.OnAttachStateChangeListener)有相同版本的支持。
转换器
对象转换
当从Binding表达式返回一个对象,一个setter会从自动、重命名以及自定义的setters中选择。该对象将被转换为所选择的setter的参数类型。
这是为了方便使用ObservableMaps来保存数据。例如:1
2
3
4<TextView
android:text='@{userMap["lastName"]}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
userMap返回一个对象并且该对象将自动转换为setText(CharSequence)的参数类型。当参数类型可能混乱时,开发人员需要在表达式中转换。
自定义转换
有时候在特定类型之间应该自动转换,例如,当设置背景时:1
2
3
4<View
android:background="@{isError ? @color/red : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
这里背景需要一个Drawable,但color是一个integer。期望一个Drawable但返回的是一个integer,integer应该被转换为ColorDrawable。这个转换是通过带有BindingConversion注解的静态方法所完成。1
2
3
4@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
return new ColorDrawable(color);
}
注意转换只发生在setter级别,因此它不允许像这样的混合类型:1
2
3
4<View
android:background="@{isError ? @drawable/error : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
来看看最后的效果:
完整示例代码可以从这里找到:https://github.com/yuweiguocn/DataBindingDemo
常见问题
- Binding类报错,但可以运行,在布局根元素添加id属性即可
参考链接:https://developer.android.com/topic/libraries/data-binding/index.html