绑定适配器

绑定适配器负责进行适当的框架调用来设置值。例如,设置属性值,例如调用 setText() 方法。另一个示例是设置事件监听器,例如调用 setOnClickListener() 方法。

借助数据绑定库,您可以指定为设置值而调用的方法,提供自己的绑定逻辑,以及使用适配器指定返回对象的类型。

设置属性值

每当绑定值发生更改时,生成的绑定类都必须使用绑定表达式对视图调用 setter 方法。您可以让数据绑定库自动确定方法,也可以明确声明方法或提供用于选择方法的自定义逻辑。

自动选择方法

对于名为 example 的属性,库会自动查找接受兼容类型作为参数的 setExample(arg) 方法。不考虑属性的命名空间。搜索方法时,系统只会使用属性名称和类型。

例如,给定 android:text="@{user.name}" 表达式,库会查找接受 user.getName() 返回的类型的 setText(arg) 方法。如果 user.getName() 的返回值类型为 String,该库会查找接受 String 参数的 setText() 方法。如果该表达式返回 int,该库会搜索接受 int 参数的 setText() 方法。表达式必须返回正确类型。您可以根据需要对返回值进行类型转换。

即使不存在具有给定名称的特性,数据绑定也会起作用。您可以使用数据绑定为任何 setter 创建属性。例如,支持类 DrawerLayout 没有属性,但有大量 setter。以下布局会自动将 setScrimColor(int)addDrawerListener(DrawerListener) 方法分别用作 app:scrimColorapp:drawerListener 属性的 setter:

<androidx.drawerlayout.widget.DrawerLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:scrimColor="@{@color/scrim}"
    app:drawerListener="@{fragment.drawerListener}">

指定自定义方法名称

一些属性具有名称不符的 setter 方法。在这些情况下,可以使用 BindingMethods 注解将属性与 setter 相关联。该注解与类一起使用,可以包含多个 BindingMethod 注解,每个重命名的方法对应一个注解。绑定方法是可添加到应用中任何类的注解。

在以下示例中,android:tint 属性与 setImageTintList(ColorStateList) 方法相关联,而不与 setTint() 方法相关联:

Kotlin

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

Java

@BindingMethods({
       @BindingMethod(type = "android.widget.ImageView",
                      attribute = "android:tint",
                      method = "setImageTintList"),
})

通常,您无需在 Android 框架类中重命名 setter。属性已经按照命名惯例实现,以便自动查找匹配的方法。

提供自定义逻辑

一些属性需要自定义绑定逻辑。例如,android:paddingLeft 属性没有关联的 setter。而是提供了 setPadding(left, top, right, bottom) 方法。借助包含 BindingAdapter 注解的静态绑定适配器方法,您可以自定义属性的 setter 的调用方式。

Android 框架类的属性已有 BindingAdapter 注解。以下示例展示了 paddingLeft 属性的绑定适配器:

Kotlin

@BindingAdapter("android:paddingLeft")
fun setPaddingLeft(view: View, padding: Int) {
    view.setPadding(padding,
                view.getPaddingTop(),
                view.getPaddingRight(),
                view.getPaddingBottom())
}

Java

@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
  view.setPadding(padding,
                  view.getPaddingTop(),
                  view.getPaddingRight(),
                  view.getPaddingBottom());
}

参数类型非常重要。第一个参数用于确定与属性关联的视图的类型。第二个参数用于确定给定属性的绑定表达式中接受的类型。

绑定适配器也适用于其他类型的自定义。例如,您可以从工作器线程调用自定义加载器来加载图片。

您还可以使用接收多个属性的适配器,如以下示例所示:

Kotlin

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

Java

@BindingAdapter({"imageUrl", "error"})
public static void loadImage(ImageView view, String url, Drawable error) {
  Picasso.get().load(url).error(error).into(view);
}

您可以在布局中使用适配器,如以下示例所示。请注意,@drawable/venueError 是指应用中的资源。用 @{} 将资源括起来可使其成为有效的绑定表达式。

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

如果将 imageUrlerror 用于 ImageView 对象,imageUrl 是字符串,而 errorDrawable,则会调用适配器。如果您希望在设置任何属性时调用适配器,请将适配器的可选 requireAll 标志设置为 false,如以下示例所示:

Kotlin

@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);
    }
}

Java

@BindingAdapter(value={"imageUrl", "placeholder"}, requireAll=false)
public static void setImageUrl(ImageView imageView, String url, Drawable placeHolder) {
  if (url == null) {
    imageView.setImageDrawable(placeholder);
  } else {
    MyImageLoader.loadInto(imageView, url, placeholder);
  }
}

绑定适配器方法可以在其处理程序中接受旧值。同时接受旧值和新值的方法必须先为属性声明所有旧值,随后再声明新值,如以下示例所示:

Kotlin

@BindingAdapter("android:paddingLeft")
fun setPaddingLeft(view: View, oldPadding: Int, newPadding: Int) {
    if (oldPadding != newPadding) {
        view.setPadding(newPadding,
                    view.getPaddingTop(),
                    view.getPaddingRight(),
                    view.getPaddingBottom())
    }
}

Java

@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());
   }
}

事件处理脚本只能与具有一种抽象方法的接口或抽象类一起使用,如以下示例所示:

Kotlin

@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)
        }
    }
}

Java

@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);
    }
  }
}

按如下方式在布局中使用此事件处理脚本:

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

当监听器具有多个方法时,必须将其拆分为多个监听器。例如,View.OnAttachStateChangeListener 有两种方法:onViewAttachedToWindow(View)onViewDetachedFromWindow(View)。该库提供了两个接口来区分它们的属性和处理程序:

Kotlin

// 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)
}

Java

@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewDetachedFromWindow {
  void onViewDetachedFromWindow(View v);
}

@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewAttachedToWindow {
  void onViewAttachedToWindow(View v);
}

由于更改一个监听器可能会影响另一个监听器,因此您需要一个适用于其中一个属性或同时适用于这两个属性的适配器。您可以在注解中将 requireAll 设置为 false,以指定并非必须为每个属性分配一个绑定表达式,如以下示例所示:

Kotlin

@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)
        }
    }
}

Java

@BindingAdapter({"android:onViewDetachedFromWindow", "android:onViewAttachedToWindow"}, requireAll=false)
public static void setListener(View view, OnViewDetachedFromWindow detach, OnViewAttachedToWindow attach) {
    if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1) {
        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);
                    }
                }
            };
        }

        OnAttachStateChangeListener oldListener = 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.adapters.ListenerUtil 类有助于跟踪这些监听器,以便在绑定适配器中将它们移除。

对象转换

自动转换对象

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

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

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

自定义转换

在某些情况下,需要在特定类型之间进行自定义转换。例如,视图的 android:background 属性需要 Drawable,但指定的 color 值是整数。以下示例展示了某个属性需要 Drawable,但实际提供了一个整数:

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

每当需要 Drawable 且返回整数时,都要将 int 转换为 ColorDrawable。如需执行转换,请使用带有 BindingConversion 注解的静态方法,如下所示:

Kotlin

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

Java

@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
    return new ColorDrawable(color);
}

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

// The @drawable and @color represent different value types in the same
// expression, which causes a build error.
<View
   android:background="@{isError ? @drawable/error : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

其他资源

如需详细了解数据绑定,请参阅以下资源。

示例

Codelab

博文