知是行之始,行是知之成。
文章配套的 Demo:https://github.com/muyi-yang/DataBindingDemo
Demo 支援 Java 和 Kotlin 雙語言,master 分支為 Java 語言代碼,kotlin 分支為 Kotlin 語言代碼。
綁定擴充卡就是把布局中的屬性表達式轉換成對應的方法調用以設定值。 一個例子是設定屬性值,比如調用
setText()
方法。 或者是設定事件偵聽器,比如調用
setOnClickListener()
方法。還允許你指定設定值的調用方法,提供你自己的綁定邏輯。
設定屬性值
當在布局中使用屬性綁定表達式時,每當綁定的變量值發生更改時,生成的綁定類必須使用綁定表達式調用 View 上的 setter 方法。你可以允許 Data Binding 自動确定方法、顯式聲明方法或提供自定義邏輯來選擇方法。
自動選擇方法
自動選擇方法就是通過屬性名和接受值的類型進行自動嘗試查找接受值相容類型作為參數,屬性名對應的 setter 方法,然後調用此 setter 方法設定接受值。比如一個常見的例子,為 TextView 設定值:
<!--activity_user.xml-->
...
<TextView
android:id="@+id/tv_name"
...
android:text="@{@string/name(user.name), [email protected]/default_name}"
.../>
...
上面有一個
android:text="@{@string/name(user.name), [email protected]/default_name}"
表達式,它接受的值是
String
類型,屬性名是
text
,那麼 Data Binding 架構就會查找接受
String
類型參數的方法
setText(String text)
。如果表達式傳回的是
int
類型,将會查找接受
int
類型參數的方法
setText(int resId)
,如果找不到相應參數和相應屬性對應的方法則會編譯出錯。以下為
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 标準屬性中,這樣的綁定表達式任然有效,Data Binding 允許你為任何 setter 方法建立綁定屬性。比如為
RecyclerView
類它有一個
setOnScrollListener
方法,但沒有
onScrollListener
屬性,我們仍然可以設定一個綁定表達式:
<!--activity_list.xml-->
...
<android.support.v7.widget.RecyclerView
...
app:onScrollListener="@{activity.scrollListener}"
... />
...
它的規則是根據屬性名尋找對應的 setter 方法,然後檢測綁定表達式傳回類型相相容的參數類型的方法作為設定器。
注意:隻要項目中開啟了 Data Binding,所有 View 的所有 setter 方法都将遵循這個規則。可以說隻要有 setter 方法的地方就可以寫綁定表達式。
自定義指定方法名稱
有些 View 屬性具有不按屬性名比對的 setter 方法,在這種情況下你可以使用
@BindingMethods
注解來關聯對應的 setter 方法。注解是寫在一個類上面,它可以包含多個
@BindingMethod
注解,每個注解對應着一個屬性的關聯方法。這些注解可以寫在任何一個類上面,但是不推薦你任意寫,最好做到分門别類,這樣便于後期維護。在下面的示例中,展示了
ImageView
的
android: tint
屬性與
setImageTintList(ColorStateList)
方法相關聯,而不是
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, [email protected]/colorPrimary}"
... />
...
</layout>
大多數情況下,你不需要寫這樣的注解,因為大多數 View 的屬性都有相比對的 setter 方法,它會以自動選擇方法的方式找到。其實在你的項目中連上面的例子提到的
android: tint
屬性你都沒必要寫注解,因為 Data Binding 架構已經幫你預置了很多擴充卡。其中就包括
android: tint
的注解,我這裡寫出來隻是為了一個示範,當你手動寫了之後它會覆寫 Data Binding 預置的。
你可以看看 Data Binding 源碼,其實大部分重要或常用的屬性都已經寫好了各種擴充卡,等待着你的使用。如果你懶得看源碼,你也可以直接在布局中寫你想綁定的屬性,如果編譯出錯則說明沒有預置這個擴充卡,多數情況是可以直接編過的。
提供自定義邏輯
有些屬性需要自定義綁定邏輯。 例如,ImageView 的
android:src
屬性,它沒有相比對的 setter 方法,但它有
setImagexxx
方法。 我們可以使用帶有
@BindingAdapte
注解的靜态綁定擴充卡方法來達到自定義調用 setter 方法。比如下面例子,我想在布局中動态為 ImageView 設定 resId:
public class BindAdapter {
@BindingAdapter("app:image")
public static void bindImage(ImageView view, int resId) {
view.setImageResource(resId);
}
}
這個自定義方法名可以任意取,方法參數類型很重要。 第一個參數确定與該屬性關聯的 View 的類型,也就是說為 ImageView 聲明了一個
app:image
屬性。 第二個參數确定給定屬性的綁定表達式中接受的類型,也就是說
app:image
屬性接受的資料類型是
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>
這樣一個自定義邏輯的綁定方法就寫好了,它的一個好處就是,你可以在方法中自定義任何邏輯,當有一些重複繁瑣的操作時,很合适寫一個自定義邏輯綁定擴充卡。
還可以聲明接受多個屬性的擴充卡。如下面例子所示:
@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 設定了兩個屬性的擴充卡,第一個參數是關聯的 View,第二個參數是第一個屬性的接受值,第三個參數是第二個屬性的接受值。如果你聲明的是兩個屬性以上的擴充卡,參數對應關系以此類推。以下為布局中使用這個擴充卡:
<!--activity_adapter.xml-->
...
<variable name="imgUrl" type="String" />
...
<ImageView
...
app:error="@{@drawable/error}"
abc:image="@{imgUrl}"/>
...
Data Binding 會忽略自定義命名空間進行擴充卡比對,比如上面擴充卡方法中聲明的屬性是,而布局中卻是使用的
app:image
,這是因為 Data Binding 忽略了命名空間,隻取
abc:image
後面的名字進行比對,是以布局中的命名空間可以任意寫。聲明擴充卡方法的屬性時也可以不寫命名空間,比如
:
可以寫成
@BindingAdapter({"app:image", "app:error"})
,它們的效果是相等的,感興趣的同學可以嘗試嘗試。我這裡使用的是
@BindingAdapter({"image", "error"})
這種規範格式,這種格式已過時,在新版本中已推薦不寫命名空間。
app:xxx
上面的聲明的擴充卡方法有一個特點是必須在布局中同時使用這些聲明的屬性,如果少一個就會編譯出錯,提示找不到對應的擴充卡方法。如果你想實作在布局中使用某一個屬性也能正常使用這個擴充卡方法,你可以在擴充卡中增加
requireAll
标志并指派為
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);
}
}
這樣在布局中就無需同時把所有屬性都寫上綁定表達式,你可選擇性的去使用這些屬性,比如隻想加載一張圖檔,不想設定占位圖和錯誤圖:
<ImageView
android:layout_width="match_parent"
android:layout_height="300dp"
android:scaleType="centerCrop"
app:image="@{imgUrl}" />
有些時候,我們在為屬性設定新值時需要擷取到老值來做一些邏輯判斷,這時候你的自定義擴充卡可以這樣做:
@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);
}
}
方法的第一個參數是屬性相關聯的 View,第二個參數是屬性的舊值,第三個參數是屬性的新值。當一個自定義擴充卡隻有一個屬性,但有三個參數,且第二個和第三個參數類型一緻時就會采用這種新舊值的規則。這裡是判斷圖檔的 url 如果沒有變化則不再重新加載,以下為布局中的使用:
<ImageView
...
app:imageUrl="@{switchUrl}"
.../>
在 Demo 中我故意延遲了一段時間進行兩次位址切換,以便體驗擴充卡效果:
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);
}
}
有些監聽器會存在多個回調方法,如果你隻想使用其中某一個回調方法并處理一些事物時,你可以将其拆分為多個自定義監聽器,并封裝成自定義擴充卡進行使用。比如
View.OnAttachStateChangeListener
有兩個回調方法:
onViewAttachedToWindow(View)
和
onViewDetachedFromWindow(View)
,我們将它拆分成兩個自定義監聽器:
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewDetachedFromWindow {
void onViewDetachedFromWindow(View v);
}
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewAttachedToWindow {
void onViewAttachedToWindow(View v);
}
然後建立一個自定義擴充卡将兩個監聽器分别關聯不同的屬性:
@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 提供的一個工具類,它幫助記錄已設定的監聽器,以便需要的時候可以擷取到。比如上面的示例,在設定新監聽器時移除以前的監聽器。
注意:上面示例相關的部分代碼在 Demo 中找不到,這是因為我直接使用了 Data Binding 中已經預制的擴充卡。源碼在
View.OnAttachStateChangeListener
中,學到這裡我覺得帶大家熟悉一下 Data Binding 中的 API 也很有必要,因為熟悉已有的 API 是熟練掌握 Data Binding 的其中一環,因為我們要避免重複造輪子。
android.databinding.adapters.ViewBindingAdapter
對象轉換
自動對象轉換
在布局中寫綁定表達式時,Data Binding 會根據表達式傳回的對象類型自動選擇設定屬性值的 setter 方法。它會自動尋找參數類型與傳回類型相相容的方法,然後把對象類型進行自動轉換。比如以下示例:
<TextView
...
android:text="@{user.task[`monday`]}"
... />
表達式
user.task[monday]
傳回一個 String 類型,它會自動轉換為
setText(CharSequence)
方法中的參數類型,如果表達式傳回的參數類型不明确,你可能需要在表達式中進行強制轉換,比如這樣
android:text="@{(CharSequence)user.task[monday]}"
。
自定義轉換
有些時候我們需要在特定類型中進行自定義轉換,比如一個 View 的顯示和隐藏需求,往往資料類型是
Boolean
,但是
android:visibility
屬性需要的是一個
int
常量。比如:
<!--activity_adapter.xml-->
...
<variable name="isShow" type="boolean" />
...
<ImageView
...
android:visibility="@{isShow}"
... />
上面
android:visibility
屬性中綁定的是一個
Boolean
類型,但是它需要的是
int
型,當出現這個中情況時 Data Binding 會嘗試尋找轉換器,當尋找不到時會編譯出錯。轉換器可以使用帶有
@BindingConversion
注解的靜态方法實作,比如:
@BindingConversion
public static int convertBooleanToVisible(boolean visible) {
return visible ? View.VISIBLE : View.GONE;
}
方法的參數是
Boolean
類型,傳回值卻是
int
類型,這樣就實作了從
Boolean
轉換
int
了。
但是,要特别注意一點的是,轉換器是全局的,它适用于整個項目,是以要謹慎使用,以防誤寫而不自知。以下為一個反面例子:
<ImageView
...
android:padding="@{isShow}"
... />
此處為
android:padding
屬性誤綁定了一個
Boolean
資料,本應該因為期望的資料類型不一緻而編譯出錯,但是因為自定義了一個
Boolean
轉換
int
類型的轉換器而變得合法,導緻編譯器認為是正常情況,進而導緻 UI 顯示異常。
此篇到這裡就結束了,可以檢視下一篇 Data Binding 詳解(六)-雙向資料綁定。
如果你覺得文章有幫助到你,記得點個喜歡以表支援,同時歡迎你的指正和建議。十分感謝!