Android Jetpack 组件之 BindingAdapter 详解

2022-01-18 21:18:10 浏览数 (1)

本篇文章主要介绍 Binding adapters 的使用方式,内容如下:

  1. databinding 机制
  2. BindingMethods
  3. BindingAdapter
  4. BindingConversion

Databinding 机制

Binding adapters 可以作为一个设置某个值的框架来使用,databinding 库可以允许指定具体的方法来进行相关值的设置,在该方法中可以做一些处理逻辑,Binding adapters 会最终给你想要的结果,那么当我们在布局文件中使用 databinding 绑定数据时是如何调用对应的属性方法呢?

代码语言:javascript复制
 <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@{user.name}" />

当在布局文件中绑定某个数据时,比如上面的 TextView 的 text 属性,在绑定时会自动接收兼容类型的参数所对应的方法,如 setText(arg),此时 databinding 库会查找接收 user.getName() 返回类型对应的 user.setName(arg) 方法,如果 user.getName() 返回的类型是字符串,则会调用参数为 String 的 setName(arg) 方法,反之如果是 int 型,则会调用参数为 Int 的 setName(arg) 方法,所以,为了保证数据的正确性,尽量保证 xml 中表达式中返回值的正确性,当然,也可以按照实际需要进行类型转换。

从上面分析可知,在布局文件中设置了属性,databinding 库会自动查找相关的 setter 方法进行设置,也就是说,如果以 TextView 为例,只有找到某个 setter 方法就可以进行验证了,TextView 中有一个 setError(error) 方法如下:

代码语言:javascript复制
@android.view.RemotableViewMethod
public void setError(CharSequence error) {
    if (error == null) {
        setError(null, null);
    } else {
        Drawable dr = getContext().getDrawable(com.android.internal.R.drawable.indicator_input_error);
        dr.setBounds(0, 0, dr.getIntrinsicWidth(), dr.getIntrinsicHeight());
        setError(error, dr);
    }
}

这个方法主要用来提示错误信息,一般我们都是在代码中进行使用,这里我们把该方法配置到布局文件中来使用,参考如下:

代码语言:javascript复制
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{user.name,default=name}"
    app:error="@{user.name}"/>

下面是测试效果图:

因为有 setError(String error) 方法,而 user.name 返回的是 String,所以能够在这里以属性的方式进行配置。

BindingMethods

这是 databinding 库提供的一个注解,用于当 View 中的某个属性与其对应的 setter 方法名称不对应时进行映射,如 TextView 的属性 android:textColorHint 与之作用相同的方法是 setHintTextColor 方法,此时属性名称与对应的 setter 方法名称不一致,这就需要使用 BindingMethods 注解将该属性与对应的 setter 方法绑定,这样 databinding 就能够按照属性值找到对应的 setter 方法了,databinding 已经处理了原生 View 中的像这种属性与 setter 方法不匹配的情况,来看一看源码中 TextView 中这些不匹配属性的处理,参考如下:

代码语言:javascript复制
@BindingMethods({
        @BindingMethod(type = TextView.class, attribute = "android:autoLink", method = "setAutoLinkMask"),
        @BindingMethod(type = TextView.class, attribute = "android:drawablePadding", method = "setCompoundDrawablePadding"),
        @BindingMethod(type = TextView.class, attribute = "android:editorExtras", method = "setInputExtras"),
        @BindingMethod(type = TextView.class, attribute = "android:inputType", method = "setRawInputType"),
        @BindingMethod(type = TextView.class, attribute = "android:scrollHorizontally", method = "setHorizontallyScrolling"),
        @BindingMethod(type = TextView.class, attribute = "android:textAllCaps", method = "setAllCaps"),
        @BindingMethod(type = TextView.class, attribute = "android:textColorHighlight", method = "setHighlightColor"),
        @BindingMethod(type = TextView.class, attribute = "android:textColorHint", method = "setHintTextColor"),
        @BindingMethod(type = TextView.class, attribute = "android:textColorLink", method = "setLinkTextColor"),
        @BindingMethod(type = TextView.class, attribute = "android:onEditorAction", method = "setOnEditorActionListener"),
})
 
public class TextViewBindingAdapter {
    //...
}

所以,对于 Android 框架中 View 中的一些属性,databinding 库已经使用 BindingMethods 已经做了属性自动查找匹配,那么当某些属性没有与之对应的 setter 方法时,如何在使用 databinding 时自定义 setter 方法呢,此时就要使用 BindingAdapter 了。

BindingAdapter

属性设置预处理 当某些属性需要自定义处理逻辑的时候可以使用 BindingAdapter,比如我们可以使用 BindingAdapter 重新定义 TextView 的 setText 方法,让输入的英文全部转换为小写,自定义 TextViewAdapter 如下:

代码语言:javascript复制
public class TextViewAdapter {
  @BindingAdapter("android:text")
  public static void setText(TextView view, CharSequence text) {
      //省略特殊处理...
      String txt = text.toString().toLowerCase();
      view.setText(txt);
  }
}

此时,当我们使用 databinding 的优先使用我们自己定义的 BindingAdapter,可能会疑惑为什么能够识别呢,在编译期间 data-binding 编译器会查找带有 @BindingAdapter 注解的方法,最终会将自定义的 setter 方法生成到与之对应的 binding 类中,生成的部分代码如下:

代码语言:javascript复制
@Override
protected void executeBindings() {
    long dirtyFlags = 0;
    synchronized(this) {
        dirtyFlags = mDirtyFlags;
        mDirtyFlags = 0;
    }
 
    // batch finished
    if ((dirtyFlags & 0x2L) != 0) {
        // api target 1
        // 注意:这里是自定义的 TextViewAdapter
        com.manu.databindsample.activity.bindingmethods.TextViewAdapter.setText(this.tvData, "这是TextView");
    }
}

下面以案例的形式验证一下 BindingAdapter 的使用,创建布局文件如下:

代码语言:javascript复制
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data> </data>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
 
        <!--默认TextView-->
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="#a37c7c"
            android:text="这是TextView..."
            android:textSize="16sp" />
 
        <!--使用dataBinding的TextView-->
        <TextView
            android:id="@ id/tvData"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:background="#a37c7c"
            android:text="@{`这是TextView...`}"
            android:textSize="16sp" />
 
    </LinearLayout>
</layout>

使用自定义的 BindingAdapter 效果如下:

可知,自定义的 TextViewAdapter 生效了,可以根据需求很方便对一下数据进行预特殊处理,这也是 BindingAdapter 的作用。

  • 自定义属性设置 自定义属性设置可以定义单个属性也可以定义多个属性,先来定义单个属性,参考如下:
代码语言:javascript复制
public class ImageViewAdapter {
  /**
   * 定义单个属性
   * @param view
   * @param url
   */
  @BindingAdapter("imageUrl")
  public static void setImageUrl(ImageView view, String url) {
      Glide.with(view).load(url).into(view);
  }
}

此时我们可以在布局文件中使用自定义属性 imageUrl 了,使用参考如下:

代码语言:javascript复制
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <data> </data>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:gravity="center_horizontal">
        <!--自定义单个属性-->
        <ImageView
            android:layout_width="100dp"
            android:layout_height="100dp"
            app:imageUrl="@{`https://goss.veer.com/creative/vcg/veer/800water/veer-136599950.jpg`}"/>
    </LinearLayout>
</layout>

上述代码测试效果如下:

这样就可以很方便的使用 imageUrl 属性来加载网络图片了,这里不要担心线程切换问题,databinding 库会自动完成线程切换,那么如何自定义多个属性呢。

下面自定义多个属性,定义方式参考如下:

代码语言:javascript复制
public class ImageViewAdapter {
    /**
     * 定义多个属性
     * @param view
     * @param url
     * @param placeholder
     * @param error
     */
    @BindingAdapter(value = {"imageUrl", "placeholder", "error"})
    public static void loadImage(ImageView view, String url, Drawable placeholder, Drawable error) {
        RequestOptions options = new RequestOptions();
        options.placeholder(placeholder);
        options.error(error);
        Glide.with(view).load(url).apply(options).into(view);
    }
}

此时,可在布局文件中使用上面定义的三个属性了,即 imageUrl、placeholder、error,使用方式参考如下:

代码语言:javascript复制
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <data> </data>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:gravity="center_horizontal">
        <!--自定义多个属性-->
        <ImageView
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:layout_marginTop="10dp"
            app:imageUrl="@{`https://goss.veer.com/creative/vcg/veer/800water/veer-136599950.jpg`}"
            app:placeholder="@{@drawable/icon}"
            app:error="@{@drawable/error}"/>
    </LinearLayout>
</layout>

此时,三个属性全部使用才能 BindingAdapter 才能正常工作,如果使用了其中的一些属性则不能正常编译通过,那么如何在自定义多个属性而正常使用其中的部分属性呢,@BindingAdapter 注解还有一个参数 requireAll ,requireAll 默认为 true,表示必须使用全部属性,将其设置为 false 就可以正常使用部分属性了,此时,自定义多个属性时要配置 注解 @BindAdapter 的 requireAll 属性为 false,参考如下:

代码语言:javascript复制
// requireAll = false
@BindingAdapter(value = {"imageUrl", "placeholder", "error"},requireAll = false)
public static void loadImage(ImageView view, String url, Drawable placeholder, Drawable error) {
    RequestOptions options = new RequestOptions();
    options.placeholder(placeholder);
    options.error(error);
    Glide.with(view).load(url).apply(options).into(view);
}

此时,布局文件就可以使用部分属性了,如下面布局文件只使用 imageUrl 和 placeholder 也不会出现编译错误:

代码语言:javascript复制
<ImageView
    android:layout_width="100dp"
    android:layout_height="100dp"
    android:layout_marginTop="10dp"
    app:imageUrl="@{`https://goss.veer.com/creative/vcg/veer/800water/veer-136599950.jpg`}"
    app:placeholder="@{@drawable/icon}"/>

BindingAdapter 的介绍到此为止。

BindingConversion

在某些情况下,在设置属性时类型之间必须进行转化,此时就可以借助注解 @BindingConversion 来完成类型之间的转换,比如 android:background 属性接收的是一个 Drawable 当我们在 databinding 的表达式中设置了一个颜色值,此时就需要 @BindingConversion,创建布局文件如下:

代码语言:javascript复制
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:app="http://schemas.android.com/apk/res-auto">
    <data> </data>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:gravity="center_horizontal">
        <!--类型转换-->
        <ImageView
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:background="@{true ? @color/colorRed : @color/colorBlue}"/>
    </LinearLayout>
</layout>

使用 @BindingConversion 进行类型转换,参考如下:

代码语言:javascript复制
public class ColorConversion {
    @BindingConversion
    public static ColorDrawable colorToDrawable(int color){
        return new ColorDrawable(color);
    }
}

上述代码测试效果如下:

使用 @BindingConversion 注解时要使用相同类型,如上面的 android:background 属性不能这样使用:

代码语言:javascript复制
<!--类型转换-->
<ImageView
    android:layout_width="100dp"
    android:layout_height="100dp"
    android:background="@{true ? @color/colorRed : @drawable/drawableBlue}"/>

不管是 BindingAdapter 还是 BindingConversion 最终都会将相关代码生成到与之对应的 binding 类中,然后在将其值设置给指定的 View,到此为止,BindingMethods 、BindingAdapter 和 BingingConversion 的相关知识就介绍到这。

0 人点赞