6. 属性 Setter

当绑定数据发生变动时,生成的 binding 类必须根据 binding 表达式调用 View 的 setter 函数来修改 View 的属性。Data binding 框架内置了几种自定义赋值给 view 的方法。

6.1. 自动 Setter

对一个 attribute 来说,data binding 会自动尝试寻找对应的 setAttribute 函数。属性的命名空间不会对这个过程产生影响,只有属性的命名才是决定因素。

举个例子,针对一个与 TextView 的 android:text 绑定的表达式,data binding会自动寻找 setText(String) 函数。如果表达式返回值为 int 类型, data binding则会寻找 setText(int) 函数。所以需要小心处理函数的返回值类型,必要的时候使用强制类型转换。需要注意的是,data binding 在对应名称的属性不存在的时候也能继续工作。你可以轻而易举地使用 data binding 为任何 setter “创建” 属性。举个例子,support 库中的 DrawerLayout 并没有任何属性,但是有很多 setter,所以你可以使用自动 setter 的特性来调用这些函数。

<android.support.v4.widget.DrawerLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:scrimColor="@{@color/scrim}"
    app:drawerListener="@{fragment.drawerListener}"/>

6.2. 重命名 Setter

一些属性的命名与 setter 不对应。针对这些函数,可以用 BindingMethods 注解来将属性与 setter 绑定在一起。例如,android:tint 属性可以像下面这样与 setImageTintList(ColorStateList)) 绑定,而不是 setTint,这个注解标注在任何类上面都有效果:

@BindingMethods({
       @BindingMethod(type = "android.widget.ImageView",
                      attribute = "android:tint",
                      method = "setImageTintList"),
})

Android 框架中的 setter 重命名已经在库中实现了,开发者只需要专注于自己的 setter。比如:

@BindingMethods({
        @BindingMethod(type = ZoomControls.class, attribute = "android:onZoomIn", method = "setOnZoomInClickListener"),
        @BindingMethod(type = ZoomControls.class, attribute = "android:onZoomOut", method = "setOnZoomOutClickListener"),
})
public class ZoomControlsBindingAdapter {
}

6.3. 自定义 Setter

6.3.1 一些属性需要自定义 setter 逻辑。例如,目前没有与 android:paddingLeft 对应的 setter,只有一个 setPadding(left, top, right, bottom) 函数。使用BindingAdapter注解的标记静态方法,允许开发人员自定义属性对应的 setter 方法。

Android 属性已经内置一些 BindingAdapter。例如,这是一个 paddingLeft 的自定义 setter:

@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
   view.setPadding(padding,
                   view.getPaddingTop(),
                   view.getPaddingRight(),
                   view.getPaddingBottom());
}

BindingAdapter 在其他自定义类型上也很好用。比如,一个 loader 可以在非主线程加载图片。

当存在冲突时,开发者创建的 binding adapter 方法会覆盖 data binding 的默认 adapter 方法。

6.3.2 你也可以创建有多个参数的 Adapter 方法:

@BindingAdapter({"app:imageUrl", "app:error"})
public static void loadImage(ImageView view, String url, Drawable error) {
   Picasso.with(view.getContext()).load(url).error(error).into(view);
}
<ImageView
    app:imageUrl=“@{venue.imageUrl}”
    app:error=“@{@drawable/venueError}”/>

当 imageUrl 与 error 属性都存在时,并且 imageUrl 是一个 String,error 是一个 Drawable,这个 adapter 会被调用。

属性与自定义 setter 在匹配时,自定义命名空间会被忽略 也可以为 android 命名空间编写 adapter 6.3.3 BindingAdapter 注解标记的 setter 方法可以获取属性旧的赋值。要使用旧值,只需要将旧值放置在前,新值放置在后:

@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());
   }
}

6.3.4 事件处理程序只能与一个抽象方法的接口或抽象类一起使用。例如:

@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);
        }
    }
}

6.3.5 当 listener 内置多个函数时,必须分割成多个 listener 和对应的多个属性。例如,View.OnAttachStateChangeListener 内置两个函数:onViewAttachedToWindow()) 与 onViewDetachedFromWindow())。在这里必须为两个不同的属性创建不同的接口。

@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewDetachedFromWindow {
    void onViewDetachedFromWindow(View v);
}

@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewAttachedToWindow {
    void onViewAttachedToWindow(View v);
}

因为改变一个 listener 会影响到另外一个,我们必须编写三个不同的 adapter,包括修改一个属性的,和修改两个属性的。

@BindingAdapter("android:onViewAttachedToWindow")
public static void setListener(View view, OnViewAttachedToWindow attached) {
    setListener(view, null, attached);
}

@BindingAdapter("android:onViewDetachedFromWindow")
public static void setListener(View view, OnViewDetachedFromWindow detached) {
    setListener(view, detached, null);
}

@BindingAdapter({"android:onViewDetachedFromWindow", "android:onViewAttachedToWindow"})
public static void setListener(View view, final OnViewDetachedFromWindow detach,
        final OnViewAttachedToWindow attach) {
    if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1) {
        final 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);
                    }
                }
            };
        }
        final OnAttachStateChangeListener oldListener = ListenerUtil.trackListener(view,
                newListener, R.id.onAttachStateChangeListener);
        if (oldListener != null) {
            view.removeOnAttachStateChangeListener(oldListener);
        }
        if (newListener != null) {
            view.addOnAttachStateChangeListener(newListener);
        }
    }
}

上面的例子比普通情况下复杂,因为 View 是 add/remove View.OnAttachStateChangeListener 而不是 set。可以使用 android.databinding.adapters.ListenerUtil 来辅助跟踪旧的 listener 并移除它。

对于 addOnAttachStateChangeListener(View.OnAttachStateChangeListener)) 支持的 api 版本,通过向 OnViewDetachedFromWindow 和 OnViewAttachedToWindow 添加 @TargetApi(VERSION_CODES.HONEYCHOMB_MR1) 注解,data binding 代码生成器会知道这些 listener 只会在 Honeycomb MR1 或更新的设备上使用。

通过自定义 setter 可以通过在 xml 中的表达式唤起任意 view 中的方法,并且在调用 view 方法之前添加我们自己的 view 控制逻辑。

比如用于给 TextView 添加 xml 字体设置属性:

<TextView app:font="@{`Source-Sans-Pro-Regular.ttf`}"/>
public class AppAdapters {
  @BindingAdapter({"font"})
  public static void setFont(TextView textView, String fontName){
    AssetManager assetManager = textView.getContext().getAssets();
    String path = "fonts/" + fontName;
    Typeface typeface = sCache.get(path);
    if (typeface == null) {
      typeface = Typeface.createFromAsset(assetManager, path);
      sCache.put(path, typeface);
    }
    textView.setTypeface(typeface);
  }
}

results matching ""

    No results matching ""