DataBinding是谷歌官方发布的一个框架,它的目的是降低布局和逻辑的耦合性,使代码的逻辑更清晰。它能够很简单的省去findViewById()
的步骤,大量减少Activity
的代码,数据直接能写在layout
文件上,而且它能自动进行空检测,很多地方对象为空不会引起空指针异常。
下面我将从以下几个方面介绍DataBinding框架:
1. DataBinding在AndroidStudio下的环境搭建
2. DataBinding的简单使用
3. DataBinding的事件处理
4. layout文件细节
5. 观察者对象
6. 生成Binding
7. 属性setters
由于DataBinding是谷歌的官方框架,所以环境搭建很简单,只需在model下的build.gradle文件上加上如下代码:
android {
···
dataBinding {
enabled = true
}
···
}
不过这要求你的Gradle是 1.5.0-alpha1或者更新的版本,AndroidStudio1.3或更新的版本才可以。DataBinding是一个兼容库,他能运行在Android2.1以上(Api level7+)。
DataBinding的layout
文件与我们一般写的layout
文件不一样,它包含数据和视图两方面,所以其的layout
文件如下:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:context="com.example.databindingtest.MainActivity">
<data>
<variable
name="user"
type="com.example.databindingtest.User"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<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
是一个java对象,它是被用于这个layout
的一个变量。
User
类如下
public class User
{
public String firstName;
public String lastName;
public User(String firstName, String lastName)
{
this.firstName = firstName;
this.lastName = lastName;
}
}
当在layout
文件中写@{user.firstName}
时,会使用user
对象的firstName
属性。
一个用DataBinding的方式写出的layout
文件会产生一个类,类名为layout
文件名的驼峰式写法加上Binding
,所以activity_main.xml
会对应于ActivityMainBinding
类,这个类包含了layout
文件的性能,包括数据和视图两部分,可以用如下方式绑定Activity
和布局文件:
DataBindingUtil.setContentView(MainActivity.this, R.layout.main_activity);
完整的Activity文件如下:
public class MainActivity extends AppCompatActivity
{
private ActivityMainBinding mBinding;
private User mUser;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
mUser = new User("李", "晓峰");
mBinding.setUser(mUser);
mBinding.setHandler(new MyHandlers());
mBinding.setPresenter(new Presenter());
}
}
运行效果:
如此就完成了DataBinding的最简单体验。
也可以把User类写成这样:
public class User
{
public String getFirstName()
{
return firstName;
}
public void setFirstName(String firstName)
{
this.firstName = firstName;
}
public String getLastName()
{
return lastName;
}
public void setLastName(String lastName)
{
this.lastName = lastName;
}
private String firstName;
private String lastName;
public User(String firstName, String lastName)
{
this.firstName = firstName;
this.lastName = lastName;
}
}
此时布局文件不能直接调用user
的firstName
属性,如果你Ctrl+左键点击ActivityMainBinding
类,会跳到对应的布局文件,这也就能理解为什么布局文件不能访问user
的除public
权限外的其余的属性和方法了。
但是其实布局文件不用做任何修改,也能达到同样的效果,这是因为@{user.firstName}
会去调用user
的getFirstName()
方法,也可以把布局文件写成@{user.getFirstName}
,将会直接调用getFirstName()
方法。
如果布局文件写成@{user.firstName}
,User
类同时包含firstName()
方法和getFirstName()
方法,会优先调用哪个方法呢?
将User
类改成
public class User
{
···
public String getFirstName()
{
Log.e("User", "getFirstName");
return firstName;
}
public String firstName()
{
Log.e("User", "firstName");
return "12132";
}
···
}
打印日志如下:
com.example.databindingtest E/User: getFirstName
所以说getFirstName()
方法被调用了,而firstName()
方法并没有被调用。
DataBinding允许我们直接将事件写在控件上,事件的属性名决定于Listener
的方法名,例如长按事件View.OnLongClickListener
有方法onLongClick()
,那么其对应的属性为android:onLongClick
。
有两种处理事件的方式:
– Method References 事件为一个对象及其的方法
– Listener Bindings 事件为一个Lambda表达式
Method References设置事件和使用android:onClick
属性,将方法写在Activity
是很相似的,主要的区别是DataBinding的Method References对表达式的检验是在编译期,因此如果方法为空或者不正确,会在编译期被发现。且它的方法不必写在Activity
里。
事件处理类:
public class MyHandlers
{
public void click(View view)
{
Log.e("MyHandlers","onClick");
}
public boolean longClick(View view)
{
Log.e("MyHandlers","onLongClick");
return true;
}
}
注意,方法的输入参数和返回值必须和事件监听器的方法一样。
xml
文件:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:context="com.example.databindingtest.MainActivity">
<variable
name="handler"
type="com.example.databindingtest.MyHandlers"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{handler::click}"
android:onLongClick="@{handler::longClick}"
android:text="button"/>
</LinearLayout>
</layout>
Activity
加上
mBinding.setHandler(new MyHandlers());
分别点击和长按按钮,打印日志如下:
com.example.databindingtest E/MyHandlers: onClick
com.example.databindingtest E/MyHandlers: onLongClick
事件处理类:
public class Presenter
{
public void click()
{
Log.e( "Presenter"," onClick");
}
public boolean longClick()
{
Log.e( "Presenter","onLongClick");
return true;
}
}
xml
文件:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:context="com.example.databindingtest.MainActivity">
<data>
<variable
name="presenter"
type="com.example.databindingtest.Presenter"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{()->presenter.click()}"
android:onLongClick="@{()->presenter.longClick()}"
android:text="button"/>
</LinearLayout>
</layout>
Activity
加上
mBinding.setPresenter(new Presenter());
日志如下:
com.example.databindingtest E/Presenter: onClick
com.example.databindingtest E/Presenter: onLongClick
所以我们在使用MVP模式的时候,就可以不必去Activity
里去绑定控件的事件与Presenter
里的方法了。
在这里我们没有传递view
属性到View.onClick
方法中, Listener bindings提供给我们两种选择:忽略方法的所有输入参数或者为方法的所有输入参数命名。
例如我们可以写成:
android:onClick="@{(view)->presenter.click()}"
如果我们想要在表达式中使用view,那么可以这样写:
public void click(View view)
{
if (view instanceof Button)
{
Log.e("Presenter", " onClick");
}
}
android:onClick="@{(button)->presenter.click(button)}"
表达式也可以有自己的输入参数:
public void click(Task task) android:onClick="@{()->presenter.click(task)}"
public void click(View view,Task task) android:onClick="@{(view)->presenter.click(view,task)}"
有一些点击事件与android:onClick
有冲突,DataBinding给他们分配了一些特别的属性名:
View | Listener Setter | Attribute |
---|---|---|
SearchView | setOnSearchClickListener(View.OnClickListener) | android:onSearchClick |
ZoomControls | setOnZoomInClickListener(View.OnClickListener) | android:onZoomIn |
ZoomControls | setOnZoomOutClickListener(View.OnClickListener) | android:onZoomOut |
前面我们对于Databinding的布局文件做了简单的介绍,现在详细介绍布局文件细节。
就像Java一样,类的使用可以是带包名的类名,也支持导入:
<data>
<import type="com.example.databindingtest.User"/>
<variable
name="user"
type="com.example.databindingtest.User"/>
</data>
java.lang
包下的类不需要导入,可以直接使用
<variable
name="str"
type="String"/>
在布局文件中可以直接使用类的静态变量和方法,不需要调用ViewDataBinding.setXxx()
public class StaticClass { public static String name = "StaticClass"; public static void printf(View v) { Log.e("StaticClass", "printf"); } }
<Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{StaticClass.name}" android:onClick="@{StaticClass::printf}"/>
注意,此时使用Listener Bindings引用方法会报错。
类名支持设置别名:
<import alias="V" type="android.view.View"/>
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user.lastName}" android:visibility="@{user.adult?V.VISIBLE:V.GONE}"/>
这样能防止出现类名相同的情况而造成的类名无法识别。
定义和使用集合:
<data> <import type="com.example.User"/> <import type="java.util.List"/> <variable name="userList" type="List<User>"/> </data>
<TextView android:id="@+id/name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{userList[0].firstName}"/>
其中”<”和”>”要使用Html的转义代替,此时AndroidStudio可能会爆红,可是是正确的,可以正确运行。
Map
集合与其类似
android:text="@{map["key"]}"
但此时引号有冲突了,所以将外层引号改成单引号:
android:text='@{map["key"]}'
Binding
类的类名可以自定义:
<data class="ContactItem">
...
</data>
指定包名:
<data class="com.example.ContactItem">
...
</data>
DataBinding
的表达式是支持简单的运算符的
– Mathematical (数学计算符) + – / * %
– String concatenation(字符串拼接符) +
– Logical(逻辑运算符) && ||
– Binary(位运算符) & | ^
– Unary(单目运算符) + – ! ~
– Shift(位移运算符) >> >>> <<
– Comparison(比较运算符) == > < >= <=
– instanceof
– Grouping ()
– Literals – character, String, numeric, null
– Cast
– Method calls
– Field access
– Array access []
– Ternary operator(三元运算符) ?:
基本上和Java保持一致。
举例:
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}"
注意,DataBinding
自动进行了很多空指针检测,对象为空调用它的属性或方法不会引起程序崩溃,而是赋予默认值。例如对于这个
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.lastName}"
android:onClick="@{(button)->user.click(button)}"/>
如果user
对象为空,那么user.lastName
会被分配它的默认值,对象是null
,int
是0
等等;TextView的点击事件会相当于没有设置。
资源使用
在表达式中使用xml文件定义的资源是可以的:
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
常用的资源和其对应注解如下:
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 |
在我们目前的代码中,如果对象改变了某个属性,UI
是无法自动更新的,其实很好理解,在User.setFirstName()
方法中,它只是改变了user
类的属性,而没有通知UI
层,DataBinding
已经封装好了通知UI
层的方法:
public 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);
}
}
在字段的get
方法或定义处添加@Bindable
注解,就可以在BR
类生成对应的字段,然后继承BaseObservable
类,就可以调用通知UI
层具体属性修改的方法了。
在按钮的点击事件上加上如下方法实现,就能看到UI
的更改动画:
valueAnimator = ValueAnimator.ofInt(0, 100);
valueAnimator.setDuration(10000);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener()
{
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator)
{
int animatedValue = (int) valueAnimator.getAnimatedValue();
mUser.setFirstName("李"+animatedValue);
}
});
valueAnimator.start();
凡是在layout
文件里面出现了的属性,均可以在BR
类里面找到其的对应。
ObservableFields
使用@Bindable
注解并调用notifyPropertyChanged(BR.xxx)
方法能达到自动更新UI
的目的,可是过于繁琐,所以DataBinding
为我们封装好了更简单易用的类
ObservableFields
,它的源代码很简单:
public class ObservableField<T> extends BaseObservable implements Serializable {
private T mValue;
public ObservableField(T value) {
mValue = value;
}
public ObservableField() {
}
public T get() {
return mValue;
}
public void set(T value) {
if (value != mValue) {
mValue = value;
notifyChange();
}
}
}
在每次调用set()
方法的时候,均会调用notifyChange()
方法,这个方法也是BaseObservable
提供的,效果等同于notifyPropertyChanged(BR._all)
。
使用举例:
实体类
public class Person
{
public ObservableField<String> name = new ObservableField<>();
public ObservableField<String> address = new ObservableField<>();
public ObservableInt age = new ObservableInt();
}
布局文件
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="person"
type="com.example.databindingtest.Person"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.databindingtest.SecondActivity">
<TextView
android:text='@{person.name}'
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
</layout>
改变属性的方法调用
Person person = new Person();
mBinding.setPerson(person);
person.name.set("lei");
当数据是集合时,使用ObservableArrayMap
ObservableArrayMap<String, Object> user = new ObservableArrayMap<>(); mBinding.setUser(user); user.put("firstName","1437"); user.put("lastName","dufjklsg");
<data> <variable name="user" type="android.databinding.ObservableArrayMap<String,Object>"/> </data> ··· <LinearLayout android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.example.databindingtest.SecondActivity"> <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>
在布局文件和java代码上的写法和普通集合基本一致。
如果key
不是字符串而是数字下标,则使用ObservableArrayList
和之前ArrayList
的用法在形式上基本上一致。
之前我们已经展示了一种生成Binding
类绑定Activity
的方法
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
还有另一种方式:
mBinding = ActivitySecondBinding.inflate(getLayoutInflater());
setContentView(mBinding.getRoot());
第一行生成Binding
类,第二行绑定Activity
,这种方式在使用RecyclerView
的时候会很有用,能够拿到Binding
类,还可以通过ViewDataBinding.getRoot()
获取根布局。
适配器的例子:
public class MessageAdapter extends RecyclerView.Adapter<MessageAdapter.MyViewHolder>
{
private LayoutInflater mInflater;
private ArrayList<Message> messageArrayList;
private Context context;
public MessageAdapter(ArrayList<Message> messageArrayList, Context context)
{
this.messageArrayList = messageArrayList;
this.context = context;
mInflater = LayoutInflater.from(context);
}
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
{
return MyViewHolder.create(mInflater);
}
@Override
public void onBindViewHolder(MyViewHolder holder, int position)
{
holder.mBinding.setMessage(messageArrayList.get(position));
}
@Override
public int getItemCount()
{
return messageArrayList == null ? 0 : messageArrayList.size();
}
static class MyViewHolder extends RecyclerView.ViewHolder
{
private ThirdBinding mBinding;
private MyViewHolder(ThirdBinding binding)
{
super(binding.getRoot());
mBinding = binding;
}
private static MyViewHolder create(LayoutInflater inflater)
{
ThirdBinding mBinding = ThirdBinding.inflate(inflater);
return new MyViewHolder(mBinding);
}
private void bindData(Message message)
{
mBinding.setMessage(message);
}
}
}
自动生成控件对象
在生成Binding
类的同时,DataBinding
会根据我们在布局文件中设置的id
自动生成对应的字段:
<Button
android:id="@+id/button_test"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="12312"/>
在具体的Binding
类上的字段
public final Button buttonTest;
依旧像之前自动转换成驼峰式命名,使用直接调用这个字段就可以:
mBinding.buttonTest.setText("");
这就是我们完全没必要使用findViewById()
了。
变量的set,get方法
正如我们之前所看到的那样,我们在data
标签下所申明的变量会生成对应的set
,get
方法:
<variable name="user" type="com.example.databindingtest.User"/>
mBinding.setUser(mUser); mBinding.getUser();
对于在布局文件中控件的每一个用表达式描述的属性,DataBinding
会试着寻找方法来设置属性。属性的名称空间不必匹配,仅仅根据属性名本身。例如,用表达式关联的TextView
的属性android:text
会寻找方法setText(String)
,如果表达式返回的是int
,则会寻找方法setText(int)
。如果在已给出的属性中没有某一个属性名,但是有set
方法,那么我们能很简单的设置属性。例如对于DrawerLayout
,他有方法public void setScrimColor(@ColorInt int color)
但是没有属性android:scrimColor
,我们可以自动的设置上这个属性:
<android.support.v4.widget.DrawerLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:scrimColor="@{@color/scrim}"
app:drawerListener="@{fragment.drawerListener}"/>
基于此,我们就能很简单的自定义控件,而不必去写属性值的xml
文件,例如写一个能设置头的TextView:
public class HeadTextView extends android.support.v7.widget.AppCompatTextView { public HeadTextView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } public void setStartText(String startText) { String text = startText + getText().toString(); setText(text); } }
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:bind="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" tools:context="com.example.databindingtest.MainActivity"> <data> <variable name="head" type="String"/> </data> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <com.example.databindingtest.HeadTextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="183****0038" app:startText="@{head}"/> </LinearLayout> </layout>
mBinding.setHead("电话号码:");
当然,这个自定义控件还有更多细节需要完善。
注意,app:startText
的属性值必须要是表达式,而不能像常规的那样写成app:startText="电话号码:"
,只有表达式才能引起DataBinding
的机制。不过可以写成app:startText="@{@string/phone}"
,因为表达式里是可以引用资源的。
一些属性有set
方法但是名称不匹配,可以使用BindingMethods
注解来联系起方法和属性,例如对于android:hint
属性,它对应的方法是setImageTintList(ColorStateList)
,而不是setTint
,可以在类的上面加上以下注解来完成匹配:
@BindingMethods({
@BindingMethod(type = ImageView.class,
attribute = "android:tint",
method = "setImageTintList"),
})
注意,此处官方文档写的有误。
Android的框架已经帮我们把framework
层的属性做了匹配。
对于HeadTextView
,可以改成:
@BindingMethods({
@BindingMethod(type = TextView.class,
attribute = "app:startText",
method = "setStartText111"),
})
public class HeadTextView extends android.support.v7.widget.AppCompatTextView
{
public HeadTextView(Context context, @Nullable AttributeSet attrs)
{
super(context, attrs);
}
public void setStartText111(String startText)
{
String text = startText + getText().toString();
setText(text);
}
}
我们可以自己为属性写set方法,例如对于android:paddingLeft
属性,并没有对应的方法,而有方法setPadding(left, top, right, bottom)
存在,使用BindingAdapter
注解能定制属性的set
方法。
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
之前的自定义控件可以这样修改:
public class AttrAdapter { @BindingAdapter("app:startText") public static void setHead(TextView textView, String head) { textView.setText(head + textView.getText()); } }
<import type="com.example.databindingtest.AttrAdapter"/>
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="183****0038" app:startText="@{@string/phone}"/>
也就是说我们可以在已有的控件上任意的拓展属性!
我们也可以用适配器来接受多个属性:
@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}"/>
当app:imageUrl
和app:error
两个属性都被设置了的时候,会调用loadImage
方法。
适配器里也可以接受之前的属性:
@BindingAdapter("android:paddingLeft") public static void setPaddingLeft(View view, int oldPadding, int newPadding) { Log.e("AttrAdapter", "oldPadding=" + oldPadding + " newPadding=" + newPadding); if (oldPadding != newPadding) { view.setPadding(newPadding, newPadding, view.getPaddingRight(), view.getPaddingBottom()); } }
<variable name="left" type="int"/>
<TextView android:paddingLeft="@{left}" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="183****0038" app:startText="@{@string/phone}"/>
mBinding.setLeft(10);
打印Log如下:
com.example.databindingtest E/AttrAdapter: oldPadding=0 newPadding=10
表达式的输入值可以与属性要求的值不一样,它会自动寻找要求的方法:
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user}"/>
@BindingAdapter("android:text") public static void setText(TextView view, User s) { view.setText(s.toString()); }
至此,DataBinding
的主要特性已经学习完毕了,绝大部分内容只是对官方文档的简单翻译。
在这里我想说一句,学习新的知识最正确的方式是直接看官方文档,因为那是第一手的资料,如果英文实在是太差,才考虑去看别人写的相关文章。
测试代码的GitHub地址