Android-DataBinding详细学习

2017/8/8 posted in  Android

前言

谷歌开发了一个非常厉害的新框架DataBinding, 数据绑定框架给我们带来了很大的方便,以前我们可能需要在每个Activity里写很多的findViewById,不仅麻烦,还增加了代码的耦合性,如果我们使用DataBinding,就可以抛弃那么多的findViewById,省时省力。说到这里,其实网上也有很多快速的注解框架,但是注解框架与DataBinding想比还是不好用,而且官网文档说DataBinding还能提高解析XML的速度,其实DataBinding的好用,不仅仅体现在可以省去使用很多啰嗦findViewById,还有很多。往下看你就会明白的。

在介绍DataBinding之前,肯定要先学会搭建使用它的环境。在Android Studio上Databinding的使用还是很简单的。

环境搭建

Android 的 Gradle 插件版本不低于 1.5.0-alpha1:classpath 'com.android.tools.build:gradle:1.5.0'然后修改对应模块(Module)的 build.grade:

android {
    ....
    dataBinding {
        enabled = true
    }
}

注意:Android stuido 的版本一定要大于1.3,而且Android Studio目前对binding对象没有自动代码提示,只会在编译时进行检查。
就是这么简单,但是1.3及以前的版本,对于环境的搭建,可能就会麻烦一点(没事1.3的环境搭建方法,网上多得是)。

基础展示

我们在具体的讲解之前,先用一个简单的小例子来学习一下基础并展现一下DataBinding的巨大魅力,估计你会被其简单的特性所吸引哦。

首先我们先建立一个java bean,就是一个非常简单的用户类吧。

package loonggg.net.databinding.bean;
/**
 * Created by loongggdroid on 2016/3/14.
 */
public class User {
    private String name;
    private String age;
    public User(String name, String age) {
        this.name = name;
        this.age = age;
    }
    public void setName(String name) {
        this.name = name;
    }
    public void setAge(String age) {
        this.age = age;
    }
    public String getName() {
        return this.name;
    }
    public String getAge() {
        return this.age;
    }
}

其次,来看看使用了DataBinding之后的布局文件是什么样子的呢?主要的变化是在layout布局文件之中。

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

    <data>
        <variable
            name="user"
            type="loonggg.net.databinding.bean.User" />
    </data>

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

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.name}" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.age}" />
    </LinearLayout>
</layout>

看到这里,可能有的人就开始有点迷糊了,没有给控件定义id,而是用了@{ }的方法,到底是怎么回事?先不忙,一会再给你解释,我们再来看看怎么把值传进去的,在Activity中是如何使用的。

package loonggg.net.databinding;

import android.app.Activity;
import android.databinding.DataBindingUtil;
import android.os.Bundle;

import loonggg.net.databinding.bean.User;
import loonggg.net.databinding.databinding.ActivityMainBinding;

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        User user = new User("loonggg", "23");
        binding.setUser(user);
    }

}

看到Activity是不是感觉很简洁,很清爽,没有了控件的初始化findViewById,然后再去setText(), 就仅仅只添加了两行代码。运行结果,不用说,显而易见,肯定会显示loonggg和23。

基础用法

布局生命

例子我们介绍完了,对于例子中出现的一些新东西,有必要解释一下,java bean那里就不解释了,大家肯定都懂,我们就从布局文件讲起。相比以前使用的xml,根节点由具体的某个layout(比如LinearLayout )变成了layout,里面包括了data节点和传统的视图。这里的data节点就像是连接 View 和 Modle 的桥梁。在这个data节点中声明一个variable变量,那值就可以轻松传到布局文件中来了。

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

<!--type中声明的就是我们的用户实体类User,一定要写全,带着包名,我们给这个实体类命名为user-->
    <data>
        <variable
            name="user"
            type="loonggg.net.databinding.bean.User" />
    </data> 
    <LinearLayout>
     ……
    </LinearLayout>
</layout>

变量名为user变量类型为"loonggg.net.databinding.bean.User "

type中声明的就是我们的用户实体类User,一定要写全,带着包名,我们给这个实体类命名为user,TextView中的@{user.name}就是把这个user中的名字展示出来,age同样如此。

绑定Variable

虽然在布局文件中对应上了,但是值是怎么传进去的呢?这就是我们要将的Activity中的那两行代码了,它把实体类和布局文件进行了绑定。修改MainActivity中的onCreate,用 DatabindingUtil.setContentView() 来替换掉 setContentView(),然后创建一个 user 对象,通过 binding.setUser(user) 与 variable 进行绑定。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
    User user = new User("loonggg", "23");
    binding.setUser(user);
}

ActivityLayoutDetailBinding这个类是自动生成的和你的布局文件名字一样,如果你想要去改变名字的话

<layout xmlns:android="http://schemas.android.com/apk/res/android"  
    xmlns:bind="http://schemas.android.com/apk/res-auto">  
  
    <!--这里你也可以为Binding类进行命名,有三种形式  
        1、Custom:会在databinding包下  
        2、.Custom:会在当前的包名下创建  
        3、com.andly.Custom:会在指定的包名下进行创建-->  
    <data class="CustomBinding"></data> //在app_package/databinding下生成CustomBinding;
    <data class=".CustomBinding"></data> //在app_package下生成CustomBinding;
    <data class="com.example.CustomBinding"></data> // 明确指定包名和类名。

高级用法

import用法

其实data节点也是支持import用法的,比如:

<data>
     <import type="loonggg.net.databinding.bean.User "/>
     <variablename="user"type="User" />
</data>

这里需要注意的是:import并不能像java 一样可以 import xx.xxx.*,必须具体到写清楚每个要导入的类名。

类名相同

到这里你可能会问如果import了两个相同名称的类咋办?别怕,人家想的很周到,可以起个别名或者昵称嘛!例如:

···
<data> 
  <import type="xxx.User" alias="MyUser"> 
  <import type="xxx.xx.User"> 

   <variable 
            name="user" 
            type="User" /> 
    <variable 
            name="myUser" 
            type="MyUser"/> 
</data> 
<TextView xxx:@{myUser.getName()}> 
<TextView xxx:@{user.getName()}>
···

变量定义的高级用法

在上面,我们学会了如何去在xml中定义一些简单的变量。我们没有定义像List、Map等这样的集合变量。那这种集合变量该如何定义呢?其实定义的方式和我们上面的基本一致,区别就在于我们还需要为它定义key的变量,例如:

<layout xmlns:android="http://schemas.android.com/apk/res/android"> 
    <data> 
        <import type="android.graphics.Bitmap" /> 
        <import type="java.util.ArrayList" /> 
        <import type="java.util.HashMap" />  
        <!-- 集合的定义 -->
        <variable 
            name="list" 
            type="ArrayList&lt;String>" /> // 左尖括号需要转义
        <variable 
            name="map" 
            type="HashMap&lt;String, String>" /> 
        <variable 
            name="array" 
            type="String[]" /> 
        <!-- 为集合定义对应的索引 -->
        <variable 
            name="listKey" 
            type="int" /> 
        <variable 
            name="mapKey" 
            type="String" /> 
        <variable 
            name="arrayKey" 
            type="int" /> 
        <!-- 字符串,布尔值和int的用法-->
        <variable 
            name="str" 
            type="String"/> 
        <variable 
            name="error" 
            type="boolean"/> 
        <variable 
            name="num" 
            type="int" /> 
    </data> 
    <LinearLayout 
        android:orientation="vertical" 
        android:layout_width="match_parent" 
        android:layout_height="wrap_content"> 

        <TextView 
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content" 
            android:text="@{list[listKey]}"/> 
        <TextView 
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content" 
            android:text="@{map[`name`]}"/> 
        <TextView 
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content" 
            android:text="@{array[0]}"/> 
        <TextView 
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content" 
            android:text="@{str}"/> 
        <TextView 
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content" 
            android:text="@{String.valueOf(num)}"/> 
    </LinearLayout> 
</layout>

在引用的同时也可以加上简单的逻辑运算

<!--数据绑定将自动检测null异常,如果你的表达式为null,它将会给它赋值为(null)  
 如果为int类型则默认为0-->  
 <!--之前都是写三元运算符的形式,当然在数据绑定中也能够使用,但更推荐下面那种-->  
 <TextView  
     android:layout_width="match_parent"  
     android:layout_height="wrap_content"  
     android:text="@{boo?note:null}" />  
 <!--?? :null合并运算符,当左边为null会显示右边-->  
 <TextView  
     android:layout_width="match_parent"  
     android:layout_height="wrap_content"  
     android:text="@{note??null}"  
     android:textColor="#00FF00"  
     android:textSize="18sp" />  
<!--引用资源文件-->  
<ImageView  
    android:layout_width="wrap_content"  
    android:layout_height="wrap_content"  
    android:paddingLeft="@{boo?@dimen/large_padding:@dimen/small_padding}"  
    android:src="@{image}" /> 
<TextView  
    android:layout_width="match_parent"  
    android:layout_height="wrap_content"  
    android:text='@{String.valueOf(map[`one`])}'/>  

Observable数据改变自动更新

Observable是一个接口,它的子类有BaseObservable,ObservableField,ObservableBoolean,ObservableByte, ObservableChar, ObservableShort, ObservableInt, ObservableLong, ObservableFloat, ObservableDouble, and ObservableParcelableObservableArrayList,ObservableArrayMap
现在我们来看看 如何在数据改变时,不手动设置,让其自动改变:

布局文件

<?xml version="1.0" encoding="utf-8"?><!--布局以layout作为根布局-->
<layout>
    <data>
        <import type="www.zhang.com.databinding.model.Person" />
        <variable
            name="person"
            type="Person" />
    </data>
    <!--我们需要展示的布局-->
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <TextView
            android:layout_width="match_parent"
            android:layout_height="56dp"
            android:text="@{`firstName:`+person.firstName}" />
        <TextView
            android:layout_width="match_parent"
            android:layout_height="56dp"
            android:text="@{`lastName:`+person.lastName}" />
        <TextView
            android:layout_width="match_parent"
            android:layout_height="56dp"
            android:text="@{`age:`+person.age}" />
        <Button
            android:id="@+id/second_btn1"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="改变数据" />
    </LinearLayout>
</layout>

Person类

package www.zhang.com.databinding.model;

import android.databinding.BaseObservable;
import android.databinding.Bindable;

import www.zhang.com.databinding.BR;

public class Person extends BaseObservable {
    private String firstName;
    private String lastName;
    private int age;
    public Person(String firstName, String lastName, int age) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }
    @Bindable
    public String getFirstName() {
        return firstName;
    }
    public void setFirstName(String firstName) {
        this.firstName = firstName;
        notifyPropertyChanged(BR.firstName);
    }
    @Bindable
    public String getLastName() {
        return lastName;
    }
    public void setLastName(String lastName) {
        this.lastName = lastName;
        notifyPropertyChanged(BR.lastName);
    }
    @Bindable
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
        notifyPropertyChanged(BR.age);
    }
}

Model类继承BaseObservable,BaseObservable实现 Android.databinding.Observable接口,Observable接口可以允许附加一个监听器到model对象以便监听对象上的所有属性的变化。

Observable接口有一个机制来添加和删除监听器,但通知与否由开发人员管理。为了使开发更容易,BaseObservable实现了监听器注册机制。DataBinding实现类依然负责通知当属性改变时。这是通过指定一个Bindable注解给getter以及setter内通知来完成的。

notifyPropertyChanged(BR.参数名)通知更新这一个参数,需要与@Bindable注解配合使用。notifyChange()通知更新所有参数,可以不用和@Bindable注解配合使用

SecondActivity

public class SecondActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivitySecondBinding binding = DataBindingUtil.setContentView(SecondActivity.this, R.layout.activity_second);

        final Person person = new Person("zhang","san",38);
        binding.setPerson(person);

        binding.secondBtn1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                person.setFirstName("li");
                person.setLastName("si");
                person.setAge(40);
            }
        });
    }
}

示例

<?xml version="1.0" encoding="utf-8"?><!--布局以layout作为根布局-->
<layout>
    <data>
        <import type="www.zhang.com.databinding.model.Animal"/>
        <variable
            name="animal"
            type="Animal"/>
        <variable
            name="list"
            type="android.databinding.ObservableArrayList&lt;String&gt;"/>
        <variable
            name="map"
            type="android.databinding.ObservableArrayMap&lt;String,String&gt;"/>
    </data>
    <!--我们需要展示的布局-->
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <TextView
            android:layout_width="match_parent"
            android:layout_height="56dp"
            android:text="@{animal.field}" />
        <TextView
            android:layout_width="match_parent"
            android:layout_height="56dp"
            android:text="@{String.valueOf(animal.age)}" />
        <TextView
            android:layout_width="match_parent"
            android:layout_height="56dp"
            android:text="@{list[0]}" />
        <!--Map集合既可以通过map[key]的方式,也可以通过调用API-->
        <TextView
            android:layout_width="match_parent"
            android:layout_height="56dp"
            android:text="@{list[1]}" />
        <TextView
            android:layout_width="match_parent"
            android:layout_height="56dp"
            android:text="@{map[`name`]}" />
        <TextView
            android:layout_width="match_parent"
            android:layout_height="56dp"
            android:text="@{map[`age`]}" />
        <Button
            android:id="@+id/four_btn"
            android:layout_width="match_parent"
            android:text="改变数据"
            android:layout_height="wrap_content" />
    </LinearLayout>
</layout>

Animal类

public class Animal {
    public final ObservableField<String> field = new ObservableField<>();
    public final ObservableInt age = new ObservableInt();
}

FourActivity

package www.zhang.com.databinding;

public class FourActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityFourBinding binding = DataBindingUtil.setContentView(FourActivity.this, R.layout.activity_four);
        final Animal animal = new Animal();

        animal.field.set("cat");
        animal.age.set(2);
        binding.setAnimal(animal);

        final ObservableArrayList<String> list = new ObservableArrayList<>();
        list.add("dog");
        list.add("mouse");
        binding.setList(list);

        final ObservableArrayMap<String, String> map = new ObservableArrayMap<>();
        map.put("name","Tom");
        map.put("age","4");
        binding.setMap(map);

        binding.fourBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                animal.field.set("dog");
                animal.age.set(4);
                list.set(0,"cat");
                list.set(1,"dog");
                map.put("name","Sam");
                map.put("age","5");
            }
        });

    }
}

当Animal属性数据改变,list/map集合数据改变,会自动更新数据,我们不需要自己手动设置,省去了一个操作,让我们更专注于业务逻辑。

事件的绑定

<!--  
    绑定事件的格式有两种:    
    1、方法引用:直接用handle.Click或者handle::Click  推荐后者  
-->  
<Button  
    android:layout_width="match_parent"  
    android:layout_height="wrap_content"  
    android:text="@{user.name}"  
    android:onClick="@{handle::Click}"/>  
<!--  
     2、监听绑定:使用()组,括号里面所填的是你为参数起的名字,这样你就可以在后面的括号进行引用  
        如果你监听的事件需要返回值,那么你的方法也要返回一个相同类型  
 -->  
 <Button  
     android:layout_width="match_parent"  
     android:layout_height="wrap_content"  
     android:onClick="@{()->handle.eventHandler(user)}"  
     android:text="传入布局文件中的数据" />  
  
 <Button  
     android:layout_width="match_parent"  
     android:layout_height="wrap_content"  
     android:onClick="@{(thisView)->handle.eventHandlerView(thisView,user)}"  
     android:text="传入此View" />  
 <!--如果你需要为一个点击事件设置一个断言,那么使用void作为一个标志,表示什么也不做-->  
 <Button  
     android:layout_width="match_parent"  
     android:layout_height="wrap_content"  
     android:onClick="@{(v)->handle.isVisible(v)?handle.doSomething():void}"  
     android:text="判断是否为visible" />  
  
 <!--对于一些控件有自己专门的单击事件,需要创建下面的属性进行避免  
    SearchView    android:onSearchClick  
    ZoomControls  android:onZoomIn  
    ZoomControls  android:onZoomOut-->  
 <SearchView  
     android:layout_width="match_parent"  
     android:layout_height="wrap_content"  
     android:onClick="@{(v)->handle.searchClick(v)}"  
     android:onSearchClick="@{(v)->handle.onSearchClick(v)}">  
  
 </SearchView>  

实现监听方法,保证参数个数、类型、返回值都要保证和你使用set时监听一样,不然就出报错。

public void checkChanged(View view, boolean isCheck) {  
    System.out.println("checkChanged:" + view + "    " + isCheck);  
}  
  
public boolean longClick(View view) {  
    System.out.println("longClick:" + view);  
    return true;  
}  

可以直接在 xml 导入android.view.View.OnClickListener,并制定其点击事件。

<variable
    name="clickListener"
    type="android.view.View.OnClickListener" />
...
  android:onClick="@{clickListener}"
...
holder.binding.setClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
               //do something
        });

表达式

其实在xml文件中还是支持表达式的,比如说如下:

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text='@{error ? "error" : "ok"}'/>

这是一个布尔值的使用。

下面我们就来看看表达式支持的一下语法和不支持的语法支持的表达式:

  • Mathematical + - / * %
  • String concatenation +
  • Logical && ||
  • Binary & | ^
  • Unary + - ! ~
  • Shift >> >>> <<
  • Comparison == > < >= <=
  • instanceof
  • Grouping ()
  • Literals - character, String, numeric, null
  • Cast
  • Method calls
  • Field access
  • Array access []
  • Ternary operator ?:

不支持的表达式:

  • this
  • super
  • new
  • Explicit generic invocation

在布局中使用include

如果你需要用到从xml传过来的数据需要去使用bind:user属性,这里的user是你定义的实体类名

<!--当你使用include的时候,你可以使用命名空间和属性中的变量名  
来将数据传送到另一个布局中去,值得注意的是当include的父节点为merge时将不支持-->  
<include  
    layout="@layout/detail_include"  
    bind:user="@{user}" /> 

然后只需要在include布局里面声明之后便可以直接使用了。

示例

activity_five.xml

<?xml version="1.0" encoding="utf-8"?><!--布局以layout作为根布局-->
<layout>

    <data >
        <import type="www.zhang.com.databinding.model.Content"/>
        <variable
            name="con"
            type="Content"/>
    </data>
    <!--我们需要展示的布局-->
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:bind="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <include
            android:id="@+id/toolbar"
            layout="@layout/toolbar"
            android:layout_height="56dp"
            android:layout_width="match_parent"
            bind:content="@{con}" />
 <!--通过命名空间将写有toolbar的xml文件中定义的content对象作为属性绑定con对象,这2个对象是同一个类-->
            <TextView
                android:text="@string/app_name"
                android:layout_width="match_parent"
                android:layout_height="wrap_content" />
    </LinearLayout>
</layout>

toolbar.xml

<?xml version="1.0" encoding="utf-8"?>
<layout >
    <data>
        <import type="www.zhang.com.databinding.model.Content"/>
      <variable
          name="content"
          type="Content"/>
    </data>

<android.support.v7.widget.Toolbar
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/toolbar"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_height="56dp"
    android:layout_width="match_parent"
    app:title="@{content.title}"
    app:subtitle="@{content.subTitle}"
    android:background="@color/colorPrimary"
    app:titleTextColor="@android:color/white"
    app:subtitleTextColor="@android:color/white" />
</layout>

在activity_five.xml中的include属性中定义了一个id,同时又在toolbar.xml中的Toolbar标签中又定义了一个id,其作用是通过binding.toolbar.toolbar等同于Toolbar控件,可以方便做一些操作等(不加id,同样能将变量参数传进去)
FiveActivity中

public class FiveActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        supportRequestWindowFeature(Window.FEATURE_NO_TITLE);

        ActivityFiveBinding binding =DataBindingUtil.setContentView(FiveActivity.this, R.layout.activity_five);

        Content con = new Content("Title","SubTitle");
        binding.setCon(con);

//        binding.toolbar.setContent(con);  //这个测试没有效果,不会显示toolbar的title/subTitle
//        binding.toolbar.toolbar.setTitle(""); 
//        binding.toolbar.toolbar.setSubtitle("");

        //下面的代码也可以通过DataBinding绑定数据
        binding.toolbar.toolbar.setNavigationIcon(R.mipmap.ic_launcher);
        setSupportActionBar(binding.toolbar.toolbar);
        binding.toolbar.toolbar.setNavigationOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                finish();
            }
        });
    }
}

Content

public class Content extends BaseObservable {
    private String title;
    private String subTitle;

    public Content(String title, String subTitle) {
        this.title = title;
        this.subTitle = subTitle;
    }

    @Bindable public String getSubTitle() {
        return subTitle;
    }

    public void setSubTitle(String subTitle) {
        this.subTitle = subTitle;
        notifyPropertyChanged(BR.subTitle);
    }

    @Bindable public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
        notifyPropertyChanged(BR.title);
    }
}

带id的控件的赋值方式

对于xml文件中控件的赋值,其实也是可以在java文件中,用java来实现的。我就以最上面那个简单的小例子来说,假如给一个TextView设置的id如下:

<TextView
    android:id="@+id/name"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    />

在Activity中这样赋值:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
    binding.name.setText("非著名程序员");
}

在布局中使用ViewStub

/** 
 * 为ViewStub设置监听,当显示的时候为它绑定数据,因为当不显示的ViewStub会在视图中消失 
 */  
vs = (ViewStub) findViewById(R.id.viewstub);  
vs.setOnInflateListener(new ViewStub.OnInflateListener() {  
    @Override  
    public void onInflate(ViewStub stub, View inflated) {  
        ViewstubBinding viewstubBinding = ViewstubBinding.bind(inflated);  
        Info info = new Info();  
        info.setInfo("Andly Info");  
        viewstubBinding.setInfo(info);  
        Drawable d = getResources().getDrawable(R.mipmap.ic_launcher);  
        viewstubBinding.setDrawable(d);  
    }  
});  
  
public void toggleViewStub(View view) {  
    vs.inflate();  
}  

在布局中使用RecycleView控件

1、添加RecycleView控件

<!--  
    这里使用到了自定义属性,因为RecycleView里面有setAdapter方法,所以这里可以直接用app:adapter  
-->  
<android.support.v7.widget.RecyclerView  
    android:id="@+id/rv"  
    android:layout_width="match_parent"  
    android:layout_height="match_parent"  
    app:adapter="@{adapter}" />  

2、为RecycleView定义适配器

@Override  
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {  
    ViewDataBinding viewDataBinding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), layoutId, parent, false);  
    return new ViewHolder(viewDataBinding);  
}  
@Override  
public void onBindViewHolder(ViewHolder holder, int position) {  
    holder.binding.setVariable(variable, list.get(position));  
    holder.binding.executePendingBindings();  
    //当然这里你也可以为其设置点击如:  
    //holder.binding.getRoot.setOnclickListener()  
}  
@Override  
public int getItemCount() {  
    return list.size();  
}  
  
class ViewHolder extends RecyclerView.ViewHolder {  
    ViewDataBinding binding;  
  
    public ViewHolder(ViewDataBinding binding) {  
        super(binding.getRoot());  
        this.binding = binding;  
    }  
}  

3、为RecycleView设置Adapter

//这里注意的是一定要是BR.dataInfo不能是其它的常数  
MyAdapter adapter = new MyAdapter(list, R.layout.rv_item, BR.dataInfo);  
binding.setAdapter(adapter);  
binding.rv.setLayoutManager(new LinearLayoutManager(this));

这样就大功告成,然而在很多情况我们都需要去对每个Item进行处理,如显示网络图片等等,这里我们就需要使用数据绑定自定义属性的功能,看代码

<ImageView  
    android:layout_width="150dp"  
    android:layout_height="90dp"  
    app:imageError="@{@drawable/android}"  
    app:imagePath="@{dataInfo.imageUrl}" />  
//当你在一个方法只需要一个参数的时候可以使用@BindingAdapter("imageUrlStr"),加上之后就可以在布局文件中直接使用imageUrlStr  
//运行之后就会调用loadImage方法  
@BindingAdapter("imageUrlStr")  
public static void loadImage(ImageView iv, String url) {  
    Glide.with(iv.getContext()).load(url).into(iv);//这里使用Glide库  
}  
  
//上面是为loadImage传入一个参数,当传入两个或多个参数的时候应使用@BindingAdapter({"imagePath", "imageError"})  
//这个的ImageView自定义了两个属性一个是imagePath传入的是url,一个是imageError为Drawable  
@BindingAdapter({"imagePath", "imageError"})  
public static void downloadImage(ImageView iv, String url, Drawable error) {  
    Glide.with(iv.getContext()).load(url).error(error).into(iv);  
}  

上面的方法使用的是静态方法,如果你不想使用静态方法你需要重写一个数据绑定组件类去实现DataBindingComponent

public class MyComponent implements android.databinding.DataBindingComponent {  
    private Utils utils;  
    @Override  
    public Utils getUtils() {  
        if (utils == null) {  
            utils = new Utils();  
        }  
        return utils;  
    }  
}  

然后你需要在Activity为其进行设置

//第一种方式  
DataBindingUtil.setDefaultComponent(new MyComponent());  
//第二种方式  
ActivityMyListViewBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_my_list_view,new MyComponent());  
//第三种方式  
DataBindingUtil.bind(root,new MyComponent());  

最后有个不起眼的小功能,就是当使用数据绑定的时候在预览界面不能看到显示的内容,这时你可以为你的控件设置默认显示内容android:text="@{placeName,default=PLACEHOLDER}"

示例

activity_five.xml

<?xml version="1.0" encoding="utf-8"?><!--布局以layout作为根布局-->
<layout>

    <data >
        <import type="www.zhang.com.databinding.model.Content"/>
        <variable
            name="con"
            type="Content"/>
    </data>
    <!--我们需要展示的布局-->
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:bind="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <include
            android:id="@+id/toolbar"
            layout="@layout/toolbar"
            android:layout_height="56dp"
            android:layout_width="match_parent"
            bind:content="@{con}" />
        <!--通过命名空间将写有toolbar的xml文件中定义的content对象作为属性绑定con对象,这2个对象是同一个类-->
            <android.support.v7.widget.RecyclerView
                android:id="@+id/recycler"
                android:layout_width="match_parent"
                android:layout_height="wrap_content" />
    </LinearLayout>
</layout>

recycler_item.xml

<?xml version="1.0" encoding="utf-8"?>
<layout>
    <data>
        <variable
            name="str"
            type="String"/>
    </data>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:orientation="vertical">

        <TextView
            android:text="@{str}"
            android:gravity="center_vertical"
            android:textColor="@android:color/black"
            android:textSize="16sp"
            android:layout_width="match_parent"
            android:layout_height="48dp" />
    </LinearLayout>
</layout>

FiveActivity

public class FiveActivity extends AppCompatActivity {

    private ActivityFiveBinding binding;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        supportRequestWindowFeature(Window.FEATURE_NO_TITLE);

        binding = DataBindingUtil.setContentView(FiveActivity.this, R.layout.activity_five);

        initToolbar();
        initRecyclerView();
    }

    private void initRecyclerView() {
        LinearLayoutManager manager = new LinearLayoutManager(FiveActivity.this);
        binding.recycler.setLayoutManager(manager);
        binding.recycler.setHasFixedSize(true);
        MyAdapter adapter = new MyAdapter(getApplicationContext());
        binding.recycler.setAdapter(adapter);
    }

    private void initToolbar() {
        Content con = new Content("Title","SubTitle");
        binding.setCon(con);

//        binding.toolbar.setContent(con);  //这个测试没有效果,不会显示toolbar的title/subTitle
//        binding.toolbar.toolbar.setTitle("");
//        binding.toolbar.toolbar.setSubtitle("");

        //下面的代码也可以通过DataBinding绑定数据
        binding.toolbar.toolbar.setNavigationIcon(R.mipmap.ic_launcher);
        setSupportActionBar(binding.toolbar.toolbar);
        binding.toolbar.toolbar.setNavigationOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                finish();
            }
        });
    }
}

MyAdapter

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {

    private Context mContext;
    String[] datas;

    public MyAdapter(Context context) {
        mContext = context;
        datas = context.getResources().getStringArray(R.array.item_list);
    }

    @Override
    public MyAdapter.MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        RecyclerItemBinding binding = DataBindingUtil.inflate(LayoutInflater.from(mContext), R.layout.recycler_item, parent, false);
        return new MyViewHolder(binding);
    }

    @Override
    public void onBindViewHolder(MyAdapter.MyViewHolder holder, int position) {
        String name = datas[position];
        holder.getBinding().setVariable(www.zhang.com.databinding.BR.str,name);
        //holder.getBinding().setStr(name); //两者都可以

        //executePendingBindings()方法说明
        // When a variable or observable changes, the binding will be scheduled to change before the next frame. 
        // There are times, however, when binding must be executed immediately. 
        // To force execution, use the executePendingBindings() method.
        holder.getBinding().executePendingBindings();//此方法必须执行在UI线程,当绑定的数据修改时更新视图(不知道翻译的准不准)
    }

    @Override
    public int getItemCount() {
        return datas.length;
    }

    public class MyViewHolder extends RecyclerView.ViewHolder {
        private RecyclerItemBinding binding;

        public MyViewHolder(ViewDataBinding binding) {
            super(binding.getRoot());
            this.binding = (RecyclerItemBinding) binding;
        }

        public RecyclerItemBinding getBinding() {
            return binding;
        }

        public void setBinding(RecyclerItemBinding binding) {
            this.binding = binding;
        }
    }
}

@BindingAdapter

@BindingAdapter用于修饰方法。

一些属性需要定制绑定逻辑,一个用@BindingAdapter修饰的静态方法可以自定义属性的setter操作。

android自身实现了大量的Adapter,你可以在项目module的android.databinding.adapters包下找到这些代码。

public class CardViewBindingAdapter {
    @BindingAdapter("contentPadding")
    public static void setContentPadding(CardView view, int padding) {
        view.setContentPadding(padding, padding, padding, padding);
    }
}

方法内的参数可以设置多个,参数的传递在布局文件中使用自定义命名空间的属性传入。

  • 1、默认的你的自定义的命名空间,在匹配时会被忽略。
@BindingAdapter("contentPadding")
  • 2、允许重写android的命名空间。
 @BindingAdapter("android:contentPadding")

app:contentPaddingandroid:contentPadding处理行为可以不一样。
app:contentPaddingcustom:contentPadding处理行为是一致的。(仅android是特殊的命名空间)。

示例

@BindingAdapter({"imageUrl", "type", "position", "debrisfly", "width", "height"})
public static void setImageUrl(SimpleDraweeView view, String url, int type, int position, boolean debrisfly, int width, int height) {
    if (url == null) {
        url = "";
    }
    Uri uri = null;
    switch (type) {
        case 1:
            uri = Util.parse7(url, BocaiApplication.getInstance());
            break;
        case 2:
            uri = Util.parse2(url, BocaiApplication.getInstance());
            break;
        case 3:
            uri = Util.parse3(url, BocaiApplication.getInstance());
            break;

        case 8:
            uri = Util.parse(url, BocaiApplication.getInstance());
            break;
        case 10:
            uri = Util.parseUrlBy230(url, BocaiApplication.getInstance());
            break;
    }
    ControllerListener controllerListener = new BaseControllerListener<ImageInfo>() {
        @Override
        public void onFinalImageSet(
                String id,
                @Nullable ImageInfo imageInfo,
                @Nullable Animatable anim) {
            if (imageInfo == null) {
                return;
            }
            if (anim != null) {
                anim.start();
            }
        }

        @Override
        public void onIntermediateImageSet(String id, @Nullable ImageInfo imageInfo) {
        }

        @Override
        public void onFailure(String id, Throwable throwable) {
        }
    };

    DraweeController controller = Fresco.newDraweeControllerBuilder()
            .setControllerListener(controllerListener)
            .setUri(uri)
            .setOldController(view.getController())
            // other setters
            .build();
    view.setController(controller);
    debrisflys(debrisfly, position,view,width,height);
}

需要注意,当你创建的适配器属性与系统默认的产生冲突时,你的自定义适配器将会覆盖掉系统原先定义的注解,这将会产生一些意外的问题。
假设需要对下面接口,做适配。

public interface ILogAction{
      void login();
      void logout();
}

则需要一个方法一个接口,这么做的原因是避免login()的修改影响到logout()。所以根据业务需要,可能需要排列组合适配这两个接口。

1、适配 login
2、适配 logout
3、适配 login + logout

@BindingBuildInfo

@BindingBuildInfo(
buildId="3fefc6ba-1e95-4dcf-8ffa-278fe0f449bd",
modulePackage="com.ipudong.library",
sdkRoot="/Users/robert/Library/Android/sdk",
layoutInfoDir="/Users/robert/android/develops/pudong-d-android/lib_basic/build/intermediates/data-binding-info/debug",
exportClassListTo="/Users/robert/android/develops/pudong-d-android/lib_basic/build/intermediates/data-binding-info/debug/_generated.txt",
isLibrary=true,
minSdk=14,
enableDebugLogs=false,
printEncodedError=true
)
public class DataBindingInfo {}

在SOURCE阶段会自动生成DataBindingInfo.class,并标记注解如上。

@BindingConversion

Annotate methods that are used to automatically convert from the expression type to the value used in the setter.

有时候会遇到类型不匹配的问题,比如R.color.whiteint,但是通过Data Binding赋值给android:background属性后,需要把int转换为ColorDrawable

@BindingConversion
public static Drawable convertColorToDrawable(int drawable) {
  return new ColorDrawable(drawable);
}

@BindingMethod && @BindingMethods

Used within an BindingMethods annotation to describe a renaming of an attribute to the setter used to set that attribute.Used to enumerate attribute-to-setter renaming.

@BindingMethods用于修饰类。

一些属性虽然拥有setters但是并不与名字相匹配,这些方法的属性可以通过 @BindingMethod && @BindingMethods 注释 setters。

@BindingMethods({
       @BindingMethod(type = "android.widget.ImageView",
                      attribute = "android:tint",
                      method = "setImageTintList"),
})

开发人员不太可能需要重命名 setters ,因为android框架属性已经实现了这一部分。

事件的默认值是带有AttrChanged的属性名称。在上面的例子中,默认值是android:textAttrChanged,即使它没有提供。

事件属性用于通知数据绑定系统值已更改。开发人员通常会创建一个BindingAdapter来分配事件。比如:

@BindingAdapter(value = {"android:beforeTextChanged", "android:onTextChanged",
                          "android:afterTextChanged", "android:textAttrChanged"},
                          requireAll = false)
 public static void setTextWatcher(TextView view, final BeforeTextChanged before,
                                   final OnTextChanged on, final AfterTextChanged after,
                                   final InverseBindingListener textAttrChanged) {
     TextWatcher newValue = new TextWatcher() {
         ...
         @Override
         public void onTextChanged(CharSequence s, int start, int before, int count) {
             if (on != null) {
                 on.onTextChanged(s, start, before, count);
             }
             if (textAttrChanged != null) {
                 textAttrChanged.onChange();
             }
         }
     }
     TextWatcher oldValue = ListenerUtil.trackListener(view, newValue, R.id.textWatcher);
     if (oldValue != null) {
         view.removeTextChangedListener(oldValue);
     }
     view.addTextChangedListener(newValue);
 }

如同BindingAdapters一样, InverseBindingAdapter方法 也可以将 DataBindingComponent作为第一个参数,可以是具有从DataBindingComponent检索的实例的实例方法。

InverseBindingListener非常有用。

参考 InverseBindingListener

@InverseBindingMethod

InverseBindingMethod用于标识如何监听对View属性的更改以及要调用的getter方法。InverseBindingMethod 应该与InverseBindingMethods的部分方法相关联。

@InverseBindingMethods({@InverseBindingMethod(
     type = android.widget.TextView.class,
     attribute = "android:text",
     event = "android:textAttrChanged",
     method = "getText")})
 public class MyTextViewBindingAdapters { ... }

@InverseBindingMethods中的属性method 是可选的。

如果其没有提供, 属性名称会查找如下几种可能性:方法名称,前缀为is或者get的方法名称。 如属性android:text, 数据绑定框架会在TextView中搜索public CharSequence getText() 方法。

@InverseBindingMethods中的属性event是可选的。

如果其没有提供,默认会使用属性名+AttrChanged后缀。如属性android:text, 默认的事件名称android:textAttrChanged。

这个事件也需要配置相关的@BindingAdapter,如下:

@BindingAdapter(value = {"android:beforeTextChanged", "android:onTextChanged",
                          "android:afterTextChanged", "android:textAttrChanged"},
                          requireAll = false)
 public static void setTextWatcher(TextView view, final BeforeTextChanged before,
                                   final OnTextChanged on, final AfterTextChanged after,
                                   final InverseBindingListener textAttrChanged) {
     TextWatcher newValue = new TextWatcher() {
         ...
         @Override
         public void onTextChanged(CharSequence s, int start, int before, int count) {
             if (on != null) {
                 on.onTextChanged(s, start, before, count);
             }
             if (textAttrChanged != null) {
                 textAttrChanged.onChange();
             }
         }
     }
     TextWatcher oldValue = ListenerUtil.trackListener(view, newValue, R.id.textWatcher);
     if (oldValue != null) {
         view.removeTextChangedListener(oldValue);
     }
     view.addTextChangedListener(newValue);
 }

@InverseBindingMethods

用于枚举属性,getter和事件关联。

@Untaggable

Data Binding相关的jar包由四部分组成,

  1. baseLibrary-2.1.0-rc1.jar
    作为运行时类库被打进APK中;
  2. DataBinderPlugin(gradle plugin)
    在编译期使用,利用gradle-api(之前叫transform-api,1.5生,2.0改名)处理xml文件,生成DataBindingInfo.java;
  3. compiler-2.1.0-rc1.jar
    在编译器使用,入口类继承自AbstractProcessor,用于处理注解,并生成Binding类,DataBindingCompoent.java,DataBinderMapper.java类;
  4. compilerCommon-2.1.0-rc1.jar
    被DataBinderPlugin和compiler-2.1.0-rc1.jar所依赖

改变监听

addOnPropertyChangedCallback: Model属性改变时回调发生 
OnRebindCallback: view发生改变重复绑定时触发

mModel.addOnPropertyChangedCallback(new Observable.OnPropertyChangedCallback() {
    @Override
    public void onPropertyChanged(Observable observable, int i) {
        if (i == BR.name) {
            Toast.makeText(TwoWayActivity.this, "name changed",
                    Toast.LENGTH_SHORT).show();
        } else if (i == BR.password) {
            Toast.makeText(TwoWayActivity.this, "password changed",
                    Toast.LENGTH_SHORT).show();
        }
    }
});

Component

通过DataBindingUtil.setDefaultComponent来设置不同环境下不同的Component
设置之后就可以使用该Component提供的Adapter方法,默认不设置是全局使用,可以理解为作用域。

public interface TestableAdapter {
    @BindingAdapter("android:src")
    void setImageUrl(ImageView imageView, String url);
}

public interface DataBindingComponent {
    TestableAdapter getTestableAdapter();
}

DataBindingUtil.setDefaultComponent(myComponent); 
 ‐ or ‐
binding = MyLayoutBinding.inflate(layoutInflater, myComponent);

相关编译流程

STEP1 资源处理

aapt或者gradle执行时,都会触发资源处理。在资源处理过程中,DataBinding都会扫描一遍现有的资源,生成不包含<layout>data-binding-layout-out以及DataBinding所需要的data-binding-info

STEP2 DataBindingInfo.class生成

在完成资源处理后,aapt或者gradle-api都会去执行DataBindingInfo.class生成操作,把相关的信息写入DataBindingInfo.class的@BindingBuildInfo注解中;

STEP3 监听到注解变化

生成@BindingBuildInfo注解,或者code中发现有新的注解写入,AbstractProcessor注解处理器就开始执行注解处理。DataBinding中有一个ProcessDataBinding.java类专门来处理DataBinding相关的注解;

STEP4 ProcessDataBinding处理注解,生成bin

ProcessDataBinding中处理注解永远会按顺执行3步,ProcessMethodAdapterProcessExpressionsProcessBindable。每次执行都会从磁盘反序列化对应的bin文件,然后往bin中写入新的,完成后再序列化到磁盘;

STEP5 生成最终产物

执行ProcessMethodAdapter生成DataBindingComponents.class;执行ProcessExpressions生成ViewDataBinding.class子类(ActivityDetail2Binding.class),并触发DataBindingMapper.class更新;执行ProcessBindable生成BR.class,并触发DataBindingMapper.class更新。

注意:

不允许使用混合类型

<!--值得注意的是  
    android:background="@{boo?@color/red:@drawable/background}"  
    这么写将会发生错误,因为在BindingConversion默认实现为:  
    @BindingConversion  
    public static ColorDrawable convertColorToDrawable(int color) {  
            return new ColorDrawable(color);  
        }  
-->  
<ImageView  
    android:layout_width="100dp"  
    android:layout_height="100dp"  
    android:layout_marginTop="20dp"  
    android:background="@{boo?@color/red:@color/green}" />