(三、3)布局和绑定表达式
表达式语言允许您编写处理视图调度的事件的表达式。 数据绑定库自动生成将布局中的视图与数据对象绑定所需的类。
数据绑定布局文件略有不同,以布局的根标签开头,后跟数据元素和视图根元素。 此视图元素是您的根在非绑定布局文件中的位置。 以下代码显示了一个示例布局文件:
<?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>
数据中的user变量描述了可在此布局中使用的属性。
<variable name="user" type="com.example.User" />
布局中的表达式使用“@ {}”语法写入属性属性中。 这里,TextView文本设置为用户变量的firstName属性:
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}" />
注意:布局表达式应保持小而简单,因为它们不能进行单元测试并且IDE支持有限。 为了简化布局表达式,您可以使用自定义绑定适配器。
一、数据对象
我们现在假设您有一个普通的对象来描述User实体:
public class User {
public final String firstName;
public final String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
这种类型的对象具有永不改变的数据。 在应用程序中,通常会读取一次并且之后不会更改的数据。 也可以使用遵循一组约定的对象,例如Java中的访问器方法的使用,如以下示例所示:
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;
}
}
从数据绑定的角度来看,这两个类是等价的。 用于android:text属性的表达式@ {user.firstName}访问前一类中的firstName字段和后一类中的getFirstName()方法。 或者,如果该方法存在,它也会解析为firstName()。
二、绑定数据
为每个布局文件生成绑定类。 默认情况下,类的名称基于布局文件的名称,将其转换为Pascal大小写并向其添加Binding后缀。 上面的布局文件名是activity_main.xml
,因此相应的生成类是ActivityMainBinding。 此类包含布局属性(例如,用户变量)到布局视图的所有绑定,并知道如何为绑定表达式指定值。创建绑定的推荐方法是在扩展布局时执行此操作,如 如下例所示:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
User user = new User("Test", "User");
binding.setUser(user);
}
在运行时,应用程序在UI中显示Test用户。 或者,您可以使用LayoutInflater获取视图,如以下示例所示:
ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());
如果在Fragment,ListView或RecyclerView适配器中使用数据绑定项,则可能更喜欢使用绑定类或DataBindingUtil类的inflate()方法,如以下代码示例所示:
ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
// or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);
三、表达语言
共同特征
表达式语言看起来很像托管代码中的表达式。 您可以在表达式语言中使用运算符和关键字:
例子:
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
缺少操作
您可以在托管代码中使用的表达式语法中缺少以下操作:
- this
- super
- new
- 显式通用调用
空结合运算符
空合并运算符(??)选择左操作数(如果它不为空)或右(如果前者为空)。
android:text="@{user.displayName ?? user.lastName}"
这在功能上等同于:
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
属性参考
表达式可以使用以下格式引用类中的属性,这对于fields,getters和ObservableField对象是相同的:
android:text="@{user.lastName}"
避免空指针异常
生成的数据绑定代码会自动检查空值并避免空指针异常。 例如,在表达式@ {user.name}
中,如果user为null,则为user.name分配其默认值null。 如果引用user.age,其中age的类型为int,则数据绑定使用默认值0。
集合
为方便起见,可以使用[ ]运算符访问公共集合,例如数组,列表,稀疏列表和映射。
<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]}"
注意:您还可以使用object.key表示法引用地图中的值。 例如,上面示例中的@ {map [key]}可以替换为@ {map.key}。
字符串文字
您可以使用单引号括起属性值,这允许您在表达式中使用双引号,如以下示例所示:
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)}"
某些资源需要显式类型评估,如下表所示:
略。
四、事件处理
数据绑定允许您编写从视图调度的表达式处理事件(例如,onClick()方法)。 事件属性名称由侦听器方法的名称确定,但有一些例外。 例如,View.OnClickListener有一个onClick()方法,因此该事件的属性是android:onClick。
click事件有一些专门的事件处理程序需要除android:onClick以外的属性以避免冲突。 您可以使用以下属性来避免这些类型的冲突:
您可以使用以下机制来处理事件:
- 方法参考:在表达式中,您可以引用符合侦听器方法签名的方法。 当表达式求值为方法引用时,Data绑定将方法引用和所有者对象包装在侦听器中,并在目标视图上设置该侦听器。 如果表达式求值为null,则数据绑定不会创建侦听器并改为设置空侦听器。
- 侦听器绑定:这些是在事件发生时计算的lambda表达式。 数据绑定总是创建一个侦听器,它在视图上设置。 调度事件时,侦听器会计算lambda表达式。
方法参考
事件可以直接绑定到处理程序方法,类似于android:onClick的方式可以分配给活动中的方法。 与View onClick属性相比,一个主要优点是表达式在编译时处理,因此如果该方法不存在或其签名不正确,则会收到编译时错误。
方法引用和侦听器绑定之间的主要区别在于,实际的侦听器实现是在绑定数据时创建的,而不是在触发事件时创建的。 如果您希望在事件发生时评估表达式,则应使用侦听器绑定。
要将事件分配给其处理程序,请使用普通绑定表达式,其值为要调用的方法名称。 例如,请考虑以下示例布局数据对象:
public class MyHandlers {
public void onClickFriend(View view) { ... }
}
绑定表达式可以将视图的单击侦听器分配给onClickFriend()方法,如下所示:
<?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插件提供此功能。
在方法引用中,方法的参数必须与事件侦听器的参数匹配。 在侦听器绑定中,只有您的返回值必须与侦听器的预期返回值匹配(除非它期望无效)。 例如,考虑以下具有onSaveClick()方法的presenter类:
public class Presenter {
public void onSaveClick(Task task){}
}
然后,您可以将click事件绑定到onSaveClick()
方法,如下所示:
<?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>
在表达式中使用回调时,数据绑定会自动创建必要的侦听器并为事件注册它。 当视图触发事件时,数据绑定会评估给定的表达式。 与常规绑定表达式一样,在评估这些侦听器表达式时,仍然可以获得数据绑定的null和线程安全性。
上面的示例中,我们尚未定义传递给onClick(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的值,则表达式也必须返回相同类型的值。 例如,如果要监听长按事件,则表达式应返回布尔值。
public class Presenter {
public boolean onLongClick(View view, Task task) { }
}
android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"
如果由于null对象而无法计算表达式,则数据绑定将返回该类型的默认值。 例如,引用类型为null,int为0,布尔值为false等。
如果需要使用带谓词的表达式(例如,三元),则可以使用void作为符号。
android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"
避免复杂的听众
监听器表达式非常强大,可以使您的代码非常容易阅读。 另一方面,包含复杂表达式的侦听器使您的布局难以阅读和维护。 这些表达式应该像将UI中的可用数据传递给回调方法一样简单。 您应该在从侦听器表达式调用的回调方法中实现任何业务逻辑。
五、引用,变量和包含
数据绑定库提供导入,变量和包含等功能。 导入使布局文件中的类很容易引用。 变量允许您描述可用于绑定表达式的属性。 包括让您在整个应用中重复使用复杂的布局。
引用
导入允许您轻松引用布局文件中的类,就像在托管代码中一样。 可以在数据元素内使用零个或多个导入元素。 以下代码示例将View类导入布局文件:
<data>
<import type="android.view.View"/>
</data>
导入View类允许您从绑定表达式中引用它。 以下示例显示如何引用View类的VISIBLE和GONE常量:
<TextView
android:text="@{user.lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
输入别名
当存在类名冲突时,可以将其中一个类重命名为别名。 以下示例将com.example.real.estate包中的View类重命名为Vista:
<import type="android.view.View"/>
<import type="com.example.real.estate.View"
alias="Vista"/>
您可以使用Vista来引用com.example.real.estate.View,View可以用来引用布局文件中的android.view.View。
导入其他类
导入的类型可以用作变量和表达式中的类型引用。 以下示例显示用作变量类型的User和List:
<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问题。
您还可以使用导入的类型来转换表达式的一部分。 以下示例将连接属性强制转换为User类型:
<TextView
android:text="@{((User)(user.connection)).lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
在表达式中引用静态字段和方法时,也可以使用导入的类型。 以下代码导入MyStringUtils类并引用其capitalize方法:
<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.lang。*会自动导入。
变量
您可以在数据元素中使用多个变量元素。 每个变量元素描述可以在布局上设置的属性,以在布局文件中的绑定表达式中使用。 以下示例声明用户,图像和注释变量:
<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接口的基类或接口,则不会观察变量。
当存在用于各种配置的不同布局文件(例如,横向或纵向)时,组合变量。 这些布局文件之间不得存在冲突的变量定义。
生成的绑定类对于每个描述的变量都有一个setter和getter。 变量采用默认的托管代码值,直到为引用类型调用setter-null,0表示int,false表示布尔值等。
根据需要生成名为context的特殊变量以用于绑定表达式。 context的值是来自根View的getContext()方法的Context对象。 上下文变量由具有该名称的显式变量声明覆盖。
包括
通过使用app命名空间和属性中的变量名,变量可以从包含的布局传递到包含的布局绑定中。 以下示例显示了name.xml和contact.xml布局文件中包含的用户变量:
<?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>
数据绑定不支持include作为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><!-- Doesn't work -->
<include layout="@layout/name"
bind:user="@{user}"/>
<include layout="@layout/contact"
bind:user="@{user}"/>
</merge>
</layout>