(译) Data Binding 绑定适配器

原文链接

绑定适配器负责在设置值的时候做出合适的系统调用。一个例子是设置一个属性值,这个值会调用 setText() 方法。另一个是设置事件监听,这个会调用 setOnClickListener() 方法。

数据绑定库允许你为一个值指定方法调用,提供你自己的绑定逻辑,以及指定使用适配器时要返回的类型。

设置属性值

每当绑定值发生更改时,生成的绑定类必须使用绑定表达式在视图上调用 setter 方法。你可以允许数据绑定库自动确定方法,显式声明方法或提供自定义逻辑来选择方法。

自动方法选择

对于一个叫做 exmaple 的属性,库自动地尝试去找出方法 setExample(args) ,这个方法接受兼容的类型作为参数。属性的命名空间不是考虑的内容,在搜索方法的时候只有属性名和类型是被使用的。

例如,android:text=”@{user.name}” 表达式,库查找的是 setText(arg 方法,这个方法接收了由 user.getName() 返回的类型。如果 user.getName() 返回类型是String ,库会查找接收 String 类型变量的 setText() 方法。如果表达式返回的是 int 类型的参数,库会查找接收 int 类型参数的 setText()方法。表达式必须返回正确的类型,必要的时候你可以对返回值进行强制转换。

数据绑定在给出的名称的属性不存在的时候也能工作。你可以对所有的 setter 方法创建属性。例如,支持库类 DrawerLayout 没有一个属性,但是有大量的 setter。以下的布局自动使用了 setScrimColor(int) 以及 setDrawerListener(DrawListener) 方法作为app:scrimColor以及app:drawerListener` 属性各自的 setter。

1
2
3
4
5
<android.support.v4.widget.DrawerLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:scrimColor="@{@color/scrim}"
app:drawerListener="@{fragment.drawerListener}">

指定一个自定义的方法名称

一些属性有着跟名称不对应的 setter 。在这些情形中,属性和 setter 之间是用BindingMethods 注解相关联的。注解在类中使用并可以包含多个BindingMethod 注解,每个重命名的方法都有一个。Binding methods 是能被添加到你应用中任意类的注解。在下例中,android:tint 属性是和setImageTintList(ColorStateList) 方法相关联的,而不是 setTint() 方法:

1
2
3
4
5
@BindingMethods(value = [
BindingMethod(
type = android.widget.ImageView::class,
attribute = "android:tint",
method = "setImageTintList")])

绝大数情况下,你不需要重命名 Android 框架类中的 setter。属性已经按名称约束实现了,会自动找到对应的方法。

提供自定义的逻辑

一些属性需要自定义的绑定逻辑。例如,没有针对 android:paddingLeft 属性的 setter。作为替代,被提供的是 setPadding(left,top,right,bottom)。有着BindingAdapter 注解的静态绑定方法允许你在属性被调用的时候自定义 setter。

Android 框架类的属性已有有创建好的 BindingAdapter。例如,以下的例子展示了针对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());
}

参数类型是十分重要的。首个参数决定了属性相关的视图的类型。第二个参数决定了给定参数在绑定表达式中接收的类型。

绑定适配器对其他类型的自定义是十分有用的。例如,一个自定义的加载器可以从工作线程加载图片。

当存在冲突的时候,你自定义的适配器会重载由 Android 框架提供的默认适配器。

你可以有接收多个属性的适配器,如下例所示:

1
2
3
4
@BindingAdapter("imageUrl", "error")
fun loadImage(view: ImageView, url: String, error: Drawable) {
Picasso.get().load(url).error(error).into(view)
}

你可以像下例一样在你的布局中使用适配器。注意,@drawable/vennuErroe指向的是你应用中的资源。将资源包裹在@{}中,使其成为一个合法的绑定表达式。

1
<ImageView app:imageUrl="@{venue.imageUrl}" app:error="@{@drawable/venueError}" />

注意:处于匹配的目的,数据绑定库忽略了自定义的命名空间。

这个适配器在 imageUrlerror 同时被用到 ImageView 对象的时候会被使用,imageUrl 是一个字符串并且 error 是一个 Drawable 。如果你想要任一的属性设置之后就能让让适配器被调用,你可以把适配器的可选的 requireAll 标记设置成 false ,如下例所示:

1
2
3
4
5
6
7
8
@BindingAdapter(value = ["imageUrl", "placeholder"], requireAll = false)
fun setImageUrl(imageView: ImageView, url: String, placeHolder: Drawable) {
if (url == null) {
imageView.setImageDrawable(placeholder);
} else {
MyImageLoader.loadInto(imageView, url, placeholder);
}
}

注意当有冲突的时候,你的绑定适配器会重载默认的绑定适配器。

绑定适配器方法可能会把旧的值带到他们的 handler 中。一个接收旧和新的值的方法,应该先声明旧的值,然后才是新的值,如下例所示:

1
2
3
4
5
6
7
8
9
@BindingAdapter("android:paddingLeft")
fun setPaddingLeft(view: View, oldPadding: Int, newPadding: Int) {
if (oldPadding != newPadding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom())
}
}

事件处理可能只能在只有一个抽象方法的接口或抽象类中使用,如下例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@BindingAdapter("android:onLayoutChange")
fun setOnLayoutChangeListener(
view: View,
oldValue: View.OnLayoutChangeListener?,
newValue: View.OnLayoutChangeListener?
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
if (oldValue != null) {
view.removeOnLayoutChangeListener(oldValue)
}
if (newValue != null) {
view.addOnLayoutChangeListener(newValue)
}
}
}

按如下所示在你的布局中使用这个事件处理器:

1
<View android:onLayoutChange="@{() -> handler.layoutChanged()}"/>

当一个监听器有多个方法的时候,它必须划分为多个监听器。例如, View.OnAttachStateChangeListener 有着两个方法:OnViewAttachedToWindown(View) 以及 onViewDetachedFromWindow(View)。这个库提供了两个接口来区分它们的属性和 handler。

1
2
3
4
5
6
7
8
9
10
// Translation from provided interfaces in Java:
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
interface OnViewDetachedFromWindow {
fun onViewDetachedFromWindow(v: View)
}

@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
interface OnViewAttachedToWindow {
fun onViewAttachedToWindow(v: View)
}

因为改变一个监听器的同时也会影响另一个,你需要一个对单个同时两个属性都好用的适配器。你可以在注解在把 requireAll 设置成 false 来制定不是每个属性都必须在绑定表达式中被赋值,如下例所示:

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
@BindingAdapter(
"android:onViewDetachedFromWindow",
"android:onViewAttachedToWindow",
requireAll = false
)
fun setListener(view: View, detach: OnViewDetachedFromWindow?, attach: OnViewAttachedToWindow?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
val newListener: View.OnAttachStateChangeListener?
newListener = if (detach == null && attach == null) {
null
} else {
object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {
attach?.onViewAttachedToWindow(v)
}

override fun onViewDetachedFromWindow(v: View) {
detach?.onViewDetachedFromWindow(v)
}
}
}

val oldListener: View.OnAttachStateChangeListener? =
ListenerUtil.trackListener(view, newListener, R.id.onAttachStateChangeListener)
if (oldListener != null) {
view.removeOnAttachStateChangeListener(oldListener)
}
if (newListener != null) {
view.addOnAttachStateChangeListener(newListener)
}
}
}

以上的例子跟常见的相比要稍微复杂一些,因为 View 类使用的是addOnAttachStateChangeListener() 以及 removeOnAttachStateChangeListener() 而不是 OnAttachStateChangeListener的 setter 方法。android.databinding.adapter.ListenerUtil 类帮助记录了之前的监听器,所以它们能在绑定适配器中被移除。

在接口 OnViewDetachedFromWindow 以及 OnViewAttachedToWindow 添加@TargetApi(VERSION_CODES.HONEYCOMB_MR1 注解,数据绑定代码生成器知道监听器只在运行在 Android 3.1(API level 12) 以及更高版本的时候被生成,与addOnAttachStateChangeListener() 方法支持的版本一致。

对象转换

自动对象转换

从绑定表达式返回Object时,库会选择用于设置属性值的方法。 Object被强制转换为所选方法的参数类型。在使用ObservableMap类存储数据的应用程序中,此行为很方便,如以下示例所示:

<TextView
       android:text='@{userMap["lastName"]}'
       android:layout_width="wrap_content"
       android:layout_height="wrap_content" />

注意:你还可以使用 object.key 表示法引用地图中的值。例如,上面示例中的@ {userMap[”lastName“]} 可以替换为 @{userMap.lastName}。

表达式中的 userMap 对象返回一个值,该值自动转换为 setText(CharSequence 方法中的参数类型,该方法用于设置 android:text 属性的值。如果参数类型不明确,则必须在表达式中强制转换返回类型。

自定义对象转换

在一些情形中,在特有的类型之间,一个自定义的转换是需要的。例如,视图的android:background 属性期待的是Drawable,但是color值指定的是一个整形。以下的例子展示的是属性期待的是Drawable ,但是提供的却是整形的值。

1
2
3
4
<View
android:background="@{isError ? @color/red : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

当期待的是 Drawable,但是返回的却是 integer 的时候,int 应该被转换成ColorDrawable。这个转换过程可以用一个带有BindingConversion 注解的静态方式完成,以下:

1
2
@BindingConversion
fun convertColorToDrawable(color: Int) = ColorDrawable(color)

但是,在绑定表达式中提供的值类型必须是一致的。你不能在相同的表达式中使用不同的类型,如下所示:

1
2
3
4
<View
android:background="@{isError ? @drawable/error : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
非典型前端coder wechat
想要随时Follow我的最新博客,可扫码关注我的公众号