PS:原文首发于微信公众号:躬行之(jzman-blog)
上篇主要是 DataBinding 的基本使用,Android Jetpack 组件系列文章如下 :
本篇文章主要介绍 Binding adapters 的使用方式,内容如下:
Binding adapters 可以作为一个设置某个值的框架来使用,databinding 库可以允许指定具体的方法来进行相关值的设置,在该方法中可以做一些处理逻辑,Binding adapters 会最终给你想要的结果,那么当我们在布局文件中使用 databinding 绑定数据时是如何调用对应的属性方法呢?
<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) 方法如下:
@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);
}
}
这个方法主要用来提示错误信息,一般我们都是在代码中进行使用,这里我们把该方法配置到布局文件中来使用,参考如下:
<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,所以能够在这里以属性的方式进行配置。
这是 databinding 库提供的一个注解,用于当 View 中的某个属性与其对应的 setter 方法名称不对应时进行映射,如 TextView 的属性 android:textColorHint 与之作用相同的方法是 setHintTextColor 方法,此时属性名称与对应的 setter 方法名称不一致,这就需要使用 BindingMethods 注解将该属性与对应的 setter 方法绑定,这样 databinding 就能够按照属性值找到对应的 setter 方法了,databinding 已经处理了原生 View 中的像这种属性与 setter 方法不匹配的情况,来看一看源码中 TextView 中这些不匹配属性的处理,参考如下:
@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 重新定义 TextView 的 setText 方法,让输入的英文全部转换为小写,自定义 TextViewAdapter 如下:
/**
* 自定义BindingAdapters
* Powered by jzman.
* Created on 2018/12/6 0006.
*/
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 类中,生成的部分代码如下:
@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 的使用,创建布局文件如下:
<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
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#a37c7c"
android:text="这是TextView..."
android:textSize="16sp" />
<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 的作用。
自定义属性设置可以定义单个属性也可以定义多个属性,先来定义单个属性,参考如下:
/**
* 自定义BindingAdapters
* Powered by jzman.
* Created on 2018/12/7 0007.
*/
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 了,使用参考如下:
<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 库会自动完成线程切换,那么如何自定义多个属性呢。
下面自定义多个属性,定义方式参考如下:
/**
* 自定义BindingAdapters
* Powered by jzman.
* Created on 2018/12/7 0007.
*/
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,使用方式参考如下:
<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,参考如下:
// 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 也不会出现编译错误:
<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 来完成类型之间的转换,比如 android:background 属性接收的是一个 Drawable 当我们在 databinding 的表达式中设置了一个颜色值,此时就需要 @BindingConversion,创建布局文件如下:
<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 进行类型转换,参考如下:
/**
* 类型转换你
* Powered by jzman.
* Created on 2018/12/7 0007.
*/
public class ColorConversion {
@BindingConversion
public static ColorDrawable colorToDrawable(int color){
return new ColorDrawable(color);
}
}
上述代码测试效果如下:
使用 @BindingConversion 注解时要使用相同类型,如上面的 android:background 属性不能这样使用:
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@{true ? @color/colorRed : @drawable/drawableBlue}"/>
不管是 BindingAdapter 还是 BindingConversion 最终都会将相关代码生成到与之对应的 binding 类中,然后在将其值设置给指定的 View,到此为止,BindingMethods 、BindingAdapter 和 BingingConversion 的相关知识就介绍到这。