为什么?
之前开发,一直使用依赖注入框架 - ButterKnife。自从Android Studio升级3.0以来,ButterKnife一直受到Gradle API的影响,不能升级Gradle版本,这也算是一大诟病,导致于升级Android studio时,慎之又慎。偶尔想到还有Google的官方框架 - DataBinding,我想是入手的时候了…本文是官方文档的翻译,只是简明的介绍了如何使用Data Binding。
概述
这篇文章介绍了如何使用Data Binding库来写声明的layouts文件,并且用最少的代码来绑定你的app逻辑和layouts文件。
Data Binding库不仅灵活而且广泛兼容- 它是一个support库,因此你可以在所有的Android平台最低能到Android 2.1(API等级7+)上使用它。
要使用数据绑定,Android Plugin for Gradle 1.5.0-alpha1
或更高版本是必需的。 了解如何更新Android Plugin for Gradle
。
构建环境
要开始使用数据绑定,需用在Android SDK管理器的Support repository
中下载支持库。
要配置应用程序可以使用数据绑定,将dataBinding
元素添加到build.gradle
文件中的app模块中。
使用以下代码片段配置数据绑定:
android {
....
}
dependencies {
annotationProcessor 'com.android.databinding:compiler:3.1.2'
}
如果app
模块依赖于使用数据绑定的库,则app
模块也必须在其build.gradle文件中配置数据绑定。
同时,确保使用的Android Studio版本兼容数据绑定功能。Android Studio1.3
以及更高版本支持数据绑定,如[Android Studio支持数据绑定]中所述。
数据绑定编译器V2
Android Gradle Plugin 3.1.0 Canary 6
附带一个可选的新编译器。 要开始使用它,更新gradle.properties
文件以包含以下行:
android.databinding.enableV2=true
在编译器v2中:
ViewBinding
类是在Java编译之前由Android Gradle Plugin
生成的。 如果java编译由于一个不相关的原因而失败,这避免了导致得到太多误报错误。- 在
V1
中 ,binding
系列的类将会在app编译完成后再次生成(去分享生成的代码并关联到 常量BR
和R
文件)。在V2
中,绑定库将保存其生成的绑定类以及映射信息,这些信息可显著提高多模块项目的数据绑定性能。
注意: 新的V2编译器是向后不兼容,所以使用v1编译的库不能被V2使用,反之亦然。
V2
还删除了一些很少使用的功能以允许这些更改:
- 在
V1
中,应用程序能够提供可以覆盖依赖项中的适配器的绑定适配器。在V2
中,它只会对自己的模块/应用程序及其依赖项中的代码生效。 - 以前,如果一个布局文件在2个或更多不同资源配置中包含具有相同标识但不同类型的View,则数据绑定会查找最常见的父类。在V2中,当配置之间的类型不匹配时,它始终默认为
View
。 - 在
V2
中,不同的模块不能在manifest
中使用相同的包名称,因为数据绑定将使用该包名称来生成绑定映射类。
数据绑定布局文件
数据绑定表达式
数据绑定布局文件略有不同,以layout
作为根标签,紧随跟着data
元素和view
根节点。这个view
根节点是在非绑定布局文件中的根标签。
<?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
,使其可以在这个layout中使用:
<variable name="user" type="com.example.User"/>
布局中的表达式使用@{}
语法将variable
的属性写入View
的属性中,下面是一个TextView
的text
设置为user
的firstName
属性:
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}"/>
数据对象
现在假设有一个普通的Java对象(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;
}
}
从数据绑定的角度来看,这两个类是等价的。用于TextView
的android:text
属性的表达式@{user.firstName}
将访问前一类中的firstName
字段和后一类中的getFirstName()
方法。或者,如果该方法存在,它也将被解析为firstName()
。
绑定数据
默认情况下,将根据布局文件的名称生成一个Binding
类,并将其转换为Pascal
格式并为其添加后缀Binding
。比如,上面的布局文件是main_activity.xml
,所以生成的类是MainActivityBinding
。这个类将布局属性(例如,user
变量)的所有绑定保存到布局的Views中,并知道如何为绑定表达式分配值。创建绑定的最简单方法是在inflate
时执行此操作:
@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
。或者,可以通过以下方式获取视图:
MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater());
如果在ListView或RecyclerView适配器内使用数据绑定,推荐这么使用:
ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
//or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);
事件处理
数据绑定允许编写表达式来处理从View
中分派的事件(例如onClick)。除少数例外,事件属性名称由监听方法的名称管理。例如,View.OnLongClickListener
有一个onLongClick()
方法,所以这个事件的属性是android:onLongClick
。
处理事件有两种方式:
- 方法引用:在表达式中,可以引用符合侦听方法签名的方法。当表达式求值为方法引用时,数据绑定将方法引用和持有者对象封装在侦听中,并将该监听绑定到在目标View上。如果表达式求值为null,则数据绑定不会创建监听,而是将监听设置为null。
- 监听绑定:这些是在事件发生时被执行的lambda表达式。数据绑定总是创建一个设置在View上的监听。 当事件分派时,监听执行lambda表达式。
方法引用
事件可以直接绑定到处理方法,类似于将android:onClick
分配给Activity中的方法。与View:onClick
属性相比,一个主要优势是表达式在编译时处理,所以如果该方法不存在或其签名不正确,则会收到编译时错误。
方法引用和监听绑定之间的主要区别在于实际的监听实现是在数据绑定时创建的,而不是在触发事件时创建的。如果希望在事件发生时对表达式求值,则应该使用侦听绑定。
要将事件分配给其处理方法,使用正常的绑定表达式,其值是要调用的方法名称。 例如,如果你的数据对象有两种方法:
public class MyHandlers {
public void onClickFriend(View view) { ... }
}
绑定表达式可以为View分配一个点击监听:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="handlers" type="com.example.MyHandlers"/>
<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="@{handlers::onClickFriend}"/>
</LinearLayout>
</layout>
注意,表达式中方法的签名必须与Listener对象中方法的签名完全匹配.
对于被引用的方法,其参数必须包含一个参数(view: View).
监听绑定
监听绑定是事件发生时执行绑定表达式。它与方法引用类似,但它运行任意数据绑定表达式。 此功能适用于Gradle 2.0及更高版本的Android Gradle Plugin
.
在方法引用中,方法的参数必须与监听事件的参数匹配。在监听绑定中,只要返回值必须与侦听的期望返回值相匹配(除非预期返回值为void)。例如,您可以拥有一个具有以下方法的presenter
类:
public class Presenter {
public void onSaveClick(Task task){}
}
然后,您可以将click事件绑定到Presenter上
,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="task" type="com.android.example.Task" />
<variable name="presenter" type="com.android.example.Presenter" />
</data>
<LinearLayout android:layout_width="match_parent" android:layout_height="match_parent">
<Button android:layout_width="wrap_content" android:layout_height="wrap_content"
android:onClick="@{() -> presenter.onSaveClick(task)}" />
</LinearLayout>
</layout>
监听由仅允许作为表达式根元素的lambda表达式表示。当在表达式中使用回调时,数据绑定会自动为该事件创建必要的侦听和注册。当View触发事件时,数据绑定将求值给定的表达式。 与常规绑定表达式一样,在求值这些侦听表达式时,仍然可以获得数据绑定的空值和线程安全。
注意,在上面的例子中,我们没有定义view
作为传入onClick(android.view.View)
的参数。监听绑定为监听参数提供了两种选择:可以忽略该方法的所有参数或命名所有参数.如果更喜欢命名参数,则可以在表达式中使用它们。例如,上面的表达式可以写成:
android:onClick="@{(view) -> presenter.onSaveClick(task)}"
或者如果想在表达式中使用参数,可以这么做:
public class Presenter {
public void onSaveClick(View view, Task task){}
}
android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}"
可以使用带有多个参数的lambda表达式:
public class Presenter {
public void onCompletedChanged(Task task, boolean completed){}
}
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onCheckedChanged="@{(cb, isChecked) -> presenter.completeChanged(task, isChecked)}" />
如果正在监听的事件返回的值不是void
,则表达式必须返回相同类型的值。例如,如果想要监听长按事件,则表达式应返回boolean
。
public class Presenter {
public boolean onLongClick(View view, Task task){}
}
android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"
如果由于空对象而无法求值表达式,Data Binding
将返回该类型的默认Java值。 例如,参考类型为null,int为0,boolean为false等。
如果需要使用带断言的表达式(例如三元),则可以使用void作为一个表达式:
android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"
避免复杂监听
监听表达式非常强大,可以让代码非常容易阅读。另一方面,监听包含复杂表达式会使布局难以阅读和无法维护。这些表达式应该像从UI中传递可用数据到回调方法一样简单。应该在监听表达式调用的回调方法内实现所有业务逻辑。
存在一些专用的单击事件处理,它们需要除android:onClick
之外的其他属性,以避免冲突. 已创建以下属性以避免此类冲突:
类 | 监听设置 | 属性 |
---|---|---|
SearchView | setOnSearchClickListener(View.OnClickListener) | android:onSearchClick |
ZoomControls | setOnZoomInClickListener(View.OnClickListener) | android:onZoomIn |
ZoomControls | setOnZoomOutClickListener(View.OnClickListener) | android:onZoomOut |
布局详情
import
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
可以用来在布局文件中引用android.view.View
。 导入的类型可以用作variable
和表达式中的类型引用:
<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尚未支持
import
,因此导入变量的自动补全功能可能无法在IDE中使用。但,应用程序仍然可以正常编译,可以通过在变量定义中使用完全限定名称来解决这个IDE问题。
<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。*
是自动导入的。
variable
data
元素内可以使用任意数量的variable
元素。每个variable
元素描述了可以在布局上设置的属性,以用于布局文件中的绑定表达式。
<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>
variable
类型在编译时被检查,所以如果一个variable
实现了android.databinding.Observable
或者是一个可观察的集合,那么它应该被映射在类型中。 如果variable
不是实现Observable *
接口的基类或接口,将不会观察变量的变化!
当各种配置(例如横向或纵向)有不同的布局文件时,variable
将被合并。 这些布局文件之间不得存在冲突的variable
定义。
生成的绑定类将为每个描述的变量创建一个setter
和getter
方法。 变量将采用Java
的默认值,直到调用setter
为止 - 引用类型为null,int为0,boolean为false。
在生成绑定类时,根据需要将默认创建一个用于绑定表达式的特殊变量context
。context
的值是根View调用getContext()
获取的Context
,也就是根View的Context
。如果显示的声明一个名为context
变量,将会覆盖其值。
自定义绑定类名称
默认情况下,根据布局文件的名称生成一个Binding
类,以大写字母开头,删除下划线(_)并大写其后的字母,然后添加后缀Binding
。该类将放置在模块包下的databinding
包中。例如,布局文件contact_item.xml
将生成类ContactItemBinding
。如果模块包是com.example.my.app
,那么它将被放置在com.example.my.app.databinding
中。
通过调整data
元素的class
属性,绑定类可以重命名或放置在不同的包中。 例如:
<data class="ContactItem">
...
</data
这会在模块包中的databinding
包中生成名为ContactItem
的绑定类。如果生成的类在模块包中的其他包中,则它可能会以。
作为前缀:
<data class=".ContactItem">
...
</data>
这样,生成的类ContactItem
将在模块包中。如果提供完整的包名,则可以使用任意的包:
<data class="com.example.ContactItem">
...
</data>
include
通过在include
属性中使用应用程序命名空间(即bind
)和变量名称,变量可以从父布局传递到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
。
数据绑定不支持inclucde
作为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
Grouping()
- 字面值 - character, String, numeric, null
- 求值
- 方法调用
- 字段访问
- 数组访问
[]
- 三元操作符
?:
例如:
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
缺失的操作符
可以在Java
中使用但在表达式语法中缺失的操作符:
- this
- super
- new
- 显式泛型调用
null合并运算符(??
)
如果null合并运算符(??
)的左操作数不为空,则选择左操作数,否则选择右操作数。
选择左操作数(如果它不为空)或选择右(如果它为空)。
android:text="@{user.displayName ?? user.lastName}"
等价于:
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
属性引用
在前面,数据绑定表达式中已经讨论了:简短形式的JavaBean引用
。 当表达式引用某个类的属性时,它将对字段,getter
和ObservableFields
使用相同的引用格式。
android:text="@{user.lastName}"
避免NullPointerException
生成的数据绑定代码会自动检查null
并避免空指针异常(NullPointerException
)。 例如,在表达式@{user.name}
中,如果user
为空,那么user.name
将默认为null
。如果引用了user.age
,而age
是一个int
类型,那么它将默认为0。
集合
常见的集合,比如array, lists sparse list, and 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}"
可以为格式化string
和plurals
提供参数:
android:text="@{@string/nameFormat(firstName, lastName)}"
android:text="@{@plurals/banana(bananaCount)}"
当一个plurals
有多个参数时,所有参数都应该传递:
Have an orange
Have %d oranges
android:text="@{@plurals/orange(orangeCount, orangeCount)}"
一些资源需要显式类型引用:
类型 | 正常引用 | 表达式引用 |
---|---|---|
String[] | @array | @stringArray |
int[] | @array | @intArray |
TypedArray | @array | @typedArray |
Animator | @animator | @animator |
StateListAnimator | @animator | @stateListAnimator |
color int | @color | @color |
ColorStateList | @color | @colorStateList |
数据对象
任何普通的POJO
都可用于数据绑定,但修改POJO不会通知UI更新。数据绑定的真正的强大特性是在POJO更改时通知UI更新。在数据绑定中,有三种不同的数据更改通知机制:可观察对象,可观察字段和可观察集合。
当这些可观察数据对象之一绑定到UI并且数据对象的属性更改时,UI将自动更新。
可观察对象
实现android.databinding.Observable
接口的类将允许绑定将单个监听添加到绑定对象以监听该对象上所有属性的更改。
android.databinding.Observable
接口具有添加和删除监听的机制,但通知由开发人员决定。为了简化开发,创建了基类android.databinding.BaseObservable
,以实现监听注册机制。实现接口的数据类仍然负责在属性更改时分发通知。这是通过使用android.databinding.Bindable
注解getter
并在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);
}
}
android.databinding.Bindable
注解在编译时在BR类文件中生成一个列表。BR类文件将在模块包中生成。如果无法更改数据类的基类,则可以使用方便的android.databinding.PropertyChangeRegistry
实现android.databinding.Observable
接口,以有效地存储和通知监听。
可观察字段
创建android.databinding.Observable
类会耗费一定时间,所以想要节省时间或具有少量可观察的属性,开发人员可能会使用android.databinding.ObservableField
及其同级的android.databinding.ObservableBoolean
,android.databinding.ObservableByte
,android.databinding.ObservableChar
,android.databinding.ObservableShort
,android.databinding.ObservableInt
,android.databinding.ObservableLong
,android.databinding.ObservableFloat
,android.databinding.ObservableDouble
和android.databinding.ObservableParcelable
。可观察字段是一个具有单个字段的独立的可观察对象。基本数据类型在访问操作期间避免装箱和拆箱。如果要使用可观察字段,需要在数据类中创建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();
}
要访问该值,需要使用set
和get
方法:
user.firstName.set("Google");
int age = user.age.get();
可观察集合
一些应用程序使用很多动态结构来保存数据。可观察集合允许对这些数据对象通过键值访问。当键是引用类型(如String
)时,android.databinding.ObservableArrayMap
非常有用。
ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);
在布局中,可以通过String
类型的键值访问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"/>
当键是一个整数时,android.databinding.ObservableArrayList
是有用的:
ObservableArrayList<Object> user = new ObservableArrayList<>();
user.add("Google");
user.add("Inc.");
user.add(17);
在布局中,list
可以通过索引访问:
<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"/>
创建绑定
生成的绑定类将布局变量与布局中的View
链接起来。如前所述,绑定类的名称和包是可以自定义的。生成的绑定类全部继承自android.databinding.ViewDataBinding
。
创建
在inflate
以后立即创建绑定,以确保View
层次结构在将带有表达式的View
绑定到布局之前不受干扰。有几种方法可以绑定到布局。最常见的是使用Binding类中的静态方法。inflate
方法将inflate
View
层次结构并将其绑定只需一步。有一个更简单的方式只需要一个LayoutInflater
,另一个则需要一个ViewGroup
:
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater);
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater, viewGroup, false);
如果布局使用不同的机制进行inflate
,则可能需要单独绑定:
MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);
有时绑定不能预先知道。在这种情况下,可以使用android.databinding.DataBindingUtil
类创建绑定:
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId,
parent, attachToParent);
ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId);
带有ID的View
生成Binding类时,将为布局中的每一个带有ID
的View
生成一个public final
字段。该绑定在View层次结构上执行单个传递,并使用ID
获取View
。 这种机制可能比调用多个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;
不使用数据绑定的View
,可以没有id
,但有些view
还是需要在代码中来访问.
Variables
生成的Binding
类,将为每个变量生成相应的get
和set
方法。
<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>
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
有点不同。它们从不可见的时候开始,如果要使它们可见时,要么被明确告知inflate
时,要么通过inflate
用另一种布局来取代布局。
<ViewStub
android:id="@+id/stub"
android:inflatedId="@+id/subTree"
android:layout="@layout/mySubTree"
android:layout_width="120dip"
android:layout_height="40dip" />
ViewStub stub = findViewById(R.id.stub);
View inflated = stub.inflate();
由于ViewStub
基本上在View
层次结构中不存在,绑定对象中的View
也必须不存在以允许收集.因为绑定对象中的View
是不可变的,所以android.databinding.ViewStubProxy
对象代替了ViewStub
,让开发人员能够在ViewStub
存在时访问ViewStub
,并且在ViewStub被填充时也可以访问填充的View
层次结构。
当填充另一个布局时,必须为新布局建立绑定。因此,ViewStubProxy
必须监听ViewStub
的ViewStub.OnInflateListener
并在此时建立绑定。 由于只有一个可以存在,因此ViewStubProxy
在建立绑定后设置一个OnInflateListener
以便ViewStub
调用它。
高级绑定
动态变量
有时,并不知道特定的绑定类。例如,针对任意布局的RecyclerView.Adapter
不知道特定的绑定类。它仍然必须在`onBindViewHolder(VH,int)中分发绑定值。
在这个例子中,RecyclerView绑定到的所有布局都有一个变量。BindingHolder有一个返回android.databinding.ViewDataBinding
基类的getBinding
方法。
public void onBindViewHolder(BindingHolder holder, int position) {
final T item = mItems.get(position);
holder.getBinding().setVariable(BR.item, item);
holder.getBinding().executePendingBindings();
}
立即绑定
当变量或可观察对象发生改变时,绑定将在显示下一帧之前改变。然而,有时候,绑定必须立即执行。要强制执行更改绑定,可以调用android.databinding.ViewDataBinding.executePendingBindings()
方法。
自动setter
对于一个属性,数据绑定试图找到方法setAttribute
。属性的命名空间并不重要,主要是属性名称本身。
例如,与TextView的属性android:text
相关联的表达式将查找setText(String)
。 如果表达式返回一个int
,那么数据绑定将查找一个setText(int)
方法。注意让表达式返回正确的类型,如果需要的话可以强制转换。注意,即使给定名称的属性不存在,数据绑定也可以工作。然后,可以使用数据绑定轻松地为任何属性创建setter
。例如,支持库中的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
。 对于这些方法,可以通过android.databinding.BindingMethods
注解将一个属性与setter
相关联。
这必须与一个类相关联,并且包含的每一个重命名方法被android.databinding.BindingMethod
注解。例如,android:tint
属性实际与setImageTintList(ColorStateList)
关联,而不是setTint
。
@BindingMethods({
@BindingMethod(type = "android.widget.ImageView",
attribute = "android:tint",
method = "setImageTintList"),
})
自定义setter
一些属性需要自定义绑定逻辑。例如,android:paddingLeft
属性没有关联的setter
。反而,setPadding(left, top, right, bottom)
存在.具有android.databinding.BindingAdapter
注解的静态绑定适配器方法可以自定义属性调用的setter
。
在BindingAdapter
中,自定义android:属性
的setter
.比如,在这里定义了与paddingLeft
相关联的setter
。
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
绑定适配器对其他类型的自定义很有用。 例如,一个自定义的加载器可以脱机调用用来加载一个图像。
发生冲突时,自定义的绑定适配器将覆盖数据绑定的默认适配器。
也可以让适配器接收多个参数。
@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);
}
<ImageView
app:imageUrl="@{venue.imageUrl}"
app:error="@{@drawable/venueError}"/>
如果imageUrl和error都用于ImageView并且imageUrl是字符串,并且error是Drawable,则将调用此适配器。
- 自定义命名空间在匹配过程中被忽略
- 也可以为android命名空间定义适配器
绑定适配器方法可以在其处理程序中选择使用旧值。如果一个方法有新旧数据作为参数,其顺序依次为旧数据、新数据,也就是这样:
@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());
}
}
事件处理参数只能是仅有一个抽象方法的接口或者抽象类。例如:
@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);
}
因为更改一个监听也会影响另一个监听,所以我们必须有三个不同的绑定适配器,一个用于每个属性,另一个用于两者,它们都应该被设置。
@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
来代替View.OnAttachStateChangeListener
的set
方法。android.databinding.adapters.ListenerUtil
类有助于跟踪以前的监听,以便可以在Binding Adaper中删除之前的监听。
通过使用@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
注解OnViewDetachedFromWindow
和OnViewAttachedToWindow
接口,数据绑定代码生成器知道只应在Honeycomb MR1
和新设备上运行监听时才能生成监听,而·addOnAttachStateChangeListenerView.(OnAttachStateChangeListener)`也是支持相同的SDK版本。
转换器
对象转换器
绑定表达式返回对象时,将从自动、重命名和自定义setter
中选择一个setter
。该对象将被转换为所选setter
的参数类型。
这对于使用ObservableMap
保存数据来说很方便。 例如:
<TextView
android:text='@{userMap["lastName"]}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
userMap
返回一个Object
,并且该Object
将自动转换为setter
- setText(CharSequence)
所需的参数类型。当可能对参数类型产生混淆时,需要在表达式中制定类型。
自定义转换器
有时转换应该在特定类型之间自动进行。 例如,在设置背景时:
<View
android:background="@{isError ? @color/red : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
在这里,background
需要一个Drawable
,但是 @color/XX
是一个整数。 每当setter
需要一个Drawable
但表达式返回一个int
时,int
应该转换为ColorDrawable
。这种转换是由BindingConversion
注解的静态方法完成的:
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
return new ColorDrawable(color);
}
注意,== 转换只发生在setter
层次,因此,表达式的返回值不允许使用混合类型 ==,如下所示:
<View
android:background="@{isError ? @drawable/error : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
Android Studio支持数据绑定
Android Studio支持许多用于数据绑定代码的代码编辑功能。 例如,它支持数据绑定表达式的以下特性:
- 语法高亮
- 表达语言语法错误的标记
- XML代码自动补全
预览窗口(即Preview
)显示数据绑定表达式的默认值(如果提供)。 在以下示例中,在布局XML文件中, 在预览窗口中显示TextView
的默认文本值 PLACEHOLDER
。
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName, default=PLACEHOLDER}"/>
如果需要在项目的开发阶段显示默认值,则还可以使用属性tools:text
而不是默认表达式值,