Data Binding 使用
需要在module中build.gradle配置data binding enable 为true,如下:
android {
...
dataBinding{
enabled = true
}
}
基本用法
创建一个User类
public class User{
private String userName;
public User() {
}
public User(String userName) {
this.userName= userName;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName= userName;
}
}
创建Activity的布局
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="user"
type="com.mvvm.demo.User"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/tv_user_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@{user.userName}"/>
</LinearLayout>
</layout>
通过上面代码可以看到,和之前的xml布局不同的是最外层是以layout标签为根标签开头,里面多了一个data标签。
在data标签中声明变量名和类的全路径
<data>
<variable
name="user"
type="com.mvvm.demo.User"/>
</data>
如果该类需要在layout中多个地方声明使用,可以使用import标签导包。
<data>
<import
type="com.zh.mvvmdemo.User"/>
<variable
name="user"
type="User"/>
</data>
如果需要导入相同的类可以使用alias来起别名。
<data>
<import
alias="CustomUser1"
type="com.zh.mvvmdemo.User"/>
<import
alias="CustomUser2"
type="com.zh.mvvmdemo.User"/>
<variable
name="user"
type="CustomUser1"/>
</data>
声明了变量后,我们将会让TextView和变量的数据进行绑定,如下:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/tv_user_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@{user.userName,default=默认文本}"/>
</LinearLayout>
通过 @{user.userName} 方法我们将TextView和User中的userName的getter方法进行关联。需要注意的是变量引用的是getter方法名称去除get的部分而不是属性名称。通过指定default的值可以在写布局时帮助我们预览文字大小字体颜色。
最后在Activity中通过DataBindingUtil类的setContentView方法来设置布局替换掉原来的SetContentView方法。并为布局中的变量赋值。
ActivityBasicUseBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_basic_use);
User user= new User("erik");
// binding.setUser(user);
binding.setVariable(BR.user, user);
从上述代码中看到,setContentView后会生成一个Binding类,通过binding类可以使用两种方法为变量赋值。
此外,我们还可以通过binding类拿到为我们准备好指定Id的控件。
binding.tvUserName.setText("erik");
监听绑定
监听绑定分为两种方式。
- 方法引用
事件可以直接绑定到布局中,类似于android:onClick
可以引用Activity中的方法,表达式在编译时处理,因此如果该方法不存在或其签名不正确,则会收到编译时错误。
例如:
public class EventPresenter {
public void onCustomClick(View view) {
Toast.makeText(view.getContext(), "按钮被点击了", Toast.LENGTH_SHORT).show();
}
}
首先创建一个类用来管理事件的方法。注意方法参数必须和监听器对象中的方法参数完全相匹配。
然后在布局中的去引用该方法,代码如下。
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="event"
type="com.zh.mvvmdemo.EventPresenter"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp"
tools:context=".BasicUseActivity">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:onClick="@{event.onCustomClick}"
android:text="绑定点击事件"/>
</LinearLayout>
</layout>
首先还是需要声明一个类,然后在 android:onClick 中引用该方法。
EventPresenter presenter = new EventPresenter();
binding.setEvent(presenter);
别忘记在Activity中为声明的类赋值。
- 监听器绑定
在方法引用中,方法的参数必须与事件侦听器的参数匹配。在监听器绑定中,我们可以自己定义方法签名,但是方法的返回值必须与监听器的返回值匹配。例如:
public class EventPresenter {
...
public void onListenerBinding(User user) {
Toast.makeText(mContext, user.toString(), Toast.LENGTH_SHORT).show();
}
}
然后在布局中使用lambda表达式来引用该方法。
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<variable
name="event"
type="com.zh.mvvmdemo.EventPresenter"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp"
tools:context=".BasicUseActivity">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{() -> event.onListenerBinding(emp)}"
android:text="监听器绑定"/>
</LinearLayout>
</layout>
在上面的事例中,我们没有用到相应事件的View,如果需要使用到可以使用带一个或者多个参数的lambda表达式,例如:
public class EventPresenter {
public void onClickBinding(Employee employee, View view) {
Toast.makeText(mContext, view.toString() + "<->" + employee.toString(), Toast.LENGTH_SHORT).show();
}
public void oncheckBoxChange(Employee employee, boolean isChecked) {
Toast.makeText(mContext, employee.toString() + ", isChecked = " + isChecked, Toast.LENGTH_SHORT).show();
}
}
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<variable
name="event"
type="com.zh.mvvmdemo.EventPresenter"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp"
tools:context=".BasicUseActivity">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{(view) -> event.onListenerBinding(emp,view)}"
android:text="监听器绑定使用带view的参数"/>
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onCheckedChanged="@{(cb, isChecked) -> event.oncheckBoxChange(emp, isChecked)}"/>
</LinearLayout>
</layout>
运算符
- 算术中的+ - / * %
- 字符串连接+
- 合乎逻辑&& ||
- 二进制& | ^
- 一元+ - ! ~
- 转移>> >>> <<
- 比较== > < >= <=(注意 < 需要转义为<)
instanceof
- 分组()
- 文字 - 字符,字符串,数字,null
- 类型转换cast
- 方法调用
- field访问
- 数组访问[]
- 三元运算符?:
目前缺省支持的操作
- this
- super
- new
- 泛型的调用
注意:在windows环境下,表达式中出现中文会发生奇怪的报错:Invalid byte 3 of 3-byte UTF-8 sequence 大概意思:databinding布局文件中的中文字符串非UTF-8编码。
解决方法:
- 可以在string.xml中配置,然后在使用@string/name 来引用
- 或者在实体类中处理,然后调用实体类的getter。
Null coalescing
空合并运算符 ?? 如果不为null 则取 ?? 左边的值,否则取 ?? 右边的值
@{tempEmp.firstName ?? @string/alertEmpty}
等价于
@{tempEmp.firstName != null ? tempEmp.firstName : @string/alertEmpty}
避免空指针异常
dataBinding会帮助我们避免空指针异常
举个栗子:当变量tempEmp为null时,会帮助我们将firstName设置为null,而不是抛出空指针异常。
include和ViewStub
include
通过使用bind命名空间和声明的变量名称,变量通过include传入到布局绑定中,例如:
创建一个layout_include.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="employee"
type="com.zh.mvvmdemo.Employee"/>
</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:layout_marginLeft="10dp"
android:layout_marginTop="10dp"
android:text="@{employee.firstName, default=erik}"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginTop="10dp"
android:text="@{employee.lastName, default=erik}"/>
</LinearLayout>
</layout>
然后在主布局中引入该布局,并将变量传递给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"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="employee"
type="com.zh.mvvmdemo.Employee"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".IncludeBindingActivity">
<TextView
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="@color/colorPrimary"
android:gravity="center"
android:text="include布局"
android:textColor="#FFFFFF"
android:textSize="20sp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorAccent"
android:gravity="center"
android:text="分割线"
android:textColor="#FFFFFF"/>
<include
layout="@layout/layout_include"
bind:employee="@{employee}"/>
</LinearLayout>
</layout>
注意 bind:employee 中employee必须和变量名称一致否则编译时会报找不到setter方法的异常。include中的变量声明也必须保持一致。
dataBinding不支持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"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="employee"
type="com.zh.mvvmdemo.Employee"/>
</data>
<Merge>
<include
layout="@layout/layout_include"
bind:employee="@{employee}"/>
</Merge>
</layout>
ViewStub
在不布局中引用ViewStub然后指定layout
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="emp"
type="com.zh.mvvmdemo.Employee"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".ViewStubBindingActivity">
<ViewStub
android:id="@+id/viewStub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/layout_view_stub"/>
</LinearLayout>
</layout>
通过binding中viewStubProxy类获取ViewStub调用inflate方法,就可以展示ViewStub布局
binding.viewStub.getViewStub().inflate();
如果需要为ViewStub绑定数据,使用方式和include一样,在ViewStub中添加自定义的命名空间将变量传递给ViewStub
<ViewStub
android:id="@+id/viewStub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/layout_view_stub"
app:emp="@{emp}"/>
我们也可以在inflate时在绑定数据,此时需要为ViewStub 设置 setOnInflateListener
回调函数,在回调函数中进行数据绑定,然后调用inflate方法。
binding.viewStub.setOnInflateListener(new ViewStub.OnInflateListener() {
@Override
public void onInflate(ViewStub stub, View inflated) {
LayoutViewStubBinding viewStubBinding = DataBindingUtil.bind(inflated);
viewStubBinding.setEmp(new Employee("ddd", "eee"));
}
});
binding.viewStub.getViewStub().inflate();
Observable单向数据绑定
通过三种方式实现数据变化驱动UI刷新,分别是 BaseObservable 、ObservableField 、ObservableCollecton
BaseObservable
BaseObservable提供了@Bindable 和 notifyPropertyChanged(); 被@Bindable 修饰的getter方法会在BR里面生成对应的flag,然后调用notifyPropertyChanged(); 传入生成的flag就可以刷新对应的View
public class Employee extends BaseObservable {
private String firstName;
public Employee() {
}
@Bindable
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
notifyPropertyChanged(BR.firstName);
}
}
首先继承自BaseObservable然后在getFirstName上加入@Bindable,最后在setFirstName方法中调用notifyPropertyChanged(BR.firstName);
<?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:tools="http://schemas.android.com/tools">
<data>
<variable
name="emp"
type="com.zh.mvvmdemo.Employee"/>
<variable
name="event"
type="com.zh.mvvmdemo.EventPresenter"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp"
tools:context=".ObservableActivity">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:hint="请输入名称"
android:onTextChanged="@{event::onTextChanged}"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:text="@{emp.first ?? @string/single_way_day_binding}"/>
</LinearLayout>
</layout>
在布局中我们会根据输入的内容刷新TextView的内容
public class EventPresenter {
public EventPresenter(Employee employee) {
mEmployee = employee;
}
public void onTextChanged(CharSequence s, int start, int before, int count) {
mEmployee.setFirstName(s.toString());
}
}
ObservableField
如果继承BaseObservable他的限制还是挺高的,如果类中只有少量字段需要绑定我们可以选择ObservableField,官方为我们提供了一些对基本类型封装的类:
ObservableBoolean
ObservableByte
ObservableChar
ObservableShort
ObservableInt
ObservableLong
ObservableFloat
ObservableDouble
ObservableParcelable
我们也可以通过ObservableField泛型来声明其他的,要使用ObservableField系列,请将对应的字段是用public final 修饰。
public class Employee {
public final ObservableField<String> age = new ObservableField<>();
public Employee() {
}
public void setAge(String age) {
this.age.set(age);
}
}
在布局中可以直接是用public 修饰的字段,比如下面例子中直接是用age
<?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:tools="http://schemas.android.com/tools">
<data>
<variable
name="emp"
type="com.zh.mvvmdemo.Employee"/>
<variable
name="event"
type="com.zh.mvvmdemo.EventPresenter"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp"
tools:context=".ObservableActivity">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:hint="请输入年龄"
android:onTextChanged="@{event::onTextChanged}"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:text="@{emp.age ?? @string/single_way_day_binding}"/>
</LinearLayout>
</layout>
然后在监听中通过ObservableField的set()方法来刷新数据和UI
public class EventPresenter {
public EventPresenter(Employee employee) {
mEmployee = employee;
}
public void onTextChanged(CharSequence s, int start, int before, int count) {
mEmployee.age.set(s.toString());
}
}
ObservableCollection
ObservableList
和 ObservableMap
,当其包含的数据发生变化时,绑定的视图也会随之进行刷新
public class ObservableActivity extends AppCompatActivity {
ObservableMap<String, String> map = new ObservableArrayMap<>();
ObservableList<String> list = new ObservableArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityObservableBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_observable);
list.add("google");
map.put("hello", "world");
binding.setMap(map);
binding.setList(list);
}
public void bindData(View view) {
map.put("hello", "world" + new Random().nextInt(100));
list.set(0, "google" + +new Random().nextInt(100));
}
}
<?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:tools="http://schemas.android.com/tools">
<data>
<variable
name="map"
type="android.databinding.ObservableMap<String, String>"/>
<variable
name="list"
type="android.databinding.ObservableList<String>"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp"
tools:context=".ObservableActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{map.hello}"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{list[0]}"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="bindData"
android:text="动态绑定数据"/>
</LinearLayout>
</layout>
DataBinding在RecyclerView中的使用
先创建没有Item的布局
<?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="item"
type="com.zh.mvvmdemo.Employee"/>
</data>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginTop="10dp"
android:text="@{item.firstName + item.lastName, default=erik}"
android:textSize="18sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/tv_age"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:text="@{String.valueOf(item.age), default=18}"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="@id/tv_name"
app:layout_constraintTop_toBottomOf="@id/tv_name"/>
</android.support.constraint.ConstraintLayout>
</layout>
然后对应的Adapter
public class EmpAdapter extends RecyclerView.Adapter<EmpAdapter.EmpViewHolder> {
private List<Employee> mEmployeeList;
private final LayoutInflater mLayoutInflater;
public EmpAdapter(Context context) {
mEmployeeList = new ArrayList<>();
mLayoutInflater = LayoutInflater.from(context);
}
@NonNull
@Override
public EmpViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
ViewDataBinding binding = DataBindingUtil.inflate(mLayoutInflater, R.layout.item_emp, parent, false);
return new EmpViewHolder(binding);
}
@Override
public void onBindViewHolder(@NonNull EmpViewHolder holder, int position) {
Employee employee = mEmployeeList.get(position);
holder.getBinding().setVariable(BR.item, employee);
}
@Override
public int getItemCount() {
return mEmployeeList.size();
}
class EmpViewHolder extends RecyclerView.ViewHolder {
private ViewDataBinding mBinding;
public EmpViewHolder(ViewDataBinding binding) {
super(binding.getRoot());
this.mBinding = binding;
}
public ViewDataBinding getBinding() {
return mBinding;
}
}
}
首先EmpViewHolder中构造方法会接受一个ViewDataBinding对象,然后在onBindViewHolder 中通过DataBindingUtil.inflate() 方法得到ViewDataBinding,最后在onBindViewHolder 中获取到Binding对象为变量赋值。
绑定适配器
@BindingMethods
在xml布局中给属性设置值,实际上会去找到对应的setter方法然后调用,但有时候属性往往具有名称不匹配的setter方法,在这些情况下,可以使用BindingMethods
注解将属性与Setter相关联,注解使用类上,一个@BindingMethods 可以包含多个@BindingMethod 例如:
@BindingMethods({@BindingMethod(type = ImageView.class, attribute = "android:imgRes", method = "setImageResource")})
public class CustomBindingMethod {
}
ImageView中有setImageResource() 方法,但是在xml布局中没有对应的属性,我们就可以创建一个类为它添加@BindingMethods 指定类型、属性名以及属性名对应的方法名称,使用的时候dataBinding会找到对应关联关系。在xml布局中我们就可以直接使用设置的属性,如下:
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.zh.mvvmdemo.R"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:imgRes="@{R.mipmap.ic_launcher}"/>
</LinearLayout>
</layout>
@BindingAdapter
@BindingAdapter 用于自定义的属性,比如ImageView去加载一个网络图片。
首先需要定义一个静态方法,为它添加注解@BindingAdapter 如下:
public class LoadImage {
@BindingAdapter(value = {"imgeUrl", "placeHolder", "error"}, requireAll = false)
public static void load(ImageView imageView, String url, int placeHolder, int error) {
RequestOptions options = new RequestOptions();
options.placeholder(placeHolder).error(error);
Glide.with(imageView.getContext()).load(url).into(imageView);
}
}
BindingAdapter中定了三个属性,分别对应静态方法参数中的参数,当ImageView被加载后,dataBinding会找到该方法,将ImageView和其他属性值传递过来。requireAll表示是否需要全部属性,如果设置为true,则所有属性必须设置,否则找不到该方法。
<?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:tools="http://schemas.android.com/tools">
<data>
<variable
name="image"
type="String"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:layout_width="150dp"
android:layout_height="150dp"
android:layout_marginTop="10dp"
app:error="@{@drawable/zds_sign_loading}"
app:imgeUrl="@{image}"
app:placeHolder="@{@drawable/bg_img_error_middle}"/>
</LinearLayout>
</layout>
@BindingAdapter还可以覆盖Android原来的属性中,被覆盖后的属性将作用全局,例如:
public class CustomBindingAdapter {
@BindingAdapter("android:text")
public static void setText(TextView textView, String text) {
textView.setText(text + "我是后缀");
}
}
在设置TextView的android:text 属性我们在获取到属性值后加入后缀,这样所有拥有android:text 属性的控件都会加入后缀。
@BindingConversion
在xml布局中属性会通过值得类型找到对应参数类型的setter方法,如果类型没有匹配,则会出现错误,这时候我们可以定义一个静态方法,然后为它添加@BindingConversion 注解,参数类型要对应属性值类型,然后在静态方法中就可以处理转换逻辑。
<?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:tools="http://schemas.android.com/tools">
<data>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background='@{"red"}'
android:orientation="vertical">
</LinearLayout>
</layout>
在布局中 android:background 输入的是String类型的值,而setBackGround方法需要的是Drawable类型的,如果不转换就会报错。
public class CustomBindingConversion {
@BindingConversion
public static ColorDrawable stringToDrawable(String text) {
if (TextUtils.equals("red", text)) {
return new ColorDrawable(Color.RED);
} else {
return new ColorDrawable(Color.YELLOW);
}
}
}
我们通过定义一个静态方法stringToDrawable 当需要通过String类型转换成Drawable类型时,该方法会被调用。
双向绑定
dataBinding提供了@={}符号,当UI发生变化数据实时更新。
<?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:tools="http://schemas.android.com/tools">
<data>
<variable
name="loginUser"
type="com.zh.mvvmdemo.User"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp"
tools:context=".TwoWayBindingActivity">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:hint="请输入用户名"
android:text="@={loginUser.userName.get()}"
android:textSize="16sp"/>
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:hint="请输入密码"
android:inputType="textPassword"
android:text="@={loginUser.password}"
android:textSize="16sp"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:onClick="login"
android:text='@{"hello, welcome " + loginUser.userName.get()}'/>
</LinearLayout>
</layout>
当输入了用户名和密码后,User类中userName和password数据也会更新。
为了对数据的变化做出反应,可以将布局中的变量继承自BaseObservable然后使用@Bindable注解和notifyPropertyChanged()
原生的控件中有些控件的属性已经实现了双向绑定的功能,如下
类 | 属性(S) | 绑定适配器 |
---|---|---|
AdapterView | android:selectedItemPosition android:selection | AdapterViewBindingAdapter |
CalendarView | android:date | CalendarViewBindingAdapter |
CompoundButton | android:checked | CompoundButtonBindingAdapter |
DatePicker | android:year android:month android:day | DatePickerBindingAdapter |
NumberPicker | android:value | NumberPickerBindingAdapter |
RadioButton | android:checkedButton | RadioGroupBindingAdapter |
RatingBar | android:rating | RatingBarBindingAdapter |
SeekBar | android:progress | SeekBarBindingAdapter |
TabHost | android:currentTab | TabHostBindingAdapter |
TextView | android:text | TextViewBindingAdapter |
TimePicker | android:hour android:minute | TimePickerBindingAdapter |
如果是自定义控件,我们则需要自己去实现双向绑定,dataBinding也为我们提供了@InverseBindingAdapter 注解。
比如,我们自定义一个常用的表单控件,左边为标题右边为输入框。
public class CommonDetailItem extends LinearLayout {
private EditText tvContent;
private TextView mTvTitle;
private OnContentChangeListener mOnContentChangeListener;
public void setOnContentChangeListener(OnContentChangeListener onContentChangeListener) {
mOnContentChangeListener = onContentChangeListener;
}
public CommonDetailItem(Context context, AttributeSet attrs) {
super(context, attrs);
LayoutInflater.from(context).inflate(R.layout.rl_common_detail_item, this, true);
......
tvContent.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (mOnContentChangeListener != null) {
mOnContentChangeListener.onContentChange();
}
}
@Override
public void afterTextChanged(Editable s) {
}
});
typedArray.recycle();
}
public void setContentText(String text) {
tvContent.setText(TextUtils.isEmpty(text) ? "" : text);
}
public String getContentText() {
return tvContent.getText().toString().trim();
}
public interface OnContentChangeListener {
void onContentChange();
}
}
实现自定义双向绑定,分为三步骤:
- 先通过@BindingAdapter对指定的属性形成单向绑定关系。
@BindingAdapter("app:contentText") public static void setContentText(CommonDetailItem commonView, String newValue) { String oldValue = commonView.getContentText(); //这个步骤为了避免死循环 if (!TextUtils.equals(oldValue, newValue)) { commonView.setContentText(newValue); } }
- 使用@InverseBindingAdapter注解可以从视图中读取更新的数据
@InverseBindingAdapter(attribute = "app:contentText", event = "app:contentAttrChange") public static String getContentText(CommonDetailItem commonView) { return commonView.getContentText(); }
@InverseBindingAdapter需要制定attribute和event
attribute:指定属性,表示视图发生改变时dataBinding调用该方法获取更新的数据。
event:指定事件,表示当视图发生变化时由和event指定的值一样的函数通知dataBinding视图发生变化更新数据,它和下面步骤中的事件的属性形成连接关系。
- 在视图上设置一个侦听器。它可以是与自定义视图关联的自定义侦听器,也可以是通用事件,例如失去焦点或文本更改。使用@BindingAdapter注释添加到方法上,该方法为属性的更新设置侦听器:
@BindingAdapter("app:contentAttrChange") public static void setContentChangeListener(CommonDetailItem item, final InverseBindingListener contentChange) { if (contentChange == null) { item.setOnContentChangeListener(null); } else { item.setOnContentChangeListener(new CommonDetailItem.OnContentChangeListener() { @Override public void onContentChange() { contentChange.onChange(); } }); } }
BindingAdapter的值和步骤二中event的值保持对应,该方法中以InverseBindingListener作为参数。我们可以使用InverseBindingListener来告诉dataBinding该属性已更改。然后,系统可以开始调用使用@InverseBindingAdapter注释的方法更新数据。