繫結配接器

繫結轉接器負責執行適當的架構呼叫以設定值。例如設定屬性值 (例如呼叫 setText() 方法)。另一個範例是設定事件監聽器,例如呼叫 setOnClickListener() 方法。

資料繫結程式庫可讓您指定呼叫的方法,以設定值、提供自己的繫結邏輯,以及使用轉接程式指定傳回的物件類型。

設定屬性值

每當繫結值變更時,產生的繫結類別都必須使用繫結運算式在檢視畫面上呼叫 setter 方法。您可以讓資料繫結程式庫自動決定方法,也可以明確宣告方法,或提供自訂邏輯以選取方法。

自動選取方法

針對名為 example 的屬性,程式庫會自動尋找接受相容類型做為引數的 setExample(arg) 方法。系統不會考慮屬性的命名空間。搜尋方法時,系統只會使用屬性名稱和類型。

例如,假設 android:text="@{user.name}" 運算式,程式庫會尋找 setText(arg) 方法,且該方法接受 user.getName() 傳回的類型。如果 user.getName() 的傳回類型為 String,程式庫會尋找接受 String 引數的 setText() 方法。如果運算式傳回 int,程式庫會搜尋接受 int 引數的 setText() 方法。運算式必須傳回正確的類型。您可以視需要轉換傳回的值。

即使沒有任何屬性符合指定名稱,資料繫結仍會運作。您可以使用資料繫結為任何 setter 建立屬性。舉例來說,支援類別 DrawerLayout 沒有屬性,但具有大量 setter。下列版面配置會自動分別使用 setScrimColor(int)addDrawerListener(DrawerListener) 方法做為 app:scrimColorapp:drawerListener 屬性的 setter:

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

指定自訂方法名稱

部分屬性的 setter 與名稱不符。在這些情況下,您可以使用 BindingMethods 註解,將屬性與 setter 建立關聯。這個註解會與類別搭配使用,可包含多個 BindingMethod 註解,每個重新命名的方法各一個。繫結方法是一種註解,可以加入應用程式的任何類別。

在以下範例中,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 架構類別中重新命名 setter。系統已採用名稱慣例實作屬性,以便自動尋找相符的方法。

提供自訂邏輯

部分屬性需要自訂繫結邏輯。例如,android:paddingLeft 屬性沒有相關聯的 setter。而是改為提供 setPadding(left, top, right, bottom) 方法。帶有 BindingAdapter 註解的靜態繫結轉接器方法可讓您自訂呼叫屬性的 setter 方式。

Android 架構類別的屬性已有 BindingAdapter 註解。以下範例顯示 paddingLeft 屬性的繫結轉接器:

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

參數類型相當重要。第一個參數會決定與屬性相關聯的檢視畫面類型。第二個參數會決定指定屬性的繫結運算式接受的類型。

繫結轉接器也可用於其他類型的自訂。例如,您可以透過工作站執行緒呼叫自訂載入器來載入圖片。

您也可以使用可接收多個屬性的轉接程式,如以下範例所示:

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}" />

如果 ImageView 物件使用 imageUrlerrorimageUrl 是字串,而 errorDrawable,系統就會呼叫轉接程式。如果您想在設定任一屬性時呼叫轉接程式,請將轉接程式的選用 requireAll 標記設為 false,如以下範例所示:

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

繫結轉接器方法可在自己的處理常式中採用舊值。採用新舊值的方法必須先宣告屬性的所有舊值,接著才是新值,如以下範例所示:

Kotlin

@BindingAdapter("android:paddingLeft")
fun setPaddingLeft(view: View, oldPadding: Int, newPadding: Int) {
    if (oldPadding != newPadding) {
        view.setPadding(newPadding,
                    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)。程式庫提供兩個介面,以便區分這些介面的屬性和處理常式:

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() 方法,而非 OnAttachStateChangeListener 的 setter 方法。android.databinding.adapters.ListenerUtil 類別可協助您追蹤這些事件監聽器,以便在繫結轉接器中移除這些事件監聽器。

物件轉換

自動轉換物件

從繫結運算式傳回 Object 時,程式庫會選取用來設定屬性值的方法。Object 會轉換為所選方法的參數類型。如果應用程式使用 ObservableMap 類別儲存資料,這項行為就很方便,如以下範例所示:

<TextView
   android:text='@{userMap["lastName"]}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content" />

運算式中的 userMap 物件會傳回一個值,該值會自動轉換為用於設定 android:text 屬性值的 setText(CharSequence) 方法中找到的參數類型。如果參數類型不明確,請在運算式中轉換傳回類型。

自訂轉換

在某些情況下,您必須進行特定類型的自訂轉換。例如,檢視畫面的 android:background 屬性預期的是 Drawable,但指定的 color 值是整數。以下範例是預期的屬性,其中預期的是 Drawable,但改為提供整數:

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

每當預期有 Drawable 且傳回整數時,請將 int 轉換為 ColorDrawable。如要執行轉換,請使用具有 BindingConversion 註解的靜態方法,如下所示:

Kotlin

@BindingConversion
fun convertColorToDrawable(color: Int) = ColorDrawable(color)

Java

@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
    return new ColorDrawable(color);
}

不過,繫結運算式提供的值類型必須一致。您無法在同一個運算式中使用不同類型,如以下範例所示:

// The @drawable and @color represent different value types in the same
// expression, which causes a build error.
<View
   android:background="@{isError ? @drawable/error : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

其他資源

如要進一步瞭解資料繫結,請參閱下列資源。

範例

程式碼研究室

網誌文章