背景
dataBinding数据绑定是谷歌推出的基于观察者模式的数据和页面内容的绑定,拥有广阔的应用前景。
使用
使能
dataBinding使能只需要在module的gradle文件里加上这么一段话
android {
...
dataBinding {
// 使能dataBinding
enabled = true
}
...
}
就可以了
创建数据实体
官方推荐的数据实体是javaBean,先让这个类继承自BaseObservable,然后在需要和布局内容绑定的属性的get方法前,加上@Bindable注解,以及set方法里,属性更改后,调用notifyPropertyChanged()方法。
ppackage applicationmanager.com.example.a123.studyofdatabinding.javaBean;
import android.databinding.BaseObservable;
import android.databinding.Bindable;
import applicationmanager.com.example.a123.studyofdatabinding.BR;
public class Person extends BaseObservable {
private String name;
private int age;
private String url;
public Person(String name, int age, String url) {
this.name = name;
this.age = age;
this.url = url;
}
@Bindable
// 在需要观察的属性对应的get方法上加Bindable注解
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
notifyPropertyChanged(BR.name);
// BR相当于R,name是其属性对应的id。调用此方法进行实时显示
}
@Bindable
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
notifyPropertyChanged(BR.age);
}
@Bindable
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
notifyPropertyChanged(BR.url);
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer("Person{");
sb.append("name='").append(name).append('\'');
sb.append(", age=").append(age);
sb.append(", url='").append(url).append('\'');
sb.append('}');
return sb.toString();
}
}
这样,一个可观察的javaBean就创建完了
布局内容
最大的变化感觉还是在布局方面,dataBinding把原来的根标签换成了<layout>标签,下面有<data>和真正的根布局。
因此,要把需要绑定的数据声明到<data>数据域里
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- 根节点换成layout -->
<!-- 数据区包裹在<data>标签下 -->
<data>
<variable
name="person"
type="applicationmanager.com.example.a123.studyofdatabinding.javaBean.Person"/>
<!-- 声明要用到的变量,名字和类型 -->
</data>
<!-- 原来的布局 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{person.name + @string/separator + person.age}" />
<!-- 获取属性 -->
<Button
android:id="@+id/increment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击改变"
/>
</LinearLayout>
</layout>
数据域里的变量要冠以variable标签,里面写上变量的名字和类型。而后在布局里用的时候用@{}来对变量进行引用。引用时,{}里不能直接出现要显示的字符串,如果要显示固定的文字,必须在资源文件(res/values/strings.xml)里定义,比如这样
<resources>
<string name="app_name">StudyOfDataBinding</string>
<string name="separator">-------</string>
</resources>
为了展示数据的实时变化,我加了一个按钮,点击一下person的年龄+1
Activity里进行绑定
方才是设计了一个javaBean实体,而后在布局文件里声明了一个变量,下面就是把它们俩绑定到一起的过程
package applicationmanager.com.example.a123.studyofdatabinding;
import android.databinding.ViewDataBinding;
import android.os.Bundle;
import applicationmanager.com.example.a123.studyofdatabinding.javaBean.Person;
public class MainActivity extends Activity {
private ViewDataBinding mBinding;
private Person person;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
// 加载绑定布局
person = new Person("Jason", 24, "http://p0.so.qhmsg.com/bdr/_240_/t01825773612648be9f.jpg");
mBinding.setVariable(BR.person, person);
findViewById(R.id.increment).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
person.setAge(person.getAge() + 1);
}
});
}
}
步骤如上所示,利用DataBindingUtil的setContentView替换原来的setContentView,来把当前Activity和指定的布局文件绑定在一起,同时获取一个ViewDataBinding对象,而后利用此对象,把布局里的person变量和activity里的person对象绑定在一起。
运行之后,效果如图所示
这样,最基本的数据绑定就实现了。
ObservableInt等
如果需要使用一个可观察的基本数据类型或内置对象,可以使用DataBinding里的ObservableInt、ObservableString、ObservableArrayMap等,使用方法和绑定javaBean相似,但亦有不同。
布局文件里声明
这时声明的话需要在数据域里导包,因为这些类不在我们的工程里
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- 根节点换成layout -->
<!-- 数据区包裹在<data>标签下 -->
<data>
<import type="android.databinding.ObservableArrayMap"/>
<import type="android.databinding.ObservableInt"/>
<import type="android.databinding.ObservableField"/>
<import type="android.databinding.ObservableArrayList"/>
<!-- 导包 -->
<variable
name="person"
type="applicationmanager.com.example.a123.studyofdatabinding.javaBean.Person"/>
<!-- 声明要用到的变量,名字和类型 -->
<variable
name="count"
type="ObservableInt"/>
<variable
name="info"
type="ObservableField<String>"/>
<!-- <>括号要转义,<改成< >改成> -->
<variable
name="maps"
type="ObservableArrayMap<String, String>"/>
<variable
name="arrays"
type="android.databinding.ObservableArrayList<String>"/>
<variable
name="index"
type="ObservableInt"/>
</data>
<!-- 原来的布局 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{person.name + @string/separator + person.age}" />
<!-- 获取属性 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{String.valueOf((Integer)count)}"/>
<!-- 整数必须先转成Integer,再转成String -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{info}"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{maps["name"]}'/>
<!-- 根据键取值 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{maps["description"]}'/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{arrays[index]}"/>
<!-- 遍历数组 -->
<ListView
android:id="@+id/list_people"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<Button
android:id="@+id/increment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击改变"
/>
<!-- 绑定监听,自动传入view -->
<Button
android:id="@+id/decrement"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击减少"
/>
</LinearLayout>
</layout>
其中映射根据键取值,很像python里的操作,仿佛是在操作数组,此刻为了区分,我们可以给键加上双引号,@{}外面换成单引号。
再者,就是<>的转义了,把<改成< >改成>才能编译通过。
最后,引用时,要把ObservableInt转成字符串,就像上面的
android:text="@{String.valueOf((Integer)count)}"
activity里绑定
为了实现可观察数组和映射,我事先定义了一组数据
private String[] names = new String[]{
"A", "B", "C", "D"
};
private String[] descriptions = new String[]{
"aa", "bb", "cc", "dd"
};
然后在定义一堆Observable对象,其中最后的indexObservable是数组元素的下标
private ObservableInt countObservable = new ObservableInt();
private ObservableField<String> infoObservable = new ObservableField<>();
private ObservableArrayMap<String, String> mapObservable = new ObservableArrayMap<>();
private ObservableArrayList<String> arraysObservable = new ObservableArrayList<>();
private ObservableInt indexObservable = new ObservableInt();
在onCreate()和布局里的变量的id进行绑定
mBinding.setVariable(applicationmanager.com.example.a123.studyofdatabinding.BR.person, person);
mBinding.setVariable(applicationmanager.com.example.a123.studyofdatabinding.BR.count, countObservable);
mBinding.setVariable(applicationmanager.com.example.a123.studyofdatabinding.BR.info, infoObservable);
mBinding.setVariable(applicationmanager.com.example.a123.studyofdatabinding.BR.maps, mapObservable);
mBinding.setVariable(applicationmanager.com.example.a123.studyofdatabinding.BR.arrays, arraysObservable);
mBinding.setVariable(BR.index, indexObservable);
而后,开启两个线程,进行数据的改变。为了实现数组的自行遍历,单门为index开了一个线程。
对于Observable对象,基本就是set设值,get取值。对于映射和数组,set/get方法和普通的映射数组一样,其中数组也可以给指定下标的元素设值。
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
String name = names[i % names.length];
String decription = descriptions[i % descriptions.length];
countObservable.set(i + 1); // set方法设值
infoObservable.set("当前计数器的值是:" + (i + 1) + ";线程名:" + Thread.currentThread().getName());
mapObservable.put("name", name);
mapObservable.put("description", decription);
arraysObservable.add(name);
// arraysObservable.set(0, name);
try {
Thread.sleep(3 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
indexObservable.set(i);
try {
Thread.sleep(3 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
运行效果如图所示
时间间隔有点长,但可以看到实现了数据的自行更新。
ListView的数据适配
其实要想显示数据列表的话,还是推荐用ListView而不是在布局里声明一个数组,毕竟ListView是单门用来显示列表的
main布局里声明
在activity布局里添加一个ListView
<ListView
android:id="@+id/list_people"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
创建列表项的布局文件
然后给列表每一项创建通用的布局文件,这个布局文件要采用dataBinding格式
<?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">
<data>
<variable
name="personItem"
type="applicationmanager.com.example.a123.studyofdatabinding.javaBean.Person"/>
</data>
<LinearLayout
android:orientation="horizontal" android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{@string/personName + personItem.name}"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{@string/separator + @string/personAge + String.valueOf(personItem.age)}"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="match_parent"
app:imageUrl = "@{personItem.url}"
android:layout_marginLeft="40dp"/>
</LinearLayout>
</layout>
就是从左往右显示名字、年龄和图片。对于图片,我也是利用dataBinding的方法来实现加载的,一会儿再说它。
创建适配器
ListView显示数据当然要创建适配器
package applicationmanager.com.example.a123.studyofdatabinding.adapter;
import android.content.Context;
import android.databinding.DataBindingUtil;
import android.databinding.ViewDataBinding;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import java.util.LinkedList;
import applicationmanager.com.example.a123.studyofdatabinding.BR;
import applicationmanager.com.example.a123.studyofdatabinding.R;
import applicationmanager.com.example.a123.studyofdatabinding.javaBean.Person;
public class MyListViewBindingAdapter extends BaseAdapter {
private LinkedList<Person> people;
private Context mContext;
public MyListViewBindingAdapter(LinkedList<Person> people, Context mContext) {
this.people = people;
this.mContext = mContext;
}
@Override
public int getCount() {
return people.size();
}
@Override
public Object getItem(int i) {
return people.get(i);
}
@Override
public long getItemId(int i) {
return i;
}
@Override
public View getView(int i, View view, ViewGroup viewGroup) {
ViewDataBinding binding = null;
if (view == null) {
Person person = people.get(i);
if (person != null) {
binding = DataBindingUtil.inflate(LayoutInflater.from(mContext), R.layout.list_item, viewGroup, false);
binding.setVariable(BR.personItem, person);
}
} else {
binding = DataBindingUtil.getBinding(view);
// return view;
}
return binding.getRoot();
}
}
和传统适配器唯一的区别来自getView()方法。
如果是第一次显示这一项,入参view必然是空,那么我们就获取people列表里第i个person,加载列表项的布局文件,获取ViewDataBinding对象,并利用此对象把person对象和布局中的personItem绑定起来。
如果不是第一次显示这一项,其实可以直接返回view了,因为此时view肯定不是空。但如果要对view数据改变绑定的话,可以通过DataBindingUtil的getBinding()方法,获取view的数据绑定对象,然后重新setVariable。
最后返回binding.getRoot(),也就是绑定的view。
但是在第一次显示的时候,不要直接返回view,因为DataBindingUtil.inflate()方法并没有给view赋值,view还是null
Activity里初始化ListView
初始化很常规
package applicationmanager.com.example.a123.studyofdatabinding;
import android.app.Activity;
import android.databinding.ViewDataBinding;
import android.os.Bundle;
import android.widget.ListView;
import applicationmanager.com.example.a123.studyofdatabinding.adapter.MyListViewBindingAdapter;
public class MainActivity extends Activity {
private ViewDataBinding mBinding;
private ListView mListView;
private MyListViewBindingAdapter mAdapter;
private LinkedList<Person> mPeole = new LinkedList<>();
private String[] names = new String[]{
"A", "B", "C", "D"
};
private String[] urls = new String[]{
"http://p5.so.qhimgs1.com/bdr/_240_/t0158432ac9d02c74bb.jpg",
"http://p4.so.qhmsg.com/bdr/_240_/t015300f4f65dc07f89.jpg",
"http://p3.so.qhimgs1.com/bdr/_240_/t014bbc3f5ab3755383.jpg",
"http://p0.so.qhimgs1.com/bdr/_240_/t017ee2f5c7e5fcef90.jpg",
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
mListView = findViewById(R.id.list_people);
mAdapter = new MyListViewBindingAdapter(mPeole, this);
mListView.setAdapter(mAdapter);
for (int i = 0; i < 10; i++) {
String name = names[i % names.length];
int age = i + 1;
String url = urls[i % urls.length];
mPeole.add(new Person(name, age, url));
}
...
}
}
运行的结果如图所示
可见ListView的适配也完成了。下面,我们既就看看dataBinding是怎么加载图片的
图片的加载
以上面的ListView中的图片为例,我们在布局文件里自定义图片加载的方法app:imageUrl
<ImageView
android:layout_width="wrap_content"
android:layout_height="match_parent"
app:imageUrl = "@{personItem.url}"
android:layout_marginLeft="40dp"/>
为了实现这个app:imageUrl,我们再定义一个Monitor类,里面进行数据绑定方面的调度。
package applicationmanager.com.example.a123.studyofdatabinding.monitor;
import android.databinding.BindingAdapter;
import android.net.Uri;
import android.view.View;
import com.bumptech.glide.Glide;
import applicationmanager.com.example.a123.studyofdatabinding.R;
import applicationmanager.com.example.a123.studyofdatabinding.myInterface.IActivity;
public class Monitor {
private IActivity mActivity;
public Monitor(IActivity mActivity) {
this.mActivity = mActivity;
}
// 绑定方法,对应app:imageUrl
@BindingAdapter("imageUrl")
public static void loadImage(ImageView view, String url) {
Glide.with(view.getContext().getApplicationContext()).load(Uri.parse(url)).into(view);
}
}
通过@BindingAdapter注解,就实现了布局文件里的方法和java里的方法的绑定,参数都是自定义的,但要注意,返回值最好是void,而且参数要合理,比如设置imageView的url,入参定义为view和url就可以了,这样也有利于高内聚。
另外,java方法一定要是静态的,如果不设置成静态的,也可以,这样就要利用dataBinding的component来实现,感觉component用处不是很大,而且不如static灵活,特别是要调用recreate()重新构造activity实现数据的变化,让我觉得不是太好用,这里我贴出一篇关于使用component的博文,供大家参考,链接。
这样,我们就实现了图片的加载。如果要改变图片,只需要在MainActivity里设置people列表里的某一项的url即可。
方法的绑定
方才,我们看到了@BindingAdapter注解,可以实现布局文件里的方法和java方法的绑定。说到方法的绑定,最常用的是布局属性的统一更改或点击事件的统一调度。
布局属性的更改
这一点和方才的imageUrl类似,只不过注解传参要传指定的属性名字比如android:layout_marginLeft。这样只要在布局文件里设置了android:layout_marginLeft属性,就会调到我们的setLeftMargin()方法里,
// 安卓属性setter
@BindingAdapter("android:layout_marginLeft")
public static void setLeftMargin(View view, int margin) {
ViewGroup.LayoutParams layoutParams= view.getLayoutParams();
if (layoutParams instanceof LinearLayout.LayoutParams) {
((LinearLayout.LayoutParams) layoutParams).leftMargin = margin + 5;
} else if (layoutParams instanceof FrameLayout.LayoutParams) {
((FrameLayout.LayoutParams) layoutParams).leftMargin = margin + 5;
} else if (layoutParams instanceof RelativeLayout.LayoutParams) {
((RelativeLayout.LayoutParams) layoutParams).leftMargin = margin + 5;
}
view.setLayoutParams(layoutParams);
}
例子里,只是让左外间距多了5像素
点击事件的统一调度
这个需要在布局文件里传入monitor对象
<variable
name="monitor"
type="applicationmanager.com.example.a123.studyofdatabinding.monitor.Monitor"/>
然后在View(比如Button)的onClick里,传入monitor的onClick
<Button
android:id="@+id/increment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击改变"
android:onClick="@{monitor::onClick}"
/>
<!-- 绑定监听,自动传入view -->
<Button
android:id="@+id/decrement"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击减少"
android:onClick="@{monitor::onClick}"/>
注意引用方法时要用双冒号
最后我们就实现monitor的onClick,注意,此时不用加注解
public void onClick(View view) {
if (mActivity == null) {
return;
}
int old = mActivity.getData();
switch(view.getId()){
case R.id.increment:
old++;
break;
case R.id.decrement:
old--;
break;
}
mActivity.onDataChanged(old);
}
这里,我用了MVP模式在实现数据的更新。mActivity是接口对象,实现在Activity里
@Override
public void onDataChanged(int data) {
mPeole.get(0).setAge(data);
}
@Override
public int getData() {
return mPeole.get(0).getAge();
}
运行效果如图所示,注意看listView第一项的年龄变动
结语
dataBinding的常用用法就如上所示了,还有一些类型强转,布局里的运算符什么的,我不是很推荐使用,首先是AS对于xml文件的调试定位能力有限,再者我觉得还是让布局文件只处理数据显示的好,要把逻辑处理什么的放在java代码里比较合适。