绑定适配器

绑定适配器负责发出相应的框架调用来设置值。例如,设置属性值就像调用 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)setDrawerListener(DrawerListener) 方法分别用作 app:scrimColorapp:drawerListener 特性的 setter:

<android.support.v4.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());
    }

    

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

绑定适配器对其他类型的自定义很有用。例如,可以通过工作器线程调用自定义加载程序来加载图片。

出现冲突时,您定义的绑定适配器会替换由 Android 框架提供的默认适配器。

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

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

如果 ImageView 对象同时使用了 imageUrlerror,并且 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(padding,
                        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 类有助于跟踪以前的监听器,以便在绑定适配器中将它们移除。

通过用 @TargetApi(VERSION_CODES.HONEYCOMB_MR1) 注释接口 OnViewDetachedFromWindowOnViewAttachedToWindow,数据绑定代码生成器知道只应在运行 Android 3.1(API 级别 12)及更高级别(addOnAttachStateChangeListener() 方法支持的相同版本)时生成监听器。

对象转换

自动对象转换

当绑定表达式返回 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);
    }

    

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

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

其他资源

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

示例

Codelab

博文