Android Data Binding框架指南
這份文件闡述瞭如何使用Data Binding Library來編寫申明式的佈局檔案和減少應用邏輯和佈局之間必要的繫結程式碼。
Data Binding Library以支援包的方式釋出,具有靈活性和廣泛的相容性,因此你可以在Android2.1(API level 7+)及以上的版本中使用這個框架
使用Data Binding Library需要使用 Android Plugin for Gradle 1.5.0-alpha1或者以上版本的gradle外掛
1 構建環境
使用data binding框架,需要使用Android SDK manager中從支援庫中下載相應的支援包,然後在你的應用中配置使用data binding框架,即在你的應用構建指令碼build.gradle中新增dataBinding元素,示例如下:
android {
...
dataBinding {
enabled = true
}
}
如果你的應用依賴一個使用了data binding框架的library,那麼,你頁需要在你的應用的構建檔案中新增上述配置。
另外,確保您使用的是已經支援了data binding的Android studio,即,需要升級到Android studio 1.3 或者更高版本。
2 資料繫結佈局檔案
2.1 編寫第一個data binding表示式
Data-binding佈局檔案與普通的佈局檔案相比,有一些細微的差別,體現在:
- 佈局檔案的根節點是layout
- layout裡面需要一個data元素和一個view根節點,view是就是普通的android的佈局檔案
示例如下:
<?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"/>
這個user變數可以被view使用
在佈局檔案中的,可以使用表示式”@{}”來讀取變數的屬性,在上例中,TextView的文字被設定成user的firstName:
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}"/>
2.2 資料物件
假設你有一個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;
}
}
或者是JavaBean風格的類User:
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}訪問第一個型別的物件的firstName屬性,也可訪問第二個型別的物件的getFirstName()方法,同時,它還可以訪問firstName()方法,如果存在這樣的方法的話。
2.3 繫結資料
預設地,AS會在佈局檔案的基礎上產生一個繫結者類,其名字遵循Pascal 格式然後以Binding為字尾,在上面的例子中,佈局檔案的名字是main_layout.xml,因此生成的類名就是MainActivityBinding。這個類包含了佈局檔案中變數屬性和控制元件屬性的繫結關係。建立繫結關係最簡單的方式如下:
@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);
}
執行上述程式碼,可以看到,User的屬性就展示在TextView中了。也可以使用另外一種方式去得到這個繫結關係:
MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater());
當你在ListView或者RecyclerView中使用data binding的時候,你需要使用下面的方式來獲取繫結關係:
ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
//or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater,
R.layout.list_item, viewGroup, false);
2.4 繫結事件
各種事件可以直接繫結到處理方法上,這與android:onClick相似,可以將空間的click事件指派給activity的一個方法一樣。大部分的事件屬性名稱與監聽者的響應方法的名稱一致,也有少許例外。例如,View.OnLongClickListener 有一個onLongClick()方法,因此事件的屬性就是android:onLongClick.
要將一個事件指派給一個處理者,可以使用正常的繫結表示式,值就是要呼叫的方法名稱。例如,你的資料物件有兩個方法:
public class MyHandlers {
public void onClickFriend(View view) { ... }
public void onClickEnemy(View view) { ... }
}
繫結表示式可以為控制元件將click事件指派給MyHandlers如下:
<?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>
一些特殊的控制元件的點選事件並不是由OnClickListener處理,因此由其他屬性來配置,此時就需要配置到其他的屬性裡,如下表:
Class | Listener Setter | Attribute |
---|---|---|
SearchView | setOnSearchClickListener(View.OnClickListener) | android:onSearchClick |
ZoomControls | setOnZoomInClickListener(View.OnClickListener) | android:onZoomIn |
ZoomControls | setOnZoomOutClickListener(View.OnClickListener) | android:onZoomOut |
3 佈局檔案細節
3.1 匯入
data元素可以包含0個或多個import子元素,每個import元素可以引入1個class,引入之後,佈局檔案就可以像在java檔案裡一樣,訪問被引入的class,例如:
<data>
<import type="android.view.View"/>
</data>
在layout的data如上程式碼,那麼在layout的view中就可以訪問android.view.View類
<TextView
android:text="@{user.lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
上面TextView中,android:visibility屬性中引用了View.VISIBLE 和 View.GONE
如果引入的兩個類的名字存在存在,那麼可以使用alias來給衝突的類起別名
<import type="android.view.View"/>
<import type="com.example.real.estate.View" alias="Vista"/>
這樣,在該佈局檔案內View就指的是android.view.View,Visita指的是com.example.real.estate.View
類引入之後,就可以作為變數的型別:
<data>
<import type="com.example.User"/>
<import type="java.util.List"/>
<variable name="user" type="User"/>
<variable name="userList" type="List<User>"/>
</data>
備註: 目前AS還不支援處理imports,因此變數的自動不全可能還不能正常工作,但是這並不影響程式的正常編譯。一個解決辦法是定義變數時使用類的全名.
也可以參與表示式運算:
<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.*
3.2 變數
data 元素可以包含任意多個variable子元素,每個variable子元素描述了一個可以向這個layout設定的用於繫結表示式的屬性。
<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>
上面定義了3個變數,以及它們的型別,在資料繫結的時候,外部為這3個變數賦值。
變數的型別在編譯的時候進行檢查,因此一個變數是否實現了Obserable或者是一個Obserable Collection反應在type屬性。如果一個變數沒有實現Observable系列的介面,哪些這個變數就不會被觀察!(即繫結之後,這個變數資料發生變化時不會反應到空間上)
當在不同配置(如橫豎屏)時有不同佈局檔案,這些變數會被合併在一起,因此這些佈局檔案之間不能有衝突的變數定義。
生成的binding class 會為每一個變數建立一個setter和getter方法,變數的預設值規則與java的預設值規則一致
一個特殊的變數context也會被建立,它的值是根空間的getContext(),如果顯示申明瞭context這個變數,那麼預設的context變數會被過載
3.3 自定義Binding名稱
Binding class 的預設名字是基於layout的名字,即首字母大寫,去掉”_”,然後以Binding結尾,這個class會放工程的在databinding包下面,例如,佈局檔案contact_item.xml將為生成ContactItemBinding,如果工程的包名是com.example.my.app,那麼這個ContactItemBinding將會放到com.example.my.app.databinding下面。
Binding class 的名字也可以定義,有一下的三種自定義方式:
方法一:
<data class="ContactItem">
...
</data>
生成Binding class : com.example.my.app.databinding.ContactItem
方法二:
<data class=".ContactItem">
...
</data>
生成Binding class : com.example.my.app.ContactItem
方法三:
<data class="com.example.ContactItem">
...
</data>
生成Binding class : com.example.ContactItem
3.4 包含
佈局檔案中的變數可以通過名稱空間何變數名作為屬性名稱傳遞到被包含的佈局檔案中去。例如:
<?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必須要包含一個名稱為name的變數。
但是,data binding並不支援向直接嵌入merge的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>
<merge>
<include layout="@layout/name" bind:user="@{user}"/>
<include layout="@layout/contact" bind:user="@{user}"/>
</merge>
</layout>
(注:上面的include作為merge的直接子孩子,因此在該佈局中申明的變數不能傳遞到被包含的佈局中去)
3.5 表示式語言
3.5.1 一般功能
表示式語言非常像java語言,下面的表示式與java一致:
- 資料運算子 +-*/ %
- 字串連線符 +
- 邏輯運算子 && ||
- 位運算子 & | ^
- 一元運算子 + - ! ~
- 移位運算子 >> >>> <<
- 比較運算子 == > < >= <=
- 例項判斷 instanceof
- 括號 ()
- 字面字串,數字,null
- 型別轉換()
- 方法呼叫
- 成員呼叫
- 陣列訪問
- 三元運算子 ? :
舉例如下:
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age > 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
3.5.2 預設操作
也有少量的表示式是預設的,可以直接使用,如下
- this
- super
- new
- Explicit generic invocation(注:顯示呼叫即.符號)
3.5.3 空聯合操作符
?? 是空聯合操作符,它的含義是如果左邊不為空則取左邊的值,否則取右邊的值,如下:
android:text="@{user.displayName ?? user.lastName}"
上面的表示式,等價於:
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
3.5.4 屬性引用
上過已經討論過,當表示式要引用一個類的成員變數時,使用相同的格式去訪問成員變數,getter和ObservableFields,如下:
android:text="@{user.lastName}"
3.5.5 空保護
自動生成的Binding自動檢查空指標從避免空指標異常, 例如表示式@{user.name}中,如果user為空,則user.name則返回其預設值null,而如果user的成員age為int型,表示式@{user.age}則返回其預設值0
3.5.6 集合
常用的集合:陣列,List,Sparse List,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]}"
3.5.7 字面字串
當需要在表示式中使用字面字串,其需要用雙引號括起來,此時需要將xml屬性值的雙引號換成單引號,如下:
android:text='@{map["firstName"]}'
或者使用反引號來標識字面字串:
android:text="@{map[`firstName`}"
android:text="@{map[`firstName`]}"
3.5.8 資源
可以使用正常的語法來訪問資源:
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
格式化字串和複數字串,可以通過提供引數計算出來:
android:text="@{@string/nameFormat(firstName, lastName)}"
android:text="@{@plurals/banana(bananaCount)}"
當一個複數字元穿需要多個引數時,需要傳遞所有的引數:
Have an orange
Have %d oranges
android:text="@{@plurals/orange(orangeCount, orangeCount)}"
一些資源需要明確的型別運算,如下表:
Type | Normal Reference | Expression Reference |
---|---|---|
String[] | @array | @stringArray |
int[] | @array | @intArray |
TypedArray | @array | @typedArray |
Animator | @animator | @animator |
StateListAnimator | @animator | @stateListAnimator |
color int | @color | @color |
ColorStateList | @color | @colorStateList |
4 資料物件
任何POJO都可以用於data binding,但是對POJO的修改並不能導致UI重新整理,data binding真正強大的地方是可以給資料物件提供在其發生變化時通知介面重新整理的的能力。共有三種不同的資料變化通知機制,即Observable Objects,ObservableFields,Observable Collections.當上面的任何一種資料繫結到控制元件之後,只要他們的一個屬性發生變化,就可以自動更新控制元件。
4.1 Observable Objects
一個類實現了android.databinding.Observable介面,就允許binding新增一個監聽者到該資料物件去監聽它的屬性的變化。
Observable介面定義了新增和移除監聽者的機制,但是通知時機是取決於開發者的,為了是開發變得更加的容易,提供了一個基類,android.databinding.BaseObservable,它實現了監聽者的註冊機制,但是資料實現者仍然需要負責通知其屬性發生了變化,這可以通過給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);
}
}
4.2 ObservableFields
建立Observable類還是需要耗費一些時間的,因此想要節省時間或者只需要少量屬性的情況下,可以使用ObservableField和他們的兄弟類,ObservableBoolean,ObservableByte,ObservableChar,ObservableShort,ObservableInt,ObservableLong,ObservableFloat,ObservableDouble和ObservableParcable
ObservableField是擁有單一成員的自包含的可觀察的物件。基本資料版本的ObservableField可以避免訪問過程中的裝箱和拆箱,想要使用ObservableField,建立一個public final的ObservableField成員即可:
private static class User {
public final ObservableField<String> firstName =
new ObservableField<>();
public final ObservableField<String> lastName =
new ObservableField<>();
public final ObservableInt age = new ObservableInt();
}
這樣,就可以使用set和get方法來訪問這些值:
user.firstName.set("Google");
int age = user.age.get();
4.3 Observable Collections
Some applications use more dynamic structures to hold data. Observable collections allow keyed access to these data objects. ObservableArrayMap is useful when the key is a reference type, such as String.
一些應用使用更加動態的資料結構來儲存資料,Observable collections允許使用關鍵字來訪問這些資料物件。當關鍵字是飲用型別時,如字串時,ObservableArrayMap就非常有用了。
ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);
in the layout, the map may be accessed through the String keys:
在佈局中可以通過keys來訪問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"/>
當關鍵字是整數時,可以使用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"/>
5 建立繫結者
產生的Binding類將佈局中的變數和佈局中的控制元件聯絡在一起,前面討論過,Binding的名字和包名可以定製。產生的Binding類都繼承自ViewDataBinding。
5.1 建立
View inflated之後需要很快建立binding以確保佈局中的表示式繫結到控制元件之前檢視層級不會被打擾。有幾種方法可以繫結到佈局。最常用的方式是使用Binding類的靜態方法。inflater方法inflate檢視層級同時一次性的繫結資料,第二個簡單一點的方法只接受一個LayoutInflater和第三個接受一個LayoutInflater 和ViewGroup的方法:
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater);
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater, viewGroup, false);
如果佈局是用另外的方法產生的,就需要單獨繫結:
MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);
有時binding關係不能提前知道,在這種情況下,binding可以用DataBindingUtil類來建立:
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId,
parent, attachToParent);
ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId);
5.2 Views With IDs
佈局中有ID的每一個控制元件都會在Binding中建立一個public final的成員。Binding只遍歷一次檢視層級,抽取有其中有ID的控制元件。這種機制比多次呼叫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;
在沒有DataBinding的情況下,IDs幾乎沒有必要,但是在某些場景下也需要使用程式碼來訪問控制元件。
5.3 變數
每一個變數會生成對應的訪問方法
<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中生成setters和getters方法:
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);
5.4 ViewStubs
ViewStubs are a little different from normal Views. They start off
ViewStub與普通的View有一些差別,他們開始是不可見的,當他們變成可見或者顯式inflat的時候,他們通過inflate其他佈局產生的控制元件來替換自己。
由於ViewStub實質上會會從檢視層級中消失,Binding物件中的View也必須消失以便於回收。因為Views是final型的,一個ViewStubProxy將代替ViewStub,它賦予開發者訪問在ViewStub存在時訪問ViewStub的許可權以及在ViewStub inflate之後,訪問替代ViewStub的檢視層級。
當inflate另一個layout的時候,必須為新的佈局建立一個binding,因此,ViewStubProxy必須監聽ViewStub的onInflateListener,同時建立binding關係。因為只會存在一個,ViewStubProxy允許開發者設定一個OnInflaterListener,當建立binding之後,將會回撥改介面。
5.5 高階 Binding
5.5.1 動態變數 Variables
有時我們並不能知道具體的繫結關係,例如,一個RecyclerView.Adapter在處理任意佈局時並不知道具體的繫結關係,但它仍然需要需要在onBindViewHolder中分配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();
}
5.5.2 立即Binding
當一個變數或者observable變化時,在下一幀之前會新增修改binding的任務排程。儘管如此,有時我們需要立即執行新的binding,此時可以使用executePendingBindings方法來要強制執行新的binding。
5.5.3 後臺執行緒處理
你可以在後臺執行緒中修改資料模型的值,只要它不是一個集合,Data binding在計算時使用區域性變數來避免可能的併發問題。
6 屬性訪問者
任何時候一個繫結的資料發生變化時,產生的繫結類必須呼叫View的一個setter方法來傳遞表示式的值。data binding框架有好幾種方法來定製呼叫View的哪一個方法來設定值。
6.1 自動訪問者
對於一個屬性,data binding會嘗試尋找對應的setAttribute方法。屬性的名稱空間並沒有關係,只匹配該屬性的名字本身。
例如,一個表示式與TextView的android:text屬性關聯,則會查詢TextView的setText(String)方法。如果一個表示式返回的值是int型的,data binding會查詢setText(int),需要注意保證express返回正確的資料型別,在必要時需要強制轉換。需要注意的是,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註解關聯在一起。這必須與一個類關聯在一起,同時包含BindingMethod註解,每一個重名方法的都需要一個這樣的註解。例如,android:tint熟悉實際上與setImageTintList(ColorStateList)關聯在一起的,而不是setTint
@BindingMethods({
@BindingMethod(type = "android.widget.ImageView",
attribute = "android:tint",
method = "setImageTintList"),
})
開發者並不需要重新命名setter,android framework的屬性已經實現了。
6.3 自定義訪問者
有些屬性需要自定義binding邏輯,例如,android:paddingLeft屬性並沒有關聯的setter方法,而是通過使用setPadding(left, top, right, bottom)來設定值。有一個靜態的帶有BindingAdapter註解的binding 介面卡方法允許開發者自定義如何呼叫一個屬性的setter方法
android的屬性已經建立了BindingAdapters,paddingLeft的如下:
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
Binding adapters 對其他型別的自定義也是非常有用的,例如,一個自定義的loader 可以通過子執行緒去載入一張圖片。
當存在衝突時,開發者建立的binding adapter會重寫系統預設的adapter。
你也可以建立有接受多個引數的adapter。
@BindingAdapter({"bind:imageUrl", "bind:error"})
public static void loadImage(ImageView view, String url, Drawable error) {
Picasso
.with(view.getContext())
.error(error)
.into(view);
}
<ImageView app:imageUrl=“@{venue.imageUrl}”
app:error=“@{@drawable/venueError}”/>
這個adapter被呼叫的條件是一個ImageView同時使用imageUrl和error兩個屬性,且imageUrl是一個字串,error是一個drawable。
再匹配的時候,自定義的名稱空間會被忽略。你也可以為android命名編寫adapter方法。
Binding adpter方法可能會選擇的使用舊的值,一個接受舊的和新的值的方法需要將舊的方法放在第一個引數,新的值放在第二引數。
@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());
}
}
事件處理者只能用於只有一個抽象方法的介面或者抽象類,例如:
Event handlers may only be used with interfaces or abstract classes with one abstract method. For example:
@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.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);
}
因為改變一個監聽者會影響到另外一個,我們必須定義三個不同的adapter,每個屬性一個adapter,和一個同時給兩個屬性賦值的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來操作listener,而不是一組操作View.OnAttachStateChangeListener的方法。android.databinding.adapters.ListenerUtil類幫助跟蹤先前的監聽者,這樣它們可以從Binding Adapter種移除。
By annotating the interfaces OnViewDetachedFromWindow and OnViewAttachedToWindow with @TargetApi(VERSION_CODES.HONEYCOMB_MR1), the data binding code generator knows that the listener should only be generated when running on Honeycomb MR1 and new devices, the same version supported by addOnAttachStateChangeListener(View.OnAttachStateChangeListener).