6. 属性 Setter
当绑定数据发生变动时,生成的 binding 类必须根据 binding 表达式调用 View 的 setter 函数来修改 View 的属性。Data binding 框架内置了几种自定义赋值给 view 的方法。
6.1. 自动 Setter
对一个 attribute 来说,data binding 会自动尝试寻找对应的 setAttribute 函数。属性的命名空间不会对这个过程产生影响,只有属性的命名才是决定因素。
举个例子,针对一个与 TextView 的 android:text 绑定的表达式,data binding会自动寻找 setText(String) 函数。如果表达式返回值为 int 类型, data binding则会寻找 setText(int) 函数。所以需要小心处理函数的返回值类型,必要的时候使用强制类型转换。需要注意的是,data binding 在对应名称的属性不存在的时候也能继续工作。你可以轻而易举地使用 data binding 为任何 setter “创建” 属性。举个例子,support 库中的 DrawerLayout 并没有任何属性,但是有很多 setter,所以你可以使用自动 setter 的特性来调用这些函数。
<android.support.v4.widget.DrawerLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:scrimColor="@{@color/scrim}"
app:drawerListener="@{fragment.drawerListener}"/>
6.2. 重命名 Setter
一些属性的命名与 setter 不对应。针对这些函数,可以用 BindingMethods 注解来将属性与 setter 绑定在一起。例如,android:tint 属性可以像下面这样与 setImageTintList(ColorStateList)) 绑定,而不是 setTint,这个注解标注在任何类上面都有效果:
@BindingMethods({
@BindingMethod(type = "android.widget.ImageView",
attribute = "android:tint",
method = "setImageTintList"),
})
Android 框架中的 setter 重命名已经在库中实现了,开发者只需要专注于自己的 setter。比如:
@BindingMethods({
@BindingMethod(type = ZoomControls.class, attribute = "android:onZoomIn", method = "setOnZoomInClickListener"),
@BindingMethod(type = ZoomControls.class, attribute = "android:onZoomOut", method = "setOnZoomOutClickListener"),
})
public class ZoomControlsBindingAdapter {
}
6.3. 自定义 Setter
6.3.1 一些属性需要自定义 setter 逻辑。例如,目前没有与 android:paddingLeft 对应的 setter,只有一个 setPadding(left, top, right, bottom) 函数。使用BindingAdapter注解的标记静态方法,允许开发人员自定义属性对应的 setter 方法。
Android 属性已经内置一些 BindingAdapter。例如,这是一个 paddingLeft 的自定义 setter:
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
BindingAdapter 在其他自定义类型上也很好用。比如,一个 loader 可以在非主线程加载图片。
当存在冲突时,开发者创建的 binding adapter 方法会覆盖 data binding 的默认 adapter 方法。
6.3.2 你也可以创建有多个参数的 Adapter 方法:
@BindingAdapter({"app:imageUrl", "app: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}”/>
当 imageUrl 与 error 属性都存在时,并且 imageUrl 是一个 String,error 是一个 Drawable,这个 adapter 会被调用。
属性与自定义 setter 在匹配时,自定义命名空间会被忽略 也可以为 android 命名空间编写 adapter 6.3.3 BindingAdapter 注解标记的 setter 方法可以获取属性旧的赋值。要使用旧值,只需要将旧值放置在前,新值放置在后:
@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());
}
}
6.3.4 事件处理程序只能与一个抽象方法的接口或抽象类一起使用。例如:
@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);
}
}
}
6.3.5 当 listener 内置多个函数时,必须分割成多个 listener 和对应的多个属性。例如,View.OnAttachStateChangeListener 内置两个函数:onViewAttachedToWindow()) 与 onViewDetachedFromWindow())。在这里必须为两个不同的属性创建不同的接口。
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewDetachedFromWindow {
void onViewDetachedFromWindow(View v);
}
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewAttachedToWindow {
void onViewAttachedToWindow(View v);
}
因为改变一个 listener 会影响到另外一个,我们必须编写三个不同的 adapter,包括修改一个属性的,和修改两个属性的。
@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 是 add/remove View.OnAttachStateChangeListener 而不是 set。可以使用 android.databinding.adapters.ListenerUtil 来辅助跟踪旧的 listener 并移除它。
对于 addOnAttachStateChangeListener(View.OnAttachStateChangeListener)) 支持的 api 版本,通过向 OnViewDetachedFromWindow 和 OnViewAttachedToWindow 添加 @TargetApi(VERSION_CODES.HONEYCHOMB_MR1) 注解,data binding 代码生成器会知道这些 listener 只会在 Honeycomb MR1 或更新的设备上使用。
通过自定义 setter 可以通过在 xml 中的表达式唤起任意 view 中的方法,并且在调用 view 方法之前添加我们自己的 view 控制逻辑。
比如用于给 TextView 添加 xml 字体设置属性:
<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);
}
}