美文网首页
数据绑定适配器(Binding Adapters)[ 翻译]

数据绑定适配器(Binding Adapters)[ 翻译]

作者: horseLai | 来源:发表于2018-10-27 22:46 被阅读0次

官方英文原文: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:scrimColorapp: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:tintsetImageTintList(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对于其他自定义功能同样很有用,比方说,自定义图片加载器,用于从工作线程中加载图片等。

当自定义的BdingAdapterAndroid 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类库在进行匹配时会忽略自定义的命名空间。

imageUrlerror都用于一个ImageView对象,且imageUrlString类型、errorDrawable类型时,这个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);
}

因为改变一个监听器时会影响到其他的监听器,所以需要让是适配器工作于单个属性或者全部属性这两种情况的一种。而对于这种情况,同样可以在注解中设置requireAllfalse来声明不需要所有属性参数都齐全也能使用,如下示例:

// 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. 自定义转换

某些情况下,自定义转换是必要的,例如,Viewandroid:background属性需要一个Drawable类型的数据,而color属性值是个int类型,如果我们给background误传了colorint数据,那么就会出问题了,如下示例:

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

每当需要Drawable类型,但实际却是colorint时,那么需要转换成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"/>

相关文章

网友评论

      本文标题:数据绑定适配器(Binding Adapters)[ 翻译]

      本文链接:https://www.haomeiwen.com/subject/qrujtqtx.html