官方英文原文:https://developer.android.google.cn/topic/libraries/data-binding/binding-adapters
一、介绍
Binding Adapter
负责调用合适的框架调用来设置值,例如通过setText
设置属性值、通过setOnclickListener
设置事件监听器等。
Databinging
类库允许通过调用特定的方法设置值、允许自定义绑定逻辑、允许指定适配器的返回类型。
二、设置属性值
每当一个已经绑定的值发生改变时,生成的绑定类都必须通过View
的数据绑定表达式来设置值。也可以通过Databinding
类库自动决定需要的方法,或者显式声明所需方法、或者提供自定义逻辑的方法。
1. 自动选择方法
假设存在一个属性example
,那么类库会自动去查找可以接受对应参数的setExample(arg)
方法,并不需要考虑属性的命名空间,搜索方法时只会用到属性的名称和类型。
比方说,给定android:text="@{user.name}"
表达式,类库就会查找可以接受user.getName()
返回数据类型的setText(arg)
方法,如果user.getName()
返回的是String
类型,那么就找一个能够接受String
类型参数的setText(arg)
方法
注意:如果返回int
类型,但是又找不到参数为int
类型的setText(arg)
方法,那么就会报错了,因此需要注意参数是否匹配,如果不匹配,就需要进行数据转换。
如果不存在与给定值相匹配的属性,DataBinding
也能工作。可以通过使用数据绑定来创建与set
方法相匹配的属性。比如说,DrawerLayout
没有任何Xml
属性,但是又大量的set
属性的方法。 下面布局将使用setScrimColor(int)
和setDrawerListener(DrawerListener)
方法作为app:scrimColor
和app:drawerListener
属性的的set
方法。
<android.support.v4.widget.DrawerLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:scrimColor="@{@color/scrim}"
app:drawerListener="@{fragment.drawerListener}">
2. 指定自定义的方法名
有些属性存在与之不匹配的set
方法,对于这种情况,一个属性可以通过使用@BindMethods
注解来关联set
方法。@BindMethods
可以看成是一个类,包含多个@BindMethod
注解,并且一个@BindMethod
代表一个重命名的方法,@BindMethods
可以存在于任何一个类中,如下示例,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 framework
类中已经存在的方法,因为这些方法已经实可以通过给定属性来进行匹配了。
3. 提供自定义逻辑
一些属性需要自定义绑定逻辑。比方说android:paddingLeft
并没有与之对应的set
方法,但是有个用于设置该属性的setPadding(left, top, right, bottom)
方法,针对这一问题,使用BindingAdapter
注解则能允许我们自定义这个属性调用set
方法的方式。
Android framework
中类已经为属性建立了BindingAdapter
注解。以下是如何为andriod:paddingLeft
属性建立BindingAdapter
的示例:
// 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());
}
参数类型很重要的。 第一个参数决定与这个属性相绑定的View
类型,第二个参数决定了通过绑定表达式传入的数据参数类型。
BingAdapter
对于其他自定义功能同样很有用,比方说,自定义图片加载器,用于从工作线程中加载图片等。
当自定义的BdingAdapter
在Android framework
中已存在时,那么我们定制的方法会复写掉Android framework
的方法。
BingAdapter
可以接接收多个属性参数,如下示例:
//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}" />
Note: DataBinding
类库在进行匹配时会忽略自定义的命名空间。
当imageUrl
和error
都用于一个ImageView
对象,且imageUrl
是String
类型、error
是Drawable
类型时,这个BindingAdapter
才会被调用。而如果想要让这个适配器在不完整参数属性时也能被调用,那么可以设置requireAll
标记为false
(说白了就是声明不需要全部参数都存在时也能被数据绑定使用,如果true
则需要全部参数都存在),如下示例:
//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);
}
}
BindingAdapter
方法可以选择在它们的处理器中使用旧值。如果一个方法包含旧值和新值,那么所有旧值都必须定义在第一个属性(在新值属性之前),如下示例:
// 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)
两个方法,Databinding
类库就为它们提供了两个区分不同属性的方法,如下:
// 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()
方法来设置和移除监听器的,而不是使用set
方法。android.databinding.adapters.ListenerUtil
类用于追踪上一个监听器,这样就使得旧监听器可以从BindingAdapter
中移除。
通过注解@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
可以然让数据绑定代码生成器知道这些方法需要运行在Android 3.1 (API 12)
以上版本。
三、对象转换 (Object conversions)
1. 自动转换
当一个Object
从绑定表达式中返回时,Databinding
库会选择方法来设置这个属性值,这个Object
最终会被转换成set
方法对应的参数类型。这种行为在使用ObservableMap
类存取数据时会非常方便,如下示例:
<TextView
android:text='@{userMap["lastName"]}'
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
Note: 同样可以使用object.key
方式来引用值,例如上面的@{userMap["lastName"]}
可以替换为@{userMap.lastName}
。
上面userMap
对象在表达就是中返回的值会自动转换为setText(CharSequence)
方法的参数类型,用于设置android:text
属性,如果参数类型很模糊,那么就必须手动转换表达式的返回值。
2. 自定义转换
某些情况下,自定义转换是必要的,例如,View
的android:background
属性需要一个Drawable
类型的数据,而color
属性值是个int
类型,如果我们给background
误传了color
的int
数据,那么就会出问题了,如下示例:
<View
android:background="@{isError ? @color/red : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
每当需要Drawable
类型,但实际却是color
的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"/>
网友评论