(譯)Data Binding 指南
來自: http://yanghui.name/blog/2016/02/17/data-binding-guide/
翻譯自 http://developer.android.com/intl/zh-cn/tools/data-binding/guide.html
這個文檔用于解釋如何使用 Data Binding Library 編寫聲明式的布局,減少應用中邏輯以及布局所需要的“膠水代碼”。
Data Binding Library 提供了靈活性與通用性 - 它是一個 support library,可以在 Android 2.1(API level 7+)以上的平臺使用。
為了使用 data binding,gradle plugin的版本必須是 1.5.0-alpha1以上。
編譯環境
為了使用 Data Binding,首先在 Android SDK manager 中下載最新的 Support repository。
然后在 build.gradle 中添加 dataBinding 段。
使用以下代碼段配置 data binding:
android { .... dataBinding { enabled = true } }
如果你的 app module 依賴了一個使用 data binding 的庫,那么你的 app module 的 build.gradle 也必須配置 data binding。
同時,確定使用了支持該特性的 Android Studio。在 Android Studio 1.3 以及之后的版本提供了 data binding 的支持,詳見 Android Studio Support for Data Binding 。
Data Binding 布局文件
編寫你的第一個 data binding 表達式
Data binding 布局文件與普通布局文件有一點不同。它以一個 layout 標簽作為根節點,里面是 data 標簽與 view 標簽。view 標簽的內容就是不使用 data binding 時的普通布局文件內容。以下是一個例子:
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <variable name="user" type="com.example.User"/> </data> <LinearLayout android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user.firstName}"/> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user.lastName}"/> </LinearLayout> </layout>
在 data 標簽中的 user 變量 描述了一個布局中會用到的屬性。
<variable name="user" type="com.example.User"/>
布局文件中的表達式使用 “@{}” 的語法。在這里,TextView 的文本被設置為 user中的 firstName 屬性。
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user.firstName}"/>
數據對象
假設你有一個 plain-old Java object(POJO) 的 User 對象。
public class User { public final String firstName; public final String lastName; public User(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } }
這種類型的對象擁有不可改變的數據。在應用中,讀一次且不變動數據的對象非常常見。也可以使用 JavaBeans 對象:
public class User { private final String firstName; private final String lastName; public User(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } public String getFirstName() { return this.firstName; } public String getLastName() { return this.lastName; } }
從 data binding 的角度看,這兩個類是一樣的。用于 TextView 的 android:text 屬性的表達式 @{user.firstName} ,會讀取 POJO 對象的 firstName 域以及 JavaBeans 對象的 getFirstName() 方法。此外,如果 firstName() 方法存在的話也同樣可用。
綁定數據
在默認情況下,會基于布局文件生成一個 Binding 類,將它轉換成帕斯卡命名并在名字后面接上”Binding”。上面的那個布局文件叫 main_activity.xml ,所以會生成一個 MainActivityBinding 類。這個類包含了布局文件中所有的綁定關系( user 變量),會根據綁定表達式給布局文件賦值。在 inflate 的時候創建 binding 的方法如下:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity); User user = new User("Test", "User"); binding.setUser(user); }
就這么簡單!運行應用,你會發現測試用戶已經顯示在界面中了。你也可以通過以下這種方式:
MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater());
如果你在 ListView 或者 RecyclerView 的 adapter 中使用 data binding,你可以這樣寫:
ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false); //or ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);
綁定事件
事件可以直接與 handler 函數綁定,類似于 android:onClick 可以指定 Activity 中的一個函數一樣。事件屬性的命名由 listener 的函數命名決定。舉個例子, View.OnLongClickListener 中有一個 onLongClick() 函數,所以這個事件的對應屬性就是 android:onLongClick 。
為了將事件分配給 handler,只需要使用一個 binding 表達式,值為要調用的函數名。舉個例子,如果你的數據對象有兩個函數:
public class MyHandlers { public void onClickFriend(View view) { ... } public void onClickEnemy(View view) { ... } }
分配點擊事件的 binding 表達式如下:
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <variable name="handlers" type="com.example.Handlers"/> <variable name="user" type="com.example.User"/> </data> <LinearLayout android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user.firstName}" android:onClick="@{user.isFriend ? handlers.onClickFriend : handlers.onClickEnemy}"/> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user.lastName}" android:onClick="@{user.isFriend ? handlers.onClickFriend : handlers.onClickEnemy}"/> </LinearLayout> </layout>
也有一個特殊的點擊事件 handler,他們有一些不同于 android:onClick 的屬性來避免沖突。下面是一些用來避免沖突的屬性:
Class | Listener Setter | Attribute |
---|---|---|
SearchView | setOnSearchClickListener(View.OnClickListener) ) | android:onSearchClick |
ZoomControls | setOnZoomInClickListener(View.OnClickListener) ) | android:onZoomIn |
ZoomControls | setOnZoomOutClickListener(View.OnClickListener) ) | android:onZoomOut |
布局細節
導入
data 標簽內可以有多個 import 標簽。你可以在布局文件中像使用 Java 一樣導入引用。
<data> <import type="android.view.View"/> </data>
現在 View 可以被這樣引用:
<TextView android:text="@{user.lastName}" android:layout_width="wrap_content" android:layout_height="wrap_content" android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
當類名發生沖突時,可以使用 alias:
<import type="android.view.View"/> <import type="com.example.real.estate.View" alias="Vista"/>
現在, Vista 可以用來引用 com.example.real.estate.View ,與 View 在布局文件中同時使用。導入的類型也可以用于變量的類型引用和表達式中:
<data> <import type="com.example.User"/> <import type="java.util.List"/> <variable name="user" type="User"/> <variable name="userList" type="List<User>"/> </data>
注意:Android Studio 還沒有對導入提供自動補全的支持。你的應用還是可以被正常編譯,要解決這個問題,你可以在變量定義中使用完整的包名。
</div>
<TextView android:text="@{((User)(user.connection)).lastName}" android:layout_width="wrap_content" android:layout_height="wrap_content"/>
導入也可以用于在表達式中使用靜態域/方法:
<data> <import type="com.example.MyStringUtils"/> <variable name="user" type="com.example.User"/> </data> … <TextView android:text="@{MyStringUtils.capitalize(user.lastName)}" android:layout_width="wrap_content" android:layout_height="wrap_content"/>
和 Java 一樣, java.lang.* 會被自動導入。
變量
data 標簽中可以有任意數量的 variable 標簽。每個 variable 標簽描述了會在 binding 表達式中使用的屬性。
<data> <import type="android.graphics.drawable.Drawable"/> <variable name="user" type="com.example.User"/> <variable name="image" type="Drawable"/> <variable name="note" type="String"/> </data>
變量類型會在編譯時被檢查,所以如果變量聲明了 Observable 接口或者是一個 可觀察容器類 ,那它會被反射使用。如果變量是一個沒有聲明 Observable* 接口的基類或借口,變量的變動則不會引起 UI 的變化!
當針對不同配置編寫不同的布局文件時(比如橫屏豎屏的布局),變量會被合并。所以這些不同配置的布局文件之間不能存在沖突。
自動生成的 binding 類會為每一個變量生產 getter/setter 函數。這些變量會使用 Java 的默認賦值,直到 setter 函數被調用。默認賦值有 null , 0 ( int ), false ( boolean )等。
binding 類也會生一個一個命名為 context 的特殊變量,這個變量被用于表達式中。 context 變量其實就是 rootView 的 getContext() ) 的返回值。 context 變量會被同名的顯式變量覆蓋。
自定義 Binding 類名
默認情況下,binding 類的名稱取決于布局文件的命名,以大寫字母開頭,移除下劃線,后續字母大寫并追加 “Binding” 結尾。這個類會被放置在 databinding 包中。舉個例子,布局文件 contact_item.xml 會生成 ContactItemBinding 類。如果 module 包名為 com.example.my.app ,binding 類會被放在 com.example.my.app.databinding 中。
通過修改 data 標簽中的 class 屬性,可以修改 Binding 類的命名與位置。舉個例子:
<data class="ContactItem"> ... </data>
以上會在 databinding 包中生成名為 ContactItem 的binding 類。如果需要放置在不同的包下,可以在前面加 “.”:
<data class=".ContactItem"> ... </data>
這樣的話, ContactItem 會直接生成在 module 包下。如果提供完整的包名,binding 類可以放置在任何包名中:
<data class="com.example.ContactItem"> ... </data>
Includes
在使用應用命名空間的布局中,變量可以傳遞到任何 include 布局中。
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:bind="http://schemas.android.com/apk/res-auto"> <data> <variable name="user" type="com.example.User"/> </data> <LinearLayout android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <include layout="@layout/name" bind:user="@{user}"/> <include layout="@layout/contact" bind:user="@{user}"/> </LinearLayout> </layout>
需要注意, name.xml 與 contact.xml 中都需要聲明 user 變量。
Data binding 不支持直接包含 merge 節點。舉個例子, 以下的代碼就不能正常運行 :
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:bind="http://schemas.android.com/apk/res-auto"> <data> <variable name="user" type="com.example.User"/> </data> <merge> <include layout="@layout/name" bind:user="@{user}"/> <include layout="@layout/contact" bind:user="@{user}"/> </merge> </layout>
表達式語言
通用特性
表達式語言與 Java 表達式有很多相似之處。下面是相同之處:
- 數學計算 + - / * %
- 字符串連接 +
- 邏輯 && ||
- 二進制 & | ^
- 一元 + - ! ~
- 位移 >> >>> <<
- 比較 == > < >= <=
- instanceof
- 組 ()
- 字面量 - 字符,字符串,數字, null
- 類型轉換
- 函數調用
- 域存取
- 數組存取 []
- 三元運算符 ?:
例子:
android:text="@{String.valueOf(index + 1)}" android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}" android:transitionName='@{"image_" + id}'
缺失的操作符
一些 Java 中的操作符在表達式語法中不能使用。
- this
- super
- new
- 顯式泛型調用 <T>
Null合并運算符
Null合并運算符( ?? )會在非 null 的時候選擇左邊的操作,反之選擇右邊。
android:text="@{user.displayName ?? user.lastName}"
等同于
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
屬性引用
首先是先前 編寫你的第一個 data binding 表達式 中所提到的:JavaBean 引用。當表達式引用了一個類內的屬性時,他會嘗試直接調用域,getter,還有 ObservableFields。
android:text="@{user.lastName}"
避免NullPointerException
自動生成的 data binding 代碼會自動檢查和避免 null pointer exceptions。舉個例子,在表達式 @{user.name} 中,如果 user 是 null, user.name 會賦予默認值 null 。如果你引用了 user.age ,因為 age 是 int 類型,所以默認賦值為 0。
容器類
通用的容器類:數組,lists,sparse lists,和 map,可以用 [] 操作符來存取
<data> <import type="android.util.SparseArray"/> <import type="java.util.Map"/> <import type="java.util.List"/> <variable name="list" type="List<String>"/> <variable name="sparse" type="SparseArray<String>"/> <variable name="map" type="Map<String, String>"/> <variable name="index" type="int"/> <variable name="key" type="String"/> </data> … android:text="@{list[index]}" … android:text="@{sparse[index]}" … android:text="@{map[key]}"
字符串字面量
使用單引號把屬性包起來,就可以很簡單地在表達式中使用雙引號:
android:text='@{map["firstName"]}'
也可以用雙引號將屬性包起來。這樣的話,字符串字面量就可以用 " 或者反引號(`) 來調用
android:text="@{map[`firstName`}" android:text="@{map["firstName"]}"
資源
也可以在表達式中使用普通的語法來引用資源:
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
字符串格式化和復數形式可以這樣實現:
android:text="@{@string/nameFormat(firstName, lastName)}" android:text="@{@plurals/banana(bananaCount)}"
當復數形式有多個參數時,應該這樣寫:
Have an orange Have %d orangesandroid:text="@{@plurals/orange(orangeCount, orangeCount)}"</pre>
一些資源需要顯示類型調用。
Class | Listener Setter | Attribute |
---|---|---|
String[] | @array | @stringArray |
int[] | @array | @intArray |
TypedArray | @array | @typedArray |
Animator | @animator | @animator |
StateListAnimator | @animator | @stateListAnimator |
color int | @color | @color |
ColorStateList | @color | @colorStateList |
數據對象
任何 POJO 都能用在 data binding 中,但是更改 POJO 并不會同步更新 UI。data binding 的強大之處就在于它可以讓你的數據擁有更新通知的能力。這里有三種不同的數據變動通知機制, Observable 對象 , observable 域 ,與 observable 容器類 。
當以上的 observable 對象綁定在 UI 上,數據發生變化時,UI 就會同步更新。
Observable 對象
當一個類聲明了 Observable 接口時,data binding 會設置一個 listener 在綁定的對象上,以便監聽對象域的變動。
Observable 接口有一個添加/移除 listener 的機制,但通知取決于開發者。為了簡化開發,我們創建了一個基類 BaseObservable ,來實現 listener 注冊機制。這個類也實現了域變動的通知,你只需要在 getter 上使用 Bindable 注解,并在 setter 中實現通知。
private static class User extends BaseObservable { private String firstName; private String lastName; @Bindable public String getFirstName() { return this.firstName; } @Bindable public String getLastName() { return this.lastName; } public void setFirstName(String firstName) { this.firstName = firstName; notifyPropertyChanged(BR.firstName); } public void setLastName(String lastName) { this.lastName = lastName; notifyPropertyChanged(BR.lastName); } }
Bindable 注解會在編譯時在 BR 類內生成一個元素。而 BR 類會生成在 module 的 package 下。如果數據基類不可修改, Observable 接口的存儲和 listener 通知可以用 PropertyChangeRegistry 來實現。
Observable域
創建 Observable 類還是需要花費一點時間的,如果開發者想要省時,或者數據類的域很少的話,可以使用 ObservableField 以及它的派生 ObservableBoolean , ObservableByte , ObservableChar , ObservableShort , ObservableInt , ObservableLong , ObservableFloat , ObservableDouble , ObservableParcelable 。 ObservableFields 是單一域的自包含 observable 對象。原始版本避免了在存取過程中做打包/解包操作。要使用它,在數據類中創建一個 public final 域:
private static class User { public final ObservableField<String> firstName = new ObservableField<>(); public final ObservableField<String> lastName = new ObservableField<>(); public final ObservableInt age = new ObservableInt(); }
就這么簡單!要存取數據,只需要使用 get set 方法:
user.firstName.set("Google"); int age = user.age.get();
Observable 容器類
一些應用會使用更加靈活的結構來保持數據。Observable 容器類允許使用 key 來獲取這類數據。當 key 是類似 String 的一類引用類型時,使用 ObservableArrayMap 會非常方便。
ObservableArrayMap<String, Object> user = new ObservableArrayMap<>(); user.put("firstName", "Google"); user.put("lastName", "Inc."); user.put("age", 17);
在布局中,可以用 String key 來獲取 map 中的數據:
<data> <import type="android.databinding.ObservableMap"/> <variable name="user" type="ObservableMap<String, Object>"/> </data> … <TextView android:text='@{user["lastName"]}' android:layout_width="wrap_content" android:layout_height="wrap_content"/> <TextView android:text='@{String.valueOf(1 + (Integer)user["age"])}' android:layout_width="wrap_content" android:layout_height="wrap_content"/>
當 key 是整數類型時,可以使用 ObservableArrayList :
ObservableArrayList<Object> user = new ObservableArrayList<>(); user.add("Google"); user.add("Inc."); user.add(17);
在布局文件中,使用下標獲取列表數據:
<data> <import type="android.databinding.ObservableList"/> <import type="com.example.my.app.Fields"/> <variable name="user" type="ObservableList<Object>"/> </data> … <TextView android:text='@{user[Fields.LAST_NAME]}' android:layout_width="wrap_content" android:layout_height="wrap_content"/> <TextView android:text='@{String.valueOf(1 + (Integer)user[Fields.AGE])}' android:layout_width="wrap_content" android:layout_height="wrap_content"/>
生成Binding
生成的 binding 類將布局中的 View 與變量綁定在一起。就像先前提到過的,類名和包名可以 自定義 。生成的 binding 類會繼承 ViewDataBinding 。
創建
binding 應該在 inflate 之后創建,確保 View 的層次結構不會在綁定前被干擾。綁定布局有好幾種方式。最常見的是使用 binding 類中的靜態方法。inflate 函數會 inflate View 并將 View 綁定到 binding 類上。此外有更加簡單的函數,只需要一個 LayoutInflater 或一個 ViewGroup:
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater); MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater, viewGroup, false);
如果布局使用不同的機制來 inflate,則可以獨立做綁定操作:
MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);
有時綁定關系是不能提前確定的。這種情況下,可以使用 DataBindingUtil :
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId, parent, attachToParent); ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId);
帶有 ID 的 View
布局中每一個帶有 ID 的 View,都會生成一個 public final 域。binding過程會做一個簡單的賦值,在 binding 類中保存對應 ID 的 View。這種機制相比調用 findViewById 效率更高。舉個例子:
<layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <variable name="user" type="com.example.User"/> </data> <LinearLayout android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user.firstName}" android:id="@+id/firstName"/> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user.lastName}" android:id="@+id/lastName"/> </LinearLayout> </layout>
將會在 binding 類內生成:
public final TextView firstName; public final TextView lastName;
ID 在 data binding 中并不是必需的,但是在某些情況下還有有必要對 View 進行操作。
變量
每一個變量會有相應的存取函數:
<data> <import type="android.graphics.drawable.Drawable"/> <variable name="user" type="com.example.User"/> <variable name="image" type="Drawable"/> <variable name="note" type="String"/> </data>
并在 binding 類中生成對應的 getter setter:
public abstract com.example.User getUser(); public abstract void setUser(com.example.User user); public abstract Drawable getImage(); public abstract void setImage(Drawable image); public abstract String getNote(); public abstract void setNote(String note);
ViewStub
ViewStub 相比普通 View 有一些不同。ViewStub 一開始是不可見的,當它們被設置為可見,或者調用 inflate 方法時,ViewStub 會被替換成另外一個布局。
因為 ViewStub 實際上不存在于 View 結構中,binding 類中的類也得移除掉,以便系統回收。因為 binding 類中的 View 都是 final 的,所以我們使用了一個叫 ViewStubProxy 的類來代替 ViewStub 。開發者可以使用它來操作 ViewStub,獲取 ViewStub inflate 時得到的視圖。
但 inflate 一個新的布局時,必須為新的布局創建一個 binding。因此, ViewStubProxy 必須監聽 ViewStub 的 ViewStub.OnInflateListener ,并及時建立 binding。由于 ViewStub 只能有一個 OnInflateListener,你可以將你自己的 listener 設置在 ViewStubProxy 上,在 binding 建立之后, listener 就會被觸發。
高級 binding
動態變量
有時候,有一些不可知的 binding 類。例如, RecyclerView.Adapter 可以用來處理不同布局,這樣的話它就不知道應該使用哪一個 binding 類。而在 onBindViewHolder(VH, int) ) 的時候,binding 類必須被賦值。
在這種情況下,RecyclerView 的布局內置了一個 item 變量。 BindingHolder 有一個 getBinding 方法,返回一個 ViewDataBinding 基類。
public void onBindViewHolder(BindingHolder holder, int position) { final T item = mItems.get(position); holder.getBinding().setVariable(BR.item, item); holder.getBinding().executePendingBindings(); }
直接 binding
當變量或者 observable 發生變動時,會在下一幀觸發 binding。有時候 binding 需要馬上執行,這時候可以使用 executePendingBindings() )。
后臺線程
只要數據不是容器類,你可以直接在后臺線程做數據變動。Data binding 會將變量/域轉為局部量,避免同步問題。
屬性 Setter
當綁定數據發生變動時,生成的 binding 類必須根據 binding 表達式調用 View 的 setter 函數。Data binding 框架內置了幾種自定義賦值的方法。
自動 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}"/>
重命名 Setter
一些屬性的命名與 setter 不對應。針對這些函數,可以用 BindingMethods 注解來將屬性與 setter 綁定在一起。舉個例子, android:tint 屬性可以這樣與 setImageTintList(ColorStateList) ) 綁定,而不是 setTint :
@BindingMethods({ @BindingMethod(type = "android.widget.ImageView", attribute = "android:tint", method = "setImageTintList"), })
Android 框架中的 setter 重命名已經在庫中實現了,開發者只需要專注于自己的 setter。
自定義 Setter
一些屬性需要自定義 setter 邏輯。例如,目前沒有與 android:paddingLeft 對應的 setter,只有一個 setPadding(left, top, right, bottom) 函數。結合靜態 binding adapter 函數與 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()); }
Binding adapter 在其他自定義類型上也很好用。舉個例子,一個 loader 可以在非主線程加載圖片。
當存在沖突時,開發者創建的 binding adapter 會覆蓋 data binding 的默認 adapter。
你也可以創建多個參數的 adapter:
@BindingAdapter({"bind:imageUrl", "bind:error"}) public static void loadImage(ImageView view, String url, Drawable error) { Picasso.with(view.getContext()).load(url).error(error).into(view); }
</div>
<ImageView app:imageUrl=“@{venue.imageUrl}” app:error=“@{@drawable/venueError}”/>
</div>
當 imageUrl 與 error 存在時這個 adapter 會被調用。imageUrl 是一個 String,error 是一個 Drawable。
- 在匹配時自定義命名空間會被忽略
- 你可以為 android 命名空間編寫 adapter
Binding adapter 方法可以獲取舊的賦值。只需要將舊值放置在前,新值放置在后:
@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()); } }
事件 handler 僅可用于只擁有一個抽象方法的接口或者抽象類。例如:
@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); } } }
當 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); }</pre>
因為改變一個 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); } }
}</pre>
上面的例子比普通情況下復雜,因為 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 或更新的設備上使用。
轉換器
對象轉換
當 binding 表達式返回對象時,會選擇一個 setter(自動 Setter,重命名 Setter,自定義 Setter),將返回對象強制轉換成 setter 需要的類型。
下面是一個使用 ObservableMap 保存數據的例子:
<TextView android:text='@{userMap["lastName"]}' android:layout_width="wrap_content" android:layout_height="wrap_content"/>在這里, userMap 會返回 Object 類型的值,而返回值會被自動轉換成 setText(CharSequence) 所需要的類型。當對參數類型存在疑惑時,開發者需要手動做類型轉換。
自定義轉換
有時候會自動在特定類型直接做類型轉換。例如,當設置背景的時候:
<View android:background="@{isError ? @color/red : @color/white}" android:layout_width="wrap_content" android:layout_height="wrap_content"/>在這里,背景需要的是 Drawable ,但是 color 是一個整數。當需要 Drawable 卻返回了一個整數時, int 會自動轉換成 ColorDrawable 。這個轉換是在一個 BindingConversation 注解的靜態函數中實現:
<p>@BindingConversion public static ColorDrawable convertColorToDrawable(int color) { return new ColorDrawable(color); }</p>
需要注意的是,這個轉換只能在 setter 階段生效,所以 不允許 混合類型:
<View android:background="@{isError ? @drawable/error : @color/white}" android:layout_width="wrap_content" android:layout_height="wrap_content"/>Android Studio 對 Data binding 的支持
Android Studio 支持 data binding 表達式的高亮,并會在編輯器中標出表達式中的語法錯誤。
在預覽窗口顯示的是 data binding 表達式的默認值。下面是一個設置默認值的例子, TextView 的 text 默認值為 PLACEHOLDER 。
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user.firstName, default=PLACEHOLDER}"/>如果你需要在設計階段顯示默認值,你可以使用 tools 屬性代替默認值表達式,詳見 設計階段布局屬性 。
</div>