Android Data Binding介绍

       Android Data Binding,Android 数据绑定库,它允许咱们在布局文件中就把UI组件和实体对象(POJO)或者事件绑定起来,而不用像之前样的写很多代码的方式来实现(按照咱们之前的做法都是在Activity中通过findViewById()找到UI组件对象,然后做各种操作设置数据或者事件)。

       前提:Android Data Binding使用之前需要在对应module的build.gradle文件添加如下代码确保当前module支持Data Binding库的使用(Android Studio的Gradle插件版本不能低于1.5.0-alpha1)

android {
    ......

    dataBinding {
        enabled true
    }
}

       我们先通过一个简单的例子来对Data Binding有个感性的认知。定义一个User类(firstName, lastName两个属性),然后把这两个属性绑定到两个TextView上去。

activity 对应的布局文件

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data class="com.tuacy.databindingdev.simple.SimpleBinding">

        <variable
            name="user"
            type="com.tuacy.databindingdev.entity.User" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="8dp"
            android:text="简单的在UI显示绑定的数据"/>

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:gravity="center"
            android:text="@{user.firstName}"
            android:textSize="18sp" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:gravity="center"
            android:text="@{user.lastName}"
            android:textSize="18sp" />
    </LinearLayout>
</layout>

Acitivty对应代码

public class SimpleBindingActivity extends AppCompatActivity {

    public static void startUp(Context context) {
        context.startActivity(new Intent(context, SimpleBindingActivity.class));
    }

    private SimpleBinding mBinding;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding = DataBindingUtil.setContentView(this, R.layout.activity_simple_binding);
        initData();
    }

    private void initData() {
        mBinding.setUser(new User("展示用户名", "展示用户密码"));
    }

}

User类

public class User {

    private String firstName;
    private 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;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
}

       通过上面的代码实现,User类设置的两个属性就直接在TextView上显示出来了。

       Data Binding的学习,我们需要从哪些方面入手呢,我们需要知道些啥呢

  • Data Binding官方文档咱们的知道,https://developer.android.com/topic/libraries/data-binding/
  • Data Binding 布局文件规则,绑定表达式。
  • 自定生成的Binding类名的自定义,默认Binding类名的生成规则(布局文件里面只能告诉UI想绑定到哪个实体对象,最终我们还是需要Binding类调用对应的setter方法把完成实体对象的绑定)。
  • 数据绑定:包括两部分数据刷新(调用Binding类把实体对象绑定到UI控件上去了,当实体对象变化的时候UI控件也能相应的变化)、双向绑定(实体对象和UI控件绑定之后,实体对象变化UI也跟着变化。UI改变实体对象也改变)。
  • 事件绑定。
  • Binding Adapter的使用。

一、Data Binding 布局文件和绑定表达式

       Data Binding使用的时候,布局文件和之前不同了。之前咱们的布局文件的根标签直接是ViewGroup,然后里面好多UI控件。现在不一样了,Data Binding的根标签是layout,同时layout下面包含两部分:data标签(描述Data Binding表达式中要用到的属性和类)、ViewGroup标签(和没有使用Data Binding的时候的文件根差不多,只是里面的UI控件会加入一些绑定表达式)。

如下是一个简单的Data Bingding布局文件

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data class="com.tuacy.databindingdev.simple.SimpleBinding">

        <variable
            name="user"
            type="com.tuacy.databindingdev.entity.User" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="8dp"
            android:text="简单的在UI显示绑定的数据"/>

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:gravity="center"
            android:text="@{user.firstName}"
            android:textSize="18sp" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:gravity="center"
            android:text="@{user.lastName}"
            android:textSize="18sp" />
    </LinearLayout>
</layout>

1.1、Data Binding 布局文件<data>标签

       <data>标签是非常重要的一部分,它定义了咱们在绑定表达式中会用到的一些类和属性,还可以自定义Binding类的生成路径和名字。

1.1.1、<data> class

       <data>标签里面的class属性用于自定义生成的Binding 类名和路径。默认情况下(在没写class属性的情况下),Binding类的命名会根据layout文件的名称来,用大写开头,除去下划线()以及()后的第一个字母大写,然后添加“Binding”后缀,同时生成的这个类将被放置在模块包里的databinding目录下。默认情况下比如模块包是com.tuacy。布局文件名为main_activity.xml,那么会默认生成com.tuacy.databinding.MainActivityBinding类。

       自定义生成的Binding类名和路径我们通过三个例子来看下

  • 在模块包的databinding目录中生成自定义的Binding类(DataImportBinding)
    <data class="DataImportBinding">
        ......
    </data>

  • 在模块包下生成自定义的Binding类(DataImportBinding)
    <data class=".DataImportBinding">
        ......
    </data>

  • 在自定义的包中生成自定义的Binding类(DataImportBinding)
    <data class="com.tuacy.databindingdev.simple.DataImportBinding">
        ......
    </data>

       Binding类在Data Binding的时候中是经常要打交道的一个类,一个Activity对应一个布局文件对应一个Binding类。在Activity中都是通过这个Binding类来完成最终的绑定。这个Binding类的内容是会根据布局文件的内容自动生成的。我们通过<data> class也只是去定义这个Binding类的路径和名字。生成的Binding类我们可以在build/generated/source/apt/debug/com里面找到。

1.1.2、<data>子标签<import>

       类似咱们在java代码中使用某个类之前先要导入这个类,<import>标签的作用也是如此。我们想要在绑定表达式和<variable>元素使用的类都需要通过import标签导入。除此之外<import>标签的另一个用途还可以用于解决类名冲突的情况(需配合alias属性使用)。比如有这么个情况:

    <data class="com.tuacy.databindingdev.simple.DataImportBinding">

        <import type="android.view.View" />
        <import type="com.tuacy.databindingdev.View" />

        <variable
            name="customerView"
            type="View" />
    </data>

这个时候<variable>里面的type元素就不晓得用哪个View了。想要追踪到正确的类,这个时候就需要配合alias属性给一个别名。

    <data class="com.tuacy.databindingdev.simple.DataImportBinding">

        <import type="android.view.View" />
        <import type="com.tuacy.databindingdev.View" alias="CustomerView"/>

        <variable
            name="customerView"
            type="CustomerView" />
    </data>

java.lang.*包下的类默认自动导入,不需要通过<import>导入。

1.1.3、<data>子标签<variable>

       我们可以认为每一个<variable>元素描述了一个我们想用于绑定UI的实体类(POJO)。其中name元素表示实体类对象的名字,type元素表示实体类对象的类型。每一个<variable>在自动生成的的Binding类中都会生成对应的setter和getter方法。例如

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data class="com.tuacy.databindingdev.simple.SimpleBinding">

        <variable
            name="user"
            type="com.tuacy.databindingdev.entity.User" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:gravity="center"
            android:text="@{user.lastName}"
            android:textSize="18sp" />
    </LinearLayout>
</layout>

如上代码我们在<variable>中描述了一个User的实体类,并且在TextView中绑定了这个实体类。同时SimpleBinding(Binding类)中同时会生成setUser()和getUser()函数。之后在Activity中我们可以通过SimpleBinding(Binding类)的setUser()方法完成绑定。

1.2、Data Binding Includes使用

       在使用应用命名空间的布局中,变量是可以传递到任何 include布局中。只是有一点我们要特别注意,当前布局文件和include对应的布局文件中都需要声明传递过去的变量(user变量)。

咱们用一个简单的实例来说明

<?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">

    <data>

        <variable
            name="user"
            type="com.tuacy.databindingdev.simple.User" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <include
            layout="@layout/include"
            bind:user="@{user}" />
    </LinearLayout>
</layout>

include.xml 布局文件里面也要申明传递过去的变量

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>

        <variable
            name="user"
            type="com.tuacy.databindingdev.simple.User" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:gravity="center"
            android:text="@{user.firstName}"
            android:textSize="18sp" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:gravity="center"
            android:text="@{user.lastName}"
            android:textSize="18sp" />
    </LinearLayout>
</layout>

       Data Binding Inlcude 不支持直接包含 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">
   <data>
       <variable name="user" type="com.connorlin.databinding.model.User"/>
   </data>
   <merge>
       <include layout="@layout/include"
            bind:user="@{user}"/>
   </merge>
</layout>

1.3、Data Binding 绑定表达式

       Data Binding在布局文件的第二部分ViewGroup节点部分。我们会把我们的UI控件绑定到实体对象。比如android:text="@{user.firstName}"就是想把user的对象的firstName属性绑定到控件的text属性。绑定的过程中我们还可以使用一些表达式比如android:text="@{importList[0].name}"就是去importList列表第一个元素的name属性。

1.3.1、通用特性

  • 数学计算 + - / * %
  • 字符串连接 +
  • 逻辑 && ||
  • 二进制 & | ^
  • 一元 + - ! ~
  • 位移 >> >>> <<
  • 比较 == > < >= <=
  • instanceof
  • 组 ()
  • 字面量 - 字符,字符串,数字, null
  • 类型转换
  • 函数调用
  • 字段存取
  • 数组存取 []
  • 三元运算符 ?:
android:text="@{String.valueOf(index + 1)}"//整数转字符串,index需要在datab标签里面申明
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"//比较操作符
android:transitionName='@{"image_" + id}'//字符串连接操作符

1.3.2、不支持的操作符

       一些Java中的操作符在表达式语法中不能使用

  • this
  • super
  • new
  • 显式泛型调用 <T>

1.3.3、Null合并运算符

       Null合并运算符 ?? 会在非 null 的时候选择左边的操作,反之选择右边。

android:text="@{user.displayName ?? user.lastName}"

等价于

android:text="@{user.displayName != null ? user.displayName : user.lastName}"

1.3.3、类的属性

       表达式可以通过使用以下格式来引用类中的属性。

android:text="@{user.lastName}" //user变量对用类的lastName属性

1.3.4、避免空指针异常

       生成的数据绑定代码会自动检查空值并避免空指针异常的产生。

1.3.5、容器类

       通用的容器类:数组,lists,sparse lists,和 maps,可以用 [] 操作符来存取。

<data>
    <import type="android.util.SparseArray"/>
    <import type="java.util.Map"/>
    <import type="java.util.List"/>
    <variable name="list" type="List&lt;String&gt;"/>
    <variable name="sparse" type="SparseArray&lt;String&gt;"/>
    <variable name="map" type="Map&lt;String, String&gt;"/>
    <variable name="index" type="int"/>
    <variable name="key" type="String"/>
</data>
…
android:text="@{list[index]}"//取list的index位置的元素
…
android:text="@{sparse[index]}"//取sparse的index位置的元素
…
android:text="@{map[key]}"//取map的key值的元素

1.3.6、字符串常量

       使用单引号把属性包起来,就可以很简单地在表达式中使用双引号

android:text='@{map["firstName"]}'

       也可以用双引号将属性包起来。这样的话,字符串常量就可以用 " 或者反引号 ( ` ) 来调用

android:text="@{map[`firstName`]}"

1.3.7、资源

       也可以在表达式中使用普通的语法来引用资源

android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"

       格式字符串和复数可以这样来实现

android:text="@{@string/nameFormat(firstName, lastName)}"
android:text="@{@plurals/banana(bananaCount)}"

       当复数形式有多个参数时,应该这样写

android:text="@{@plurals/orange(orangeCount, orangeCount)}"

关于android里面plurals复数资源的使用可以自行去百度下(例如一棵树是one tree, 两颗树是two trees。 为了解决后缀的问题,Android引入了plurals复数这种资源)

一些资源需要显示类型调用

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

二、Data Binding的使用

       上面咱们简单的了解了下Data Binding的语法,接下来就是Data Binding的使用了。

       Data Binding使用之前都要先获取Binding类实例(关于对应Binding类的名字则要看布局文件里面<data>标签class元素部分),所有Binding实例的生成都可以通过DataBindingUtil类里面的方法获取。

    private ActivityIncludeBindingBinding mBinding;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding = DataBindingUtil.setContentView(this, R.layout.activity_include_binding);
        initData();
    }

       如果我们之前使用setContentView()函数,现在使用DataBindingUtil.setContentView()函数同时返回一个Binding类对象。如果之前使用inflate()函数,现在则使用DataBindingUtil.inflate()函数同时返回Binding类对象。

2.1、去除findViewById

       使用Data Binding后,我们再也不需要findViewById,因为有id的view,都已经在Binding类中被初始化完成了,Binding类中会生成这个id对应的变量了。只需要通过Binding实例访问即可。我们只需要知道id生成对应View变量的名称的规则就行:除去下划线并且把下划线后面的第一个字母改成大写。比如android:id="@+id/text_name" 对应变量的名字就是 textName。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data class="NoFindViewByIdBinding">

        <variable
            name="user"
            type="com.tuacy.databindingdev.entity.User" />
    </data>

    <LinearLayout
        android:id="@+id/linear_parent"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="8dp"
            android:text="不通过findViewById去找控件对象了" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="8dp"
            android:layout_marginTop="8dp"
            android:gravity="center"
            android:text="点击按钮变成abc"
            android:textAllCaps="false"
            android:textSize="16sp" />

        <Button
            android:id="@+id/button_name"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:gravity="center"
            android:text="@{user.firstName}"
            android:textAllCaps="false"
            android:textSize="18sp" />
    </LinearLayout>
</layout>

这样在获取到Binding类之后咱们就可以通过Binding类(SimpleBinding)的textName来访问我们指定id的View了。

public class NoFindViewByIdActivity extends AppCompatActivity {

    public static void startUp(Context context) {
        context.startActivity(new Intent(context, NoFindViewByIdActivity.class));
    }

    private NoFindViewByIdBinding mBinding;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding = DataBindingUtil.setContentView(this, R.layout.activity_no_find_view_by_id);
        initData();
    }

    private void initData() {
        mBinding.setUser(new User("Test", "User"));

        /**
         * 通过mBinding就直接拿到了Button对象(布局文件里面已经写了id,android:id="@+id/button_name" 所以对象的名字是buttonName),
         * 不需要findViewById()了
         */
        mBinding.buttonName.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mBinding.buttonName.setText("abc");
            }
        });
    }

}

2.2、数据绑定

       数据绑定指的是咱们java代码里面的实体对象(POJO)和布局文件里面UI控件的数据对应起来。

先在布局文件中指定UI控件想要绑定的实体对象,然后再Activity中通过调用Binding类的set方法完成绑定。

2.2.1、单向绑定(实体对象POJO->UI控件)

       单向绑定我们指的就是把实体对象绑定到UI控件上。用法也很简单先在布局文件中指定UI控件想要绑定的实体对象,然后再Activity中通过调用Binding类的set方法完成绑定(可以参考文章开始的实例代码)。但是这种情况大部分情况都不能满足我们的需求。因为如果我们直接修改实体对象(POJO)中的数据,这些数据的改变并不会直接反应到布局文件的UI上。为了解决这个问题,Data Binding给了我们一套很好的通知机制,分别有三类: Observable objects(观察对象), observable fields(观察字段), observable collections(观察集合)。如果相应的对象、字段、集合中数据变化时候,那么UI也会相应的自动更新数据。

其实实体对象变化的时候相应的UI会不会刷新,关键在实体对象是否实现了Observable接口。

  • Observable Objects(观察对象)

       Observable Objects就是自定义一个对象实现Observable接口。因为Observable是个接口,Google也为我们提供了一个BaseObservable类,所以我们只要把自定义的类继承自BaseObservable就获得了通知UI更新数据的能力了,然后再getter方法上添加@Bindable注解,在setter方法中有数据变化的时候调用下notifyPropertyChanged通知UI更新数据。

public class UserObservable extends BaseObservable {

    private String userName;

    public UserObservable(String userName) {
        this.userName = userName;
    }

    @Bindable
    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
        notifyPropertyChanged(BR.userName);//通知变化
    }
}

       重点在自定义的类要继承自BaseObservable,然后在getter上添加@Bindable注解、在setter方法中调用notifyPropertyChanged提醒UI更新数据。里面notifyPropertyChanged()函数里面出现了BR类。BR类和咱们经常见到的R类一样是自动生成的,当咱们在getter上添加@Bindable注解之后BR类就自动生成了。

  • Observable Field(观察字段)

       Google也推出了ObservableField<T>类,T一般是java的基本类型数据,来帮助我们简化是实体类(POJO)。使用的时候也简单,如果之前的实体类里面的字段是String的话,咱们就换成ObservableField<String>。如下给出一个简单的实例。

public class UserField {

    private ObservableField<String> userName;
    private ObservableInt           age;

    public UserField(ObservableField<String> userName, ObservableInt age) {
        this.userName = userName;
        this.age = age;
    }

    public ObservableField<String> getUserName() {
        return userName;
    }

    public void setUserName(ObservableField<String> userName) {
        this.userName = userName;
    }

    public ObservableInt getAge() {
        return age;
    }

    public void setAge(ObservableInt age) {
        this.age = age;
    }
}

       调用ObservableField<T>的set()方法来设置新的数据,之后会自动通知数据刷新

mUserField.getUserName().set("数据变化了");

       除了ObservableField<T>之外咱们也可以使用不需要装箱和拆箱的(可以加快速度)封装类ObservableBoolean、ObservableByte、 ObservableChar、ObservableShort、ObservableInt、ObservableLong、ObservableFloat、ObservableDouble、ObservableParcelable来实现UI的自动刷新。

  • Observable Collections(观察集合)

       Google也为我们提供了一些通知类型的集合,有三种:ObservableArrayList<T>、ObservableArrayMap<K,V>、ObservableMap<K,V>,它和平常使用的List、Map用法一样,只是多了通知的功能。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data class="DataRefreshBinding">

        <import type="android.databinding.ObservableMap" />

        <variable
            name="userCollections"
            type="ObservableMap&lt;String, Object&gt;" />
    </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="8dp"
            android:layout_marginRight="8dp"
            android:text="通过Observable Collections(观察集合)实现数据刷新:" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:gravity="center"
            android:text='@{"姓名:" + userCollections.name + "年龄:" + userCollections.age}'
            android:textSize="18sp" />

        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:onClick="@{changeEvent.changeOnClick}"
            android:text="数据刷新" />
    </LinearLayout>
</layout>

绑定数据

    mBinding = DataBindingUtil.setContentView(this, R.layout.activity_data_refresh);
    mUserCollections = new ObservableArrayMap<>();
    mUserCollections.put("name", "tuacy");
    mUserCollections.put("age", 27);
    mBinding.setUserCollections(mUserCollections);

关于实体对象数据改变UI刷新的几个实例的简单写法可以参考下文给出的DEMO里面数据刷新界面。

2.2.2、双向绑定(实体对象POJO<->UI)

       要实现双向绑定其实也很简单,和单向绑定有一个细微的差别就是把"@{}"改成了"@={}"就实现了双向绑定,同时实例类要继承BaseObservable类。这里咱们通过一个简单的例子要说明

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data class="TwoWayBinding">

        <variable
            name="user"
            type="com.tuacy.databindingdev.simple.UserObservable"/>

        <variable
            name="changeString"
            type="String"/>

    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:layout_marginBottom="8dp"
            android:gravity="center"
            android:inputType="text"
            android:hint="请输入内容"
            android:text="@={user.userName}"/>

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:text="@{changeString}"/>
    </LinearLayout>
</layout>
        mBinding.setUser(mUser = new UserObservable("tuacy"));
        mUser.addOnPropertyChangedCallback(new Observable.OnPropertyChangedCallback() {
            @Override
            public void onPropertyChanged(Observable sender, int propertyId) {
                switch (propertyId) {
                    case BR.userName:
                        mBinding.setChangeString(mUser.getUserName());
                        break;
                }
            }
        });

2.2.3、自定义View实现双向绑定

       自定义View实现数据的绑定,先要确定我们需要绑定的属性。

       正向绑定很简单,实现自定义View实现对应的属性的getter和setter方法就行。咱们的关注点在反向绑定。需要实现反向绑定我们还需要额外的多做一些事情,在数据变化的时候咱还得通知数据改变。下面我们总结下自定义实现双向绑定的步骤

  1. 确定View的哪个属性需要双向绑定。
  2. 实现属性的getter,setter方法。
  3. 添加@BindingAdapter,设置监听。
  4. @InverseBindingAdapter关联数据变更。
  5. 触发监听

我们以一个具体的实例来感知下自定义View实现正向反向绑定。自定义CustomerTwoWayBindingView类绑定value属性。要注意@BindingAdapter、@InverseBindingAdapter的写法。

public class CustomerTwoWayBindingView extends View {

    private Context   mContext;
    private TextPaint mPaint;
    private String    value;

    public CustomerTwoWayBindingView(Context context) {
        this(context, null);
    }

    public CustomerTwoWayBindingView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CustomerTwoWayBindingView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mContext = context;
        initAttribute(attrs, defStyleAttr);
        init();
    }

    private void initAttribute(AttributeSet attrs, int defStyleAttr) {
        TypedArray typeArray = mContext.obtainStyledAttributes(attrs, R.styleable.CustomerTwoWayBindingView, defStyleAttr, 0);
        value = typeArray.getString(R.styleable.CustomerTwoWayBindingView_value);
        typeArray.recycle();
    }

    private void init() {
        mPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(Color.BLACK);
        mPaint.setTextSize(DensityUtils.sp2px(getContext(), 16));
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);
        if (!TextUtils.isEmpty(value)) {
            float valueWidth = mPaint.measureText(value, 0, value.length());
            canvas.drawText(value, getMeasuredWidth() / 2 - valueWidth / 2, getMeasuredHeight() / 2, mPaint);
        }
    }

    public String getValue() {
        return this.value;
    }

    public void setValue(String value) {
        this.value = value;
        postInvalidate();
        if (valueInverseBindingListener != null) {
            //通知数据改变
            valueInverseBindingListener.onChange();
        }
    }

    @BindingAdapter(value = "value", requireAll = false)
    public static void setValue(CustomerTwoWayBindingView view, String value) {
        //避免无限循环,数据不相同的时候我们才去改变数据,因为是双向绑定吗,数据改变通知View刷新,View刷新又通知数据改变
        if (TextUtils.isEmpty(view.getValue()) && TextUtils.isEmpty(value)) {
            return;
        }
        if (view.getValue() != null && view.getValue().equals(value)) {
            return;
        }
        view.setValue(value);
    }

    /**
     * 关联某个数据变更
     */
    @InverseBindingAdapter(attribute = "value", event = "valueAttrChanged")
    public static String getValue(CustomerTwoWayBindingView view) {
        return view.getValue();
    }

    private static InverseBindingListener valueInverseBindingListener;

    @BindingAdapter(value = {"valueAttrChanged"}, requireAll = false)
    public static void setValueChangedListener(CustomerTwoWayBindingView view, final InverseBindingListener bindingListener) {
        valueInverseBindingListener = bindingListener;
    }


}

对应布局文件,注意双向绑定需要写成 “@=”

<?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 class="CustomerTwoWayBinding">

        <variable
            name="user"
            type="com.tuacy.databindingdev.simple.UserObservable" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <com.tuacy.databindingdev.simple.CustomerBindingView
            android:id="@+id/customer_view"
            android:layout_width="match_parent"
            android:layout_height="64dp"
            app:value="@={user.userName}" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:text="@{user.userName}" />

        <Button
            android:id="@+id/button_change"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="改变数据"/>

    </LinearLayout>
</layout>

2.3、事件绑定

       Data Binding允许咱们处理View事件。大部分情况下事件属性名称都是由侦听器方法的名称确定。例如,View.OnClickListener有一个方法onClick(),所以这个事件的属性是android:onClick。当然也有一小部分例外。有一些针对click事件的特殊事件处理程序需要android之外的其他属性来避免onClick冲突。您可以使用以下属性来避免这些类型的冲突:

Class Listener setter Attribute
SearchView setOnSearchClickListener(View.OnClickListener) android:onSearchClick
ZoomControls setOnZoomInClickListener(View.OnClickListener) android:onZoomIn
ZoomControls setOnZoomOutClickListener(View.OnClickListener) android:onZoomOut

       咱们有两种机制来处理事件的绑定:Method References(方法调用)、Listener Bindings(监听绑定)。二者主要区别在于方法调用在编译时处理,而监听绑定于事件发生时处理。

编译时处理的话,有错误的话在编译的时候就会报出来。

2.3.1、Method References(方法调用)

       编译时处理。事件可以直接绑定到处理的方法。

定义一个类,申明咱们要绑定到的方法

    public class ButtonClickHandler {

        /**
         * Method References(方法调用)
         */
        public void onMethodButtonClick(View view) {
            mBinding.buttonMethodReferences.setText("点击了");
        }
        
    }

布局文件 注意绑定表达式 "@{buttonClick::onMethodButtonClick}"

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data class="EventBinding">

        <variable
            name="buttonClick"
            type="com.tuacy.databindingdev.simple.EventBindingActivity.ButtonClickHandler" />

    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="8dp"
            android:text="Method References(方法调用)绑定" />

        <Button
            android:id="@+id/button_method_references"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onClick="@{buttonClick::onMethodButtonClick}"
            android:text="绑定点击事件" />
        

    </LinearLayout>
</layout>

绑定

mBinding.setButtonClick(new ButtonClickHandler());

2.3.2、Listener Bindings(监听绑定)

       运行是处理。此功能适用于Gradle 2.0版及更高版本的Android Gradle插件。

申明一个类,定义我们要处理的方法

    public class ButtonClickHandler {

        /**
         * Listener Bindings(监听绑定)
         *
         * @param user 可以带参数
         */
        public void onBindingsButtonClick(User user) {
            mBinding.buttonListenerBindings.setText("点击了 " + user.getFirstName());
        }
    }

布局文件,注意绑定表达式 "@{() -> buttonClick.onBindingsButtonClick(user)}"

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data class="EventBinding">

        <variable
            name="user"
            type="com.tuacy.databindingdev.simple.User" />

        <variable
            name="buttonClick"
            type="com.tuacy.databindingdev.simple.EventBindingActivity.ButtonClickHandler" />

    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="8dp"
            android:text="Listener Bindings(监听绑定)绑定" />

        <Button
            android:id="@+id/button_listener_bindings"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onClick="@{() -> buttonClick.onBindingsButtonClick(user)}"
            android:text="绑定点击事件"
            android:textAllCaps="false"/>

    </LinearLayout>
</layout>

绑定

        mBinding.setButtonClick(new ButtonClickHandler());
        mBinding.setUser(new User("tuacy", "tuacy"));

       Data Biding的事件绑定,个人认为能不用就不用了,感觉还把代码变复杂了,还不好理解。上文中也提到了去除findViewById的方法,我们可以通过Binding类拿到View对象。然后咱们还是用原来的方式,自己去调用set函数设置监听。

2.4、Binding adapters

       Binding Adapter 所做的工作咱么可以简单的理解为就是去寻找合适的方法设置属性值。怎么讲,举个例子,比如布局文件里面有 android:text="@{user.firstName}" 那么当数据绑定的时候默认情况下会调用该View的setText()函数。当然了Binding Adapter能帮我们做的事情远不止这些。接下来咱们来总结下Binding Adapter的用法。

2.4.1 自动选择setter方法(默认情况)

       Data Binding 绑定属性的时候默认会去寻找该View对应的setter方法,android:text="@{user.firstName}"默认就会去找到该View的setText()方法来设置属性。非常好一点就是不仅仅支持原有的属性还支持不存在的属性。什么意思,app:name="@{name}" 也会默认去找setName()方法。如下例子

<?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 class="BindingAdapterBinding">

        <variable
            name="name"
            type="String" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <com.tuacy.databindingdev.adapter.AdapterTestView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:gravity="center"
            app:name="@{name}"
            android:textSize="18sp" />
    </LinearLayout>
</layout>
public class AdapterTestView extends AppCompatTextView {

    public AdapterTestView(Context context) {
        super(context);
    }

    public AdapterTestView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public AdapterTestView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public void setName(String name) {
        Log.d("tuacy", "setName setName = " + name);
        setText(name);
    }
}

2.4.2 指定一个自定义方法名称

       有些情况自动选择方法满足不了我们的需求, android:text="@{name}"默认回去选择setText()方法,但是我不想让他调用setText()方法,我想让他调用setName()。通过注解BindingMethods、BindingMethod也是可以做到的。

@BindingMethods({
    @BindingMethod(
        type = com.tuacy.databindingdev.adapter.AdapterTestView.class, 
        attribute = "android:text", 
        method = "setName"
    ),
})
public class AdapterTestView extends AppCompatTextView {

    public AdapterTestView(Context context) {
        super(context);
    }

    public AdapterTestView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public AdapterTestView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public void setName(String name) {
        Log.d("tuacy", "setName setName = " + name);
        setText(name);
    }
}

<?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 class="BindingAdapterBinding">

        <variable
            name="name"
            type="String" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <com.tuacy.databindingdev.adapter.AdapterTestView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:gravity="center"
            android:text="@{name}"
            android:textSize="18sp" />
    </LinearLayout>
</layout>

2.4.3 提供自定义的处理逻辑

       使用注释BindingAdapter的静态绑定方法允许我们去调用自定的函数。自定义处理逻辑。我们用ImageView调用Picasso加载图片一个实际的例子黎说明。

使用注释BindingAdapter的静态绑定方法loadImage, 这个静态方法随便你放在哪里。这里我们使用imageUrl作为属性

public class BindingAdapterModel {

    @BindingAdapter({"imageUrl"})
    public static void loadImage(ImageView view, String imageUrl) {
        Picasso.with(view.getContext()).load(imageUrl).into(view);
    }
}

Data Binding 表达式 ImageView 里面有app:imageUrl="@{viewModel}",这样当调用mBinding.setViewModel(item);绑定数据的时候会找到注释BindingAdapter的静态绑定方法loadImage()加载图片

<?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 class="RecyclerHolderItemBinding">
        <variable
            name="viewModel"
            type="String" />
    </data>

    <android.support.v7.widget.CardView

        android:layout_width="match_parent"
        android:layout_height="150dp"
        android:clickable="true"
        android:focusable="true"
        android:foreground="?android:selectableItemBackground"
        app:cardCornerRadius="4dp">

        <ImageView
            android:id="@+id/image_icon"
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:layout_gravity="center"
            app:imageUrl="@{viewModel}"
            android:contentDescription="@string/app_name" />

    </android.support.v7.widget.CardView>
</layout>

       接下来总结下注释BindingAdapter的正确使用方式

  • @BindingAdapter绑定单个属性
    @BindingAdapter({"imageUrl"})
    public static void loadImage(ImageView view, String imageUrl) {
        Picasso.with(view.getContext()).load(imageUrl).into(view);
    }

Data Binding框架会自动去找到静态方法loadImage,如果布局文件里面绑定表达式,对应的View正好是ImageView,并且绑定的属性是imageUrl(不在乎命名空间,app:imageUrl、tuacy:imageUrl都能匹配到),并且绑定的参数类型正好是String。

  • @BindingAdapter绑定多个属性

这种写法,规定在布局文件里面app:imageUrl, app:error一定要同时存在,才会去调用。

@BindingAdapter({"imageUrl", "error"})
public static void loadImage(ImageView view, String url, Drawable error) {
  Picasso.with(view.getContext()).load(url).error(error).into(view);
}

配合requireAll的使用,app:imageUrl, app:error不同时存在才会去调用,这里就是要注意空值的处理

@BindingAdapter(value={"imageUrl", "placeholder"}, requireAll=false)
public static void setImageUrl(ImageView imageView, String url, Drawable placeHolder) {
  if (url == null) {
    imageView.setImageDrawable(placeholder);
  } else {
    MyImageLoader.loadInto(imageView, url, placeholder);
  }
}
  • @BindingAdapter同时获取旧值和新值
           有的时候处理逻辑可能需要知道旧值,没关系@BindingAdapter也是支持的
@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());
   }
}

2.5、Data Binding配合ListView,RecyclerView使用

public class GridRecyclerAdapter extends RecyclerView.Adapter<GridRecyclerAdapter.ItemHolder> {

    private List<String> mDataList;

    public GridRecyclerAdapter(List<String> dataList) {
        mDataList = dataList;
    }

    @NonNull
    @Override
    public ItemHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        return new ItemHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_recycler_grid, parent, false));
    }

    @Override
    public void onBindViewHolder(@NonNull ItemHolder holder, int position) {
        holder.setBinding(mDataList.get(position));
    }

    @Override
    public int getItemCount() {
        return mDataList == null ? 0 : mDataList.size();
    }

    static class ItemHolder extends RecyclerView.ViewHolder {

        RecyclerHolderItemBinding mBinding;

        ItemHolder(View itemView) {
            super(itemView);
            mBinding = DataBindingUtil.bind(itemView);
        }

        public ItemHolder setBinding(String item) {
            mBinding.setViewModel(item);
            return this;
        }
    }

}

       本文涉及到的所有例子的下载地址

       关于Data Binding内容想说的就这些,最后还想说。Data Binding用起来不复杂,确实可以简化我们的代码。但是总感觉Android Studio对Data Binding支持的还不是那么的好,有的时候我们明明能编译过去,但是Android Studio还是会标红。而且布局文件里面有错误时候很难发现。

       如果大家有疑问,或者文章里面哪里写的有误的,可以留言。能力范围之内会一一解答的。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,100评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,862评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,993评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,309评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,303评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,421评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,830评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,501评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,689评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,506评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,564评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,286评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,826评论 3 305
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,875评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,114评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,705评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,269评论 2 341

推荐阅读更多精彩内容