前几天小试牛刀写了一篇 Android DataBinding 初探,只是简单的介绍了一下 DataBinding 的几个小问题,并没有特别详细的去介绍 DataBinding 的更多方法,这几天看了一下 DataBinding 的官网的相关内容,觉得有必要把官网的用法记录一下,用来参考及以后使用时的参考,以前大家很多人都使用过注解框架,包括 Jake Wharton 大神的 ButterKnife,但 DataBinding 出来后,相信会对此类的框架形成碾压,毕竟是 Google 的官方出品,接下来我们切入正题了,开始详细的去介绍这个 Android DataBinding Library,先上一张概况图:
一、构建环境(Build Environment)
android {
....
dataBinding {
enabled = true
}
}
1) DataBinding 表达式
数据绑定的布局文件和我们以前经常写的布局文件稍有不同,并从布局的根标记开始,后面依次是数据元素和视图根元素,即根布局是 layout,接下来是 data 节点,variable 节点,示例如下:
数据中的用户变量描述此布局中可能使用的属性:
布局中的表达式使用 "@{}" 语法在属性中写入,在这里,TextView 的文本设置为用户 FirstName 属性:
2)数据对象(Data Object)假设你现在有一个普通的 Java 对象(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} 将访问前者 User 对象中的 firstName 和后者 JavaBeans 对象中的 getFirstName 方法
3)数据绑定(Binding Data)
默认情况下,绑定类将根据 layout 文件的名称生成,首字母大写的命名规范,并添加 "Binding" 后缀,上述的布局文件是main_activity.xml,所以生成类是 MainActivityBinding, 该类将布局属性(例如 User 变量)的所有绑定保存到布局视图中,并知道如何为绑定表达式赋值,创建最简单的方法是在 inflating 时绑定,如下:
@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);
}
就这样,运行应用程序,你将会在 UI 中看到 Test User,或者你可以通过如下获取 View:
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);
4)事件处理(Event Handing)
数据绑定允许你编写表达式处理,如 onClick 事件,有少数例外事件属性的名称受监听方法名称的约束,例如View.OnLongClickListener 有一个 onLongClick 方法,所以此事件的属性是:Android:onLongClick,它有两种处理事件的方法:
方法引用:事件可以直接绑定到处理程序的方法,类似于 Android:onClick,相比来看 View#onClick 属性更重要的优势是,表达式在编译的时候处理的,所以如果方法不存在或签名是不正确的,你将会在编译时出错,方法引用和监听器绑定之间的主要区别是,当数据绑定时,实际的监听器实现将创建,而不是在触发时,如果你希望事件发生时对表达式进行审核,则应该使用监听器绑定,若将事件分配给其处理程序,请使用常规绑定表达式,该值是要调用的方法名,例如,如果你的数据对象有两种方法:
public class MyHandlers {
public void onClickFriend(View view) { ... }
}
绑定表达式可以为视图分配单击监听器:
注意表达式中的方法的签名必须与监听对象中的方法的签名完全匹配
public class Presenter {
public void onSaveClick(Task task){}
}
然后你可以在你的 xml 文件中将点击事件绑定如下:
监听器由 Lambda 表达式表示,这些表达式只允许作为表达式的根元素,当表达中使用回调时,数据绑定会自动创建时间的必要监听器和寄存器,当视图触发事件时,数据绑定将审核给定的表达式,与常规绑定方式一样,在审核这些监听器表达式时,任然可以得到数据绑定的 null 和线程安全性
请注意,在上面的例子中,我们还没有定义 View 的参数,通过 onClick(Android.view.View)Listener 绑定为监听器参数提供两种选择:你可以忽略所有方法参数或命名所有参数,如果你更喜欢命名参数,则也可以在表达式中使用它们,例如,上面的表达式可以写成:
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){}
}
如果监听的事件返回类型无效的值,则表达式必须返回相同类型的值。例如,如果要监听长点击事件,则表达式应返回布尔值:
public class Presenter {
public boolean onLongClick(View view, Task task){}
}
android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"
你还可以使用三元表达式:
android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"
避免复杂的监听:
三、布局细节(Layout Details)
1)imports
可以在数据元素内使用零个或多个导入元素。这些可以参考在你的布局文件的类,就像在 java:
现在,视图可以在绑定表达式中使用:
当有类名冲突时,其中一个类可以重命名为“alias:”,如下:
现在,Vista 可以用来参考的 com.example.real.estate.view 和视图可以用来参考 android.view.view 在布局文件中,导入类型可以用作变量和表达式中的类型引用:
注意:Android Studio 还不能很好的兼容支持,变量可能不能在 IDE 中完成自动提示功能。但是你的应用程序将仍然可以编译,你可以通过使用完全限定名称来定义变量解决 IDE 的问题:
在表达式中引用静态字段和方法时也可以使用导入类型:
…
就像在 java 语言,java * 自动导入
2)Variables
可以在数据元素内使用任意数量的变量元素。每个变量元素描述可以在布局文件中用于绑定表达式中的布局的属性:
3)自定义绑定类名(Custom Binding Class Names)
默认情况下,绑定类是基于布局的文件名生成,开始用大写,去掉下划线 "_",然后加后缀 "Binding",这个类将会被放置在一个绑定包的模块包下,例如,布局文件是 contant_item.xml 将生成 ContactItemBinding,如果模块封装为 com.example.my.app,那么它将被放置在 com.example.my.app.databinding
通过调整数据元素的类属性可以将绑定类重命名或放置在不同的包中。例如:
...
这个生成绑定类中的模块封装在数据绑定包 ContactItem,如果类产生在不同的包中的模块封装内,它可能会加 ".",如下:
...
在这种情况下,ContactItem 直接在模块封装生成,如果提供完整的包,任何包可以使用:
...
4)Includes
通过使用应用程序命名空间和属性中的变量名,可以将变量从包含布局中传递到包含布局中:
在这里,必须有两 name.xml 和 contact.xml 布局文件的用户变量
数据绑定不支持包括合并元素的直接子项。例如,不支持下列布局:
常用表达式给 Java 表达式很像,如下:
+ - / * %"
instanceof
null
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}"
android:text="@{user.lastName}"
避免空指针(Avoiding NullPointerException)
…
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)}"
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);
}
}
private static class User {
public final ObservableField firstName =
new ObservableField<>();
public final ObservableField lastName =
new ObservableField<>();
public final ObservableInt age = new ObservableInt();
}
user.firstName.set("Google");
int age = user.age.get();
ObservableArrayMap user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);
…
ObservableArrayList 用于键是整数:
ObservableArrayList
在布局中,可以通过索引访问 List:
…
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater);
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater, viewGroup, false);
如果 layout 使用不同的机制 inflate,则可以单独绑定:
MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId,
parent, attachToParent);
ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId);
将生成一个 Binding 类:
public final TextView firstName;
public final TextView lastName;
它会在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);
public void onBindViewHolder(BindingHolder holder, int position) {
final T item = mItems.get(position);
holder.getBinding().setVariable(BR.item, item);
holder.getBinding().executePendingBindings();
}
@BindingMethods({
@BindingMethod(type = "android.widget.ImageView",
attribute = "android:tint",
method = "setImageTintList"),
})
上面的例子,开发者不太可能重命名编译程序,Android 框架属性已经实现了
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
Binding 适配对其他定制类型非常有用,例如,自定义 loader 可以用异步载入图像
@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);
}
@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);
}
}
}
@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);
}
}
}
在 userMap 返回一个对象并且该对象将自动转换为 setText(CharSequence) 的参数类型,当有关参数类型可能混乱,开发人员需要在表达式中转换
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
return new ColorDrawable(color);
}