Android DataBinding

1,745 阅读15分钟

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>

运算符

  • 算术中的+ - / * %
  • 字符串连接+
  • 合乎逻辑&& ||
  • 二进制& | ^
  • 一元+ - ! ~
  • 转移>> >>> <<
  • 比较== > < >= <=(注意 需要转义为&lt;
  • instanceof
  • 分组()
  • 文字 - 字符,字符串,数字,null
  • 类型转换cast
  • 方法调用
  • field访问
  • 数组访问[]
  • 三元运算符?:

   目前缺省支持的操作

  • this
  • super
  • new
  • 泛型的调用

注意:在windows环境下,表达式中出现中文会发生奇怪的报错:Invalid byte 3 of 3-byte UTF-8 sequence 大概意思:databinding布局文件中的中文字符串非UTF-8编码。

解决方法:

  1. 可以在string.xml中配置,然后在使用@string/name 来引用
  2. 或者在实体类中处理,然后调用实体类的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,官方为我们提供了一些对基本类型封装的类:

我们也可以通过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

ObservableListObservableMap,当其包含的数据发生变化时,绑定的视图也会随之进行刷新

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&lt;String, String>"/>

        <variable
            name="list"
            type="android.databinding.ObservableList&lt;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)
绑定适配器
AdapterViewandroid:selectedItemPosition
android:selection
AdapterViewBindingAdapter
CalendarViewandroid:dateCalendarViewBindingAdapter
CompoundButtonandroid:checkedCompoundButtonBindingAdapter
DatePickerandroid:year
android:month
android:day
DatePickerBindingAdapter
NumberPickerandroid:valueNumberPickerBindingAdapter
RadioButtonandroid:checkedButtonRadioGroupBindingAdapter
RatingBarandroid:ratingRatingBarBindingAdapter
SeekBarandroid:progressSeekBarBindingAdapter
TabHostandroid:currentTabTabHostBindingAdapter
TextViewandroid:textTextViewBindingAdapter
TimePickerandroid: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();
    }
}

实现自定义双向绑定,分为三步骤:

  1. 先通过@BindingAdapter对指定的属性形成单向绑定关系。

    @BindingAdapter("app:contentText")
    public static void setContentText(CommonDetailItem commonView, String newValue) {
        String oldValue = commonView.getContentText();
        //这个步骤为了避免死循环
        if (!TextUtils.equals(oldValue, newValue)) {
            commonView.setContentText(newValue);
        }
    }
  2. 使用@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视图发生变化更新数据,它和下面步骤中的事件的属性形成连接关系。

  3. 在视图上设置一个侦听器。它可以是与自定义视图关联的自定义侦听器,也可以是通用事件,例如失去焦点或文本更改。使用@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注释的方法更新数据。