本文档介绍了如何使用DataBinding库来编写声明式布局,并尽量减少绑定应用程序逻辑和布局所需的中间代码。
DataBinding库提供了灵活性和广泛的兼容性 - 这是一个支持库,所以您可以在Android 2.1(API级别7+)之后的所有Android平台上使用它。
要使用数据绑定,Gradle 1.5.0-alpha1或更高版本的Android插件是必需的。请参阅如何为Gradle更新Android插件。
构建环境
要开始使用数据绑定,请从Android SDK管理器的支持库中下载DataBinding库。
要配置应用程序以使用数据绑定,请将dataBinding
元素添加到应用程序模块(module)的build.gradle
文件中。
使用下面的代码片段来配置数据绑定:
android { .... dataBinding { enabled = true } }
如果您的应用程序模块依赖了使用数据绑定的库,则您的应用程序模块也必须在其build.gradle
文件中配置数据绑定。
另外,请确保您使用的是Android Studio的兼容版本。 Android Studio 1.3及更高版本支持数据绑定,如Android Studio数据绑定支持中所述。
数据绑定编译器V2
3.1.0 Canary 6版本的Android Gradle插件附带一个可选的新编译器。要开始使用它,请更新您的gradle.properties
文件以包含以下行:
android.databinding.enableV2=true
在编译器v2中:
ViewBinding
类是在java编译器之前由Android Gradle插件生成的。这可以避免由于不相关的原因使得java编译失败进而导致过多误报的错误。在V1中,编译应用程序时会重新生成库的绑定类(以共享生成的代码并访问最终的
BR
和R
文件)。在V2中,库保持其生成的绑定类以及映射器信息,这显著提高了多模块项目的数据绑定性能。
请注意,这个新的编译器是向后不兼容的,所以用v1编译的库不能被v2使用,反之亦然。
V2还会删除一些很少使用的功能来允许这些更改:
在V1中,一个应用程序能够提供绑定适配器,可以覆盖依赖项中的适配器。在V2中,它只会在您自己的模块/应用程序及其依赖项中生效。
以前,如果一个布局文件在两个或多个不同的资源配置中包含一个
View
具有相同id但不同类的数据,则数据绑定将查找最常见的父类。在V2中,当配置之间的类型不匹配时,它将始终默认为View
。在V2中,不同的模块不能在清单文件中使用相同的包名,因为数据绑定将使用该包名来生成绑定映射类。
数据绑定布局文件
编写您的第一套数据绑定表达式
数据绑定布局文件稍有不同,从布局的根标签开始,后跟数据元素和视图根元素。这个视图根元素跟非绑定式布局文件的根元素一样。示例文件如下所示:
<?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
)描述了一个可能在此布局中使用的属性。
<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}"/>
数据对象
现在让我们假设您有一个面向User
的简单的Java对象(POJO):
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
变量)的所有绑定保存到布局的视图中,并知道如何为绑定表达式赋值。创建绑定的最简单方法是在填充布局时进行:
@Overrideprotected 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); }
您完成了!运行应用程序,您会看到UI中的测试用户。或者,您可以通过以下方式获取视图:
MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater());
如果您在ListView
或RecyclerView
适配器内使用数据绑定项目,则可能更愿意使用:
ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);//orListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);
错误处理
数据绑定允许您编写表达式来处理从视图中分发的事件(例如onClick
)。除少数例外,事件属性名称由监听器方法的名称来管理。例如,View.OnLongClickListener
onLongClick()
有一个onLongClick()
方法,所以这个事件的属性是android:onLongClick
。处理事件有两种方法。
方法引用: 在您的表达式中,您可以引用符合监听器方法签名的方法。当表达式评估为方法引用时,数据绑定将方法引用和所有者对象包装在监听器中,并将该监听器设置在目标视图上。如果表达式得出的值为null,则数据绑定不会创建监听器,而是设置空监听器。
监听器绑定: 这些是在事件发生时被计算的lambda表达式。数据绑定总是创建一个监听器,它在视图上设置。事件分发时,监听器计算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>
请注意,表达式中方法的签名必须与监听器对象中方法的签名完全匹配。
监听器绑定
监听器绑定是事件发生时运行的绑定表达式。它们类似于方法引用,但是它们允许您运行任意的数据绑定表达式。此功能适用于Gradle 2.0版及更高版本的Android Gradle插件。
在方法引用中,方法的参数必须与事件监听器的参数匹配。在监听器绑定中,只有你的返回值必须与监听器的期望返回值相匹配(除非它预期为void)。例如,您可以有一个具有以下方法的演示者(presenter)类:
public class Presenter { public void onSaveClick(Task task){} }
然后,您可以将click事件绑定到您的类,如下所示:
<?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表达式表示。在表达式中使用回调函数时,数据绑定会自动为事件创建必要的监听器和注册表。当视图触发事件时,数据绑定将计算给定的表达式。就像在常规的绑定表达式中一样,当这些监听器表达式被计算的时候,你仍然可以获得null和数据绑定的线程安全性。
请注意,在上面的例子中,我们没有定义传入onClick(android.view.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)}"
如果由于null对象导致无法计算表达式,数据绑定将返回该类型的默认Java值。例如,null
用于引用类型,0
用于int
类型, false
用于boolean
类型等。
如果您需要使用谓词(例如三元)表达式,则可以将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
元素。这些就像在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
。导入的类型可以用作变量和表达式中的类型引用:
<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尚未处理导入,因此导入变量的自动填充可能无法在您的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
元素描述可以在布局上设置的属性,以用于布局文件中的绑定表达式。
<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集合,那么这个类型应该被描述出来。如果变量没有实现Observable* 接口的基类或接口,变量将不会被检查!
当不同的配置文件(例如横向或纵向)有不同的布局文件时,变量将被合并。这些布局文件之间不得存在冲突的变量定义。
生成的绑定类将为每个描述的变量设置一个setter
和getter
。变量将采用默认的Java值,直到setter
被调用 - null
用于引用类型,0
用于int
类型, false
用于boolean
类型等。
根据需要生成一个名为context
的特殊变量用于绑定表达式。context
值来自根视图的getContext()
得到的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>
包含
通过在属性中使用应用程序命名空间和变量名称,变量可以从包含的布局传递到容器的布局的绑定中:
<?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
变量 。
数据绑定不支持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>
表达式语言
共同特征
表达式语言看起来很像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
明确的泛型调用
空合并运算符
null合并运算符(??)选择左边(如果不是null)或右边(如果为空)的操作。
android:text="@{user.displayName ?? user.lastName}"
这在功能上等同于:
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
属性引用
第一种已经在上面的编写您的第一个数据绑定表达式中讨论过了:简短形式的JavaBean引用。当一个表达式引用一个类的属性时,它对字段,getter方法和Observable字段使用相同的格式。
android:text="@{user.lastName}"
避免NullPointerException
生成的数据绑定代码自动检查空值并避免空指针异常。例如,在表达式中@{user.name}
,如果user
为null
,user.name
将被赋予其默认值(null
)。如果你是引用user.age
,年龄是一个int
,那么它将默认为0。
集合
常见的集合:数组,列表,稀疏列表(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 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 |
数据对象
任何简单的Java对象(POJO)都可以用于数据绑定,但修改POJO不会导致UI更新。当数据改变的时候,您的数据对象能够发出通知,这才是数据绑定的威力。有三种不同的数据更改通知机制: Observable对象,observable字段, 和observable集合.
当这些observable数据对象之一被绑定到UI并且数据对象的属性改变时,UI将被自动更新。
Observable对象
实现Observable
接口的类将允许附加单个监听器到绑定对象,以监听该对象上所有属性的更改。
Observable
接口具有添加和删除监听器的机制,但通知由开发者决定。为了简化开发,创建了一个基类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); } }
在编译期间Bindable
注解会在BR类中生成一个条目。BR类文件将在模块包中生成。如果数据类的基类不能改变,那么Observable接口可以使用Observable
interface may be implemented using the convenient PropertyChangeRegistry
帮助类来是实现,以用于存储和高效地通知监听器。
ObservableField
一个小的工作是参与创建Observable
类,所以想要节约时间的开发者可能有少数几个属性会用到ObservableField
和它的同胞类ObservableBoolean
, ObservableByte
,ObservableChar
, ObservableShort
,ObservableInt
, ObservableLong
,ObservableFloat
, ObservableDouble
,和ObservableParcelable
。ObservableFields
是具有单个字段的独立observable对象。原始版本在访问操作期间避免装箱和取消装箱。要使用,请在数据类中创建一个公共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();
Observable集合
一些应用程序使用更加动态化的结构来保存数据。Observable集合允许对这些数据对象进行键存取。当键是String等引用类型时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"/>
当键是一个整数时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"/>
生成的绑定
生成的绑定类将布局变量与布局中的视图链接起来。如前所述,绑定的名称和包可能是自定义的。生成的绑定类全部继承自ViewDataBinding
。
创建
应该在填充布局之后立即创建绑定类,以确保在将带有表达式的布局绑定到视图之前,View的层级不会被打乱。有几种方法可以绑定到布局。最常见的是使用Binding类中的静态方法。inflate
方法填充了View
的层级,并且一步就可以绑定到布局。有一个更简单的版本,只需要一个LayoutInflater
和一个 ViewGroup
:
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater); MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater, viewGroup, false);
如果布局是使用不同的机制填充的,那么它可能会分开绑定:
MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);
有时不能预先知道绑定。在这种情况下,绑定可以使用DataBindingUtil
类来创建:
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId, parent, attachToParent); ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId);
带有ID的视图
将在布局中为每个带ID的视图生成一个public
的final
字段。该绑定在View的层级上执行一次扫描,提取带有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>
将会生成一个绑定类:
public final TextView firstName;public final TextView lastName;
ID的必要性比没有数据绑定时小,但是仍然有一些情况下需要通过代码访问视图。
变量
每个变量都会有访问的方法。
<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>
将会在绑定中生成setter和getter:
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);
ViewStubs
ViewStub
和普通的视图有些不同。它们一开始是不可见的,当他们变得可见或者被明确告知需要填充时,它们会填充另一个布局来取代自己的布局。
由于ViewStub
本质上在View的层级上是消失的,所以绑定对象中的视图也必须消失以允许收集。由于视图(View)是final类型的,所以用一个ViewStubProxy
来代替了ViewStub
,让开发者在ViewStub
存在的时候可以访问它,并且在ViewStub
填充的时候也可以访问被填充的视图层级。
在填充另一个布局的时候,必须为新的布局建立一个绑定。因此,ViewStubProxy
必须监听ViewStub
的ViewStub.OnInflateListener
并且在那个时候建立绑定。由于只能存在一个ViewStub.OnInflateListener
,所以ViewStubProxy
允许开发者对它设置一个OnInflateListener
,这样就会在建立绑定后调用这个监听。
高级绑定
动态变量
有时并不知道特定的绑定类是什么。例如,RecyclerView.Adapter
正对任意布局的操作将不知道具体的绑定类。它仍然必须在onBindViewHolder(VH, int)
的过程中分配绑定值。
在这个例子中,RecyclerView
绑定的所有布局都有一个“item”变量。BindingHolder
有一个getBinding
方法返回ViewDataBindingViewDataBinding
。
public void onBindViewHolder(BindingHolder holder, int position) { final T item = mItems.get(position); holder.getBinding().setVariable(BR.item, item); holder.getBinding().executePendingBindings(); }
即时绑定
当变量或observable变化时,绑定将被安排在下一帧之前改变。但有时候,绑定必须立即执行。要强制执行,请使用executePendingBindings()
方法。
后台线程
只要不是集合,就可以在后台线程中更改数据模型。数据绑定将在计算时本地化每个变量/字段,以避免任何并发问题的出现。
属性Setter
每当绑定值发生变化时,生成的绑定类必须使用绑定表达式在视图上调用setter方法。数据绑定框架可以自主选择调用哪个方法来设置值。
自动化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跟属性名不匹配。对于这些方法,一个属性可以通过BindingMethods
注解与setter相关联。每个重命名的方法的属性必须与一个类相关联,并包含BindingMethod
注解。例如,android:tint
属性确实与setImageTintList(ColorStateList)
关联,而不是setTint
。
@BindingMethods({ @BindingMethod(type = "android.widget.ImageView", attribute = "android:tint", method = "setImageTintList"), })
开发者不太可能需要重命名setter; android框架的属性已经实现了。
自定义Setter
一些属性需要自定义绑定逻辑。例如,该android:paddingLeft
属性没有关联的setter 。取而代之的是setPadding(left, top, right, bottom)
。带BindingAdapter
注解的静态绑定适配器方法允许开发者自定义如何调用属性的setter。
Android属性已经创建了BindingAdapter
。例如,这里是一个用于设置paddingLeft
的例子:
@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,则将调用此适配器。
This adapter will be called if both imageUrl and error are used for an ImageView and imageUrl is a string and error is a 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); } } }
上面的例子比正常情况稍微复杂,因为视图在监听器中使用add和remove来代替View.OnAttachStateChangeListener
中的set的方法。android.databinding.adapters.ListenerUtil
类可以帮助跟踪以前的监听器,让他们可以在绑定Adaper时被移除。
通过给OnViewDetachedFromWindow
和OnViewAttachedToWindow
接口添加@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
注解,数据绑定代码生成器就知道只在Honeycomb MR1和新设备生成监听器,由addOnAttachStateChangeListener(View.OnAttachStateChangeListener)
支持相同的版本。
转换器
对象转换
从绑定表达式返回一个对象时,将从自动化,重命名和自定义setter中选择一个setter。该对象将被转换为所选setter的参数类型。
这对于那些使用ObservableMaps
来保存数据的人来说是很方便的。例如:
<TextView android:text='@{userMap["lastName"]}' android:layout_width="wrap_content" android:layout_height="wrap_content"/>
userMap
返回一个对象,并且对象会自动转换为在setText(CharSequence)
中发现的参数类型。当参数类型可能混淆时,开发者需要在表达式中进行强制转换。
自定义转换
有时转换应该在特定类型之间自动进行。例如,设置背景时:
<View android:background="@{isError ? @color/red : @color/white}" android:layout_width="wrap_content" android:layout_height="wrap_content"/>
在这里,背景需要一个Drawable
,但颜色是一个整数。每当Drawable
是期望值但返回一个整数时,int
应该被转换成一个ColorDrawable
。这个转换是通过一个带有BindingConversion
注解的静态方法完成的:
@BindingConversionpublic 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支持数据绑定代码的许多代码编辑功能。例如,它支持数据绑定表达式的以下功能:
注意:在没有错误的时候,数组和泛型类型(如Observable
类)可能会显示错误。
Preview窗格显示数据绑定表达式的默认值(如果提供的话)。在下面的示例中摘录了布局XML文件中的一个元素,Preview窗格将显示PLACEHOLDER
默认的文本值TextView
。
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user.firstName, default=PLACEHOLDER}"/>
如果您需要在项目设计阶段显示默认值,还可以使用tools
属性而不是默认表达式值,如设计时的布局属性中所述。
共同学习,写下你的评论
评论加载中...
作者其他优质文章