Data Binding 詳解(五)-綁定適配器

知是行之始,行是知之成。
文章配套的 Demohttps://github.com/muyi-yang/DataBindingDemo
Demo 支持 Java 和 Kotlin 雙語言,master 分支為 Java 語言代碼,kotlin 分支為 Kotlin 語言代碼。

綁定適配器就是把布局中的屬性表達(dá)式轉(zhuǎn)換成對應(yīng)的方法調(diào)用設(shè)置值。 一個例子是設(shè)置屬性值,比如調(diào)用 setText() 方法。 或者是設(shè)置事件偵聽器,比如調(diào)用 setOnClickListener() 方法。還允許你指定設(shè)置值的調(diào)用方法,提供你自己的綁定邏輯。

設(shè)置屬性值

當(dāng)在布局中使用屬性綁定表達(dá)式時,每當(dāng)綁定的變量值發(fā)生更改時,生成的綁定類必須使用綁定表達(dá)式調(diào)用 View 上的 setter 方法。你可以允許 Data Binding 自動確定方法、顯式聲明方法或提供自定義邏輯來選擇方法。

自動選擇方法

自動選擇方法就是通過屬性名接受值的類型進(jìn)行自動嘗試查找接受值兼容類型作為參數(shù),屬性名對應(yīng)的 setter 方法,然后調(diào)用此 setter 方法設(shè)置接受值。比如一個常見的例子,為 TextView 設(shè)置值:

<!--activity_user.xml-->
    ...
       <TextView
            android:id="@+id/tv_name"
            ...
            android:text="@{@string/name(user.name), default=@string/default_name}"
            .../>
    ...

上面有一個 android:text="@{@string/name(user.name), default=@string/default_name}" 表達(dá)式,它接受的值是 String 類型,屬性名是 text,那么 Data Binding 框架就會查找接受 String 類型參數(shù)的方法 setText(String text)。如果表達(dá)式返回的是 int 類型,將會查找接受 int 類型參數(shù)的方法 setText(int resId),如果找不到相應(yīng)參數(shù)和相應(yīng)屬性對應(yīng)的方法則會編譯出錯。以下為 int 類型的 setText 示例:

<!--activity_user.xml-->
    ...
    <variable
            name="stringResId"
            type="int" />
    ...
    <TextView
            ...
            android:text="@{stringResId}"
            .../>
    ...
public class UserActivity extends AppCompatActivity {
    ...
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = DataBindingUtil.setContentView(this, R.layout.activity_user);
        ...
        binding.setStringResId(R.string.app_name);
    }
}

有些時候綁定的屬性名不在 View 標(biāo)準(zhǔn)屬性中,這樣的綁定表達(dá)式任然有效,Data Binding 允許你為任何 setter 方法創(chuàng)建綁定屬性。比如為 RecyclerView 類它有一個 setOnScrollListener 方法,但沒有 onScrollListener 屬性,我們?nèi)稳豢梢栽O(shè)置一個綁定表達(dá)式:

    <!--activity_list.xml-->
    ...
    <android.support.v7.widget.RecyclerView
            ...
            app:onScrollListener="@{activity.scrollListener}"
            ... />
    ...

它的規(guī)則是根據(jù)屬性名尋找對應(yīng)的 setter 方法,然后檢測綁定表達(dá)式返回類型相兼容的參數(shù)類型的方法作為設(shè)置器。

注意:只要項目中開啟了 Data Binding,所有 View 的所有 setter 方法都將遵循這個規(guī)則。可以說只要有 setter 方法的地方就可以寫綁定表達(dá)式。

自定義指定方法名稱

有些 View 屬性具有不按屬性名匹配的 setter 方法,在這種情況下你可以使用 @BindingMethods 注解來關(guān)聯(lián)對應(yīng)的 setter 方法。注解是寫在一個類上面,它可以包含多個 @BindingMethod 注解,每個注解對應(yīng)著一個屬性的關(guān)聯(lián)方法。這些注解可以寫在任何一個類上面,但是不推薦你任意寫,最好做到分門別類,這樣便于后期維護(hù)。在下面的示例中,展示了 ImageViewandroid: tint 屬性與 setImageTintList(ColorStateList) 方法相關(guān)聯(lián),而不是 setTint() 方法:

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

在布局中的使用:

<!--activity_adapter.xml-->
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        <variable
            name="tintColor"
            type="android.content.res.ColorStateList" />
    </data>
    ...
        <ImageView
            ...
            android:tint="@{tintColor, default=@color/colorPrimary}"
            ... />
    ...
</layout>

大多數(shù)情況下,你不需要寫這樣的注解,因為大多數(shù) View 的屬性都有相匹配的 setter 方法,它會以自動選擇方法的方式找到。其實在你的項目中連上面的例子提到的 android: tint 屬性你都沒必要寫注解,因為 Data Binding 框架已經(jīng)幫你預(yù)置了很多適配器。其中就包括 android: tint 的注解,我這里寫出來只是為了一個演示,當(dāng)你手動寫了之后它會覆蓋 Data Binding 預(yù)置的。

你可以看看 Data Binding 源碼,其實大部分重要或常用的屬性都已經(jīng)寫好了各種適配器,等待著你的使用。如果你懶得看源碼,你也可以直接在布局中寫你想綁定的屬性,如果編譯出錯則說明沒有預(yù)置這個適配器,多數(shù)情況是可以直接編過的。

提供自定義邏輯

有些屬性需要自定義綁定邏輯。 例如,ImageView 的 android:src 屬性,它沒有相匹配的 setter 方法,但它有 setImagexxx 方法。 我們可以使用帶有 @BindingAdapte 注解的靜態(tài)綁定適配器方法來達(dá)到自定義調(diào)用 setter 方法。比如下面例子,我想在布局中動態(tài)為 ImageView 設(shè)置 resId:

public class BindAdapter {
    @BindingAdapter("app:image")
    public static void bindImage(ImageView view, int resId) {
        view.setImageResource(resId);
    }
}

這個自定義方法名可以任意取,方法參數(shù)類型很重要。 第一個參數(shù)確定與該屬性關(guān)聯(lián)的 View 的類型,也就是說為 ImageView 聲明了一個 app:image 屬性。 第二個參數(shù)確定給定屬性的綁定表達(dá)式中接受的類型,也就是說 app:image 屬性接受的數(shù)據(jù)類型是 int 型。以下為布局中的使用:

<!--layout_avatar.xml-->
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        <variable name="resId" type="int" />
    </data>
    <ImageView
        ...
        app:image="@{resId}"
        ... />
</layout>

這樣一個自定義邏輯的綁定方法就寫好了,它的一個好處就是,你可以在方法中自定義任何邏輯,當(dāng)有一些重復(fù)繁瑣的操作時,很合適寫一個自定義邏輯綁定適配器。


還可以聲明接受多個屬性的適配器。如下面例子所示:

    @BindingAdapter({"app:image", "app:error"})
    public static void loadImage(ImageView view, String url, Drawable error) {
        RequestOptions options = new RequestOptions().error(error);
        Glide.with(view).load(url).apply(options).into(view);
    }

這是同時為一個 View 設(shè)置了兩個屬性的適配器,第一個參數(shù)是關(guān)聯(lián)的 View,第二個參數(shù)是第一個屬性的接受值,第三個參數(shù)是第二個屬性的接受值。如果你聲明的是兩個屬性以上的適配器,參數(shù)對應(yīng)關(guān)系以此類推。以下為布局中使用這個適配器:

    <!--activity_adapter.xml-->
    ...
    <variable  name="imgUrl" type="String" />
    ...
    <ImageView
            ...
            app:error="@{@drawable/error}"
            abc:image="@{imgUrl}"/>
    ...

Data Binding 會忽略自定義命名空間進(jìn)行適配器匹配,比如上面適配器方法中聲明的屬性是 app:image,而布局中卻是使用的 abc:image,這是因為 Data Binding 忽略了命名空間,只取 : 后面的名字進(jìn)行匹配,所以布局中的命名空間可以任意寫。聲明適配器方法的屬性時也可以不寫命名空間,比如 @BindingAdapter({"app:image", "app:error"}) 可以寫成 @BindingAdapter({"image", "error"}),它們的效果是相等的,感興趣的同學(xué)可以嘗試嘗試。我這里使用的是 app:xxx 這種規(guī)范格式,這種格式已過時,在新版本中已推薦不寫命名空間。

上面的聲明的適配器方法有一個特點是必須在布局中同時使用這些聲明的屬性,如果少一個就會編譯出錯,提示找不到對應(yīng)的適配器方法。如果你想實現(xiàn)在布局中使用某一個屬性也能正常使用這個適配器方法,你可以在適配器中增加 requireAll 標(biāo)志并賦值為 false,比如:

    @BindingAdapter(value = {"image", "app:placeholder", "app:error"}, requireAll = false)
    public static void loadImage(ImageView view, String url, Drawable placeholder, Drawable error) {
        if (TextUtils.isEmpty(url)) {
            view.setImageDrawable(placeholder);
        } else {
            RequestOptions options = new RequestOptions().placeholder(placeholder).error(error);
            Glide.with(view).load(url).apply(options).into(view);
        }
    }

這樣在布局中就無需同時把所有屬性都寫上綁定表達(dá)式,你可選擇性的去使用這些屬性,比如只想加載一張圖片,不想設(shè)置占位圖和錯誤圖:

    <ImageView
            android:layout_width="match_parent"
            android:layout_height="300dp"
            android:scaleType="centerCrop"
            app:image="@{imgUrl}" />

有些時候,我們在為屬性設(shè)置新值時需要獲取到老值來做一些邏輯判斷,這時候你的自定義適配器可以這樣做:

    @BindingAdapter("app:imageUrl")
    public static void bindImage(ImageView view, String oldUrl, String newUrl) {
        if (oldUrl == null || !oldUrl.equals(newUrl)) {
            Glide.with(view).load(newUrl).into(view);
        }
    }

方法的第一個參數(shù)是屬性相關(guān)聯(lián)的 View,第二個參數(shù)是屬性的舊值,第三個參數(shù)是屬性的新值。當(dāng)一個自定義適配器只有一個屬性,但有三個參數(shù),且第二個和第三個參數(shù)類型一致時就會采用這種新舊值的規(guī)則。這里是判斷圖片的 url 如果沒有變化則不再重新加載,以下為布局中的使用:

    <ImageView
            ...
            app:imageUrl="@{switchUrl}"
            .../>

在 Demo 中我故意延遲了一段時間進(jìn)行兩次地址切換,以便體驗適配器效果:

public class AdapterActivity extends AppCompatActivity {
    ...
    private Handler handler = new Handler();
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = DataBindingUtil.setContentView(this, R.layout.activity_adapter);
        ...
        binding.setSwitchUrl("https://s2.ax1x.com/2019/03/03/kLWJ3D.jpg");
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                binding.setSwitchUrl("https://s2.ax1x.com/2019/03/03/kLOdSA.jpg");
            }
        }, 2000);
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                binding.setSwitchUrl("https://s2.ax1x.com/2019/03/03/kLOdSA.jpg");
            }
        }, 4000);
    }
}

有些監(jiān)聽器會存在多個回調(diào)方法,如果你只想使用其中某一個回調(diào)方法并處理一些事物時,你可以將其拆分為多個自定義監(jiān)聽器,并封裝成自定義適配器進(jìn)行使用。比如 View.OnAttachStateChangeListener 有兩個回調(diào)方法: onViewAttachedToWindow(View)onViewDetachedFromWindow(View),我們將它拆分成兩個自定義監(jiān)聽器:

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

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

然后創(chuàng)建一個自定義適配器將兩個監(jiān)聽器分別關(guān)聯(lián)不同的屬性:

    @BindingAdapter(value = {"android:onViewDetachedFromWindow", "android:onViewAttachedToWindow"}, requireAll = false)
    public static void setOnAttachStateChangeListener(View view,
            final OnViewDetachedFromWindow detach, final OnViewAttachedToWindow attach) {
        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);
        }
    }

最后在布局中使用它:

    <!--activity_adapter.xml-->
    <ImageView
            ...
            android:onViewAttachedToWindow="@{attachListener}"
            android:onViewDetachedFromWindow="@{detachListener}"
            ... />

上面的例子中,使用到 ListenerUtil 類,它是 Data Binding 提供的一個工具類,它幫助記錄已設(shè)置的監(jiān)聽器,以便需要的時候可以獲取到。比如上面的示例,在設(shè)置新監(jiān)聽器時移除以前的監(jiān)聽器。

注意:上面示例 View.OnAttachStateChangeListener 相關(guān)的部分代碼在 Demo 中找不到,這是因為我直接使用了 Data Binding 中已經(jīng)預(yù)制的適配器。源碼在 android.databinding.adapters.ViewBindingAdapter 中,學(xué)到這里我覺得帶大家熟悉一下 Data Binding 中的 API 也很有必要,因為熟悉已有的 API 是熟練掌握 Data Binding 的其中一環(huán),因為我們要避免重復(fù)造輪子。

對象轉(zhuǎn)換

自動對象轉(zhuǎn)換

在布局中寫綁定表達(dá)式時,Data Binding 會根據(jù)表達(dá)式返回的對象類型自動選擇設(shè)置屬性值的 setter 方法。它會自動尋找參數(shù)類型與返回類型相兼容的方法,然后把對象類型進(jìn)行自動轉(zhuǎn)換。比如以下示例:

    <TextView
            ...
            android:text="@{user.task[`monday`]}"
            ... />

表達(dá)式 user.task[monday] 返回一個 String 類型,它會自動轉(zhuǎn)換為 setText(CharSequence) 方法中的參數(shù)類型,如果表達(dá)式返回的參數(shù)類型不明確,你可能需要在表達(dá)式中進(jìn)行強制轉(zhuǎn)換,比如這樣 android:text="@{(CharSequence)user.task[monday]}"。

自定義轉(zhuǎn)換

有些時候我們需要在特定類型中進(jìn)行自定義轉(zhuǎn)換,比如一個 View 的顯示和隱藏需求,往往數(shù)據(jù)類型是 Boolean,但是 android:visibility 屬性需要的是一個 int 常量。比如:

        <!--activity_adapter.xml-->
        ...
        <variable name="isShow" type="boolean" />
        ...
        <ImageView
            ...
            android:visibility="@{isShow}"
            ... />

上面 android:visibility 屬性中綁定的是一個 Boolean 類型,但是它需要的是 int 型,當(dāng)出現(xiàn)這個中情況時 Data Binding 會嘗試尋找轉(zhuǎn)換器,當(dāng)尋找不到時會編譯出錯。轉(zhuǎn)換器可以使用帶有 @BindingConversion 注解的靜態(tài)方法實現(xiàn),比如:

    @BindingConversion
    public static int convertBooleanToVisible(boolean visible) {
        return visible ? View.VISIBLE : View.GONE;
    }

方法的參數(shù)是 Boolean 類型,返回值卻是 int 類型,這樣就實現(xiàn)了從 Boolean 轉(zhuǎn)換 int 了。
但是,要特別注意一點的是,轉(zhuǎn)換器是全局的,它適用于整個項目,所以要謹(jǐn)慎使用,以防誤寫而不自知。以下為一個反面例子

        <ImageView
            ...
            android:padding="@{isShow}"
            ... />

此處為 android:padding 屬性誤綁定了一個 Boolean 數(shù)據(jù),本應(yīng)該因為期望的數(shù)據(jù)類型不一致而編譯出錯,但是因為自定義了一個 Boolean 轉(zhuǎn)換 int 類型的轉(zhuǎn)換器而變得合法,導(dǎo)致編譯器認(rèn)為是正常情況,從而導(dǎo)致 UI 顯示異常。

此篇到這里就結(jié)束了,可以查看下一篇 Data Binding 詳解(六)-雙向數(shù)據(jù)綁定。

如果你覺得文章有幫助到你,記得點個喜歡以表支持,同時歡迎你的指正和建議。十分感謝!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容