JetPack之DataBinding

Jetpack之DataBinding

官方地址

1.简述

DataBinding是一个用于将数据绑定到应用界面布局文件中view元素的组件,能够避免我们手动重复逻辑,例如findViewById、setText等。

2.简单使用入门

需要注意的是,DataBinding能运行到API 14(4.0)或更高版本上;Gradle(android 版本)需要1.5.0或以上。

2.1 启用DataBinding

在module下的build.gradle的android节点下启用DataBinding。

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

2.2 使用

<?xml version="1.0" encoding="utf-8"?>
<layout>
    <data>
        <variable
            name="chapter"
            type="com.hudson.databindingtest.entity.Chapter" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout 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"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{chapter.name}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

上面中,根节点变成了layout,旗下有两个节点,分别是data和我们原始的布局的根节点,我们的数据源定义在data节点中。
在下面的TextView中,通过DataBinding把数据Chapter的name字段绑定到它的text属性上。
点击编译之后,DataBinding会生成一个Binding类,名字与布局文件名有关。一般情况下,Activity根布局情况下,使用DataBindingUtil.setContentView获取。如果是在View、Fragment、ListView或RecyclerView适配器中使用时可以通过指定生成类的inflate或者DataBindingUtil的inflate方法获取。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
//        setContentView(R.layout.activity_main);
    ActivityMainBinding mainBinding = DataBindingUtil
            .setContentView(this, R.layout.activity_main);
}

在DataBinding绑定时,可以使用一些表达式:

算术运算符 + - / * %

字符串连接运算符 +

逻辑运算符 && ||

二元运算符 & | ^

一元运算符 + - ! ~

移位运算符 >> >>> <<

比较运算符 == > < >= <=(请注意,< 需要转义为&lt ;,此处没有空格,主要时为了避免被自动转义)

instanceof

分组运算符 ()

字面量运算符 - 字符、字符串、数字、null

类型转换

方法调用

字段访问

数组访问 []

三元运算符 ?:

例如:

<data>
    <import type="android.view.View"/>
    <variable
        name="chapter"
        type="com.hudson.databindingtest.entity.Chapter" />
</data>

<TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text='@{chapter.name + "extend"}'
        android:visibility="@{chapter.visible == 1 ? View.VISIBLE : View.INVISIBLE}"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

如果需要引用外部的类,可以通过在data中添加import标签来引入。如果有import的类出现冲突,那么需要使用alias来标识import的类的别名。另外import表示的是导入的类,并不能直接在后面的视图布局中作为变量使用,而只能使用其静态属性或者方法,要使用变量需要通过variable标签。
值得注意一些特殊情况,例如

1)表达式中有 <时,需要使用&lt ;(此处没有空格,主要时为了避免被自动转义)替换
2)字符串拼接时,外部应该用单引号,内部使用双引号

另外,DataBinding能够自动判断字段是否是null值,如果是,则不会显示内容;如果引用对象是null,引用的该对象的字段值时并不会报空指针异常,而是直接作为无数据处理。引用数据的初始值和类成员变量初始值类似,例如int默认为0.

2.3 集合类型的使用

<import type="java.util.List"/>
<variable
        name="datas"
        type="List&lt;String>" />

    <variable
        name="index"
        type="int" />
<TextView
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        android:text="@{datas[index]}"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

在Activity中可以使用生成的类直接通过set/get方法操作绑定的数据

ActivityMainBinding mainBinding = DataBindingUtil
            .setContentView(this, R.layout.activity_main);

mainBinding.setChapter(null);
List<String> datas = new ArrayList<>();
datas.add("jetpack");
mainBinding.setDatas(datas);
mainBinding.setIndex(0);

2.4资源的引用

这里仅简单介绍string资源的引用,其他参考官方文档。
因为我们普通的@string方式可以在不用dataBinding的情况下直接使用。所以这里以特殊情况,带参数的格式化字符串来示例。

<string name="format_test">名字是%1$s</string>

布局文件中如下:

<TextView
        android:text="@{@string/format_test(datas[index])}"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

还有一种针对英文的复数形式,例如 1 time; 2 times,那么这时time后面得多一个s,这时使用@plurals,可以参考这篇文章
还有其他资源的引用并混合表达式,参考官方文档

2.5 方法关联

官方文档中归类为:事件处理、方法引用、监听器绑定。大致其实都一样,都是对布局文件中一些属性和方法的关联,最常见的也就是onClick属性,有以下两种形式:

形式一:

public class MyHandlers {
    public void onClickFriend(View view) { ... }
}

<TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.firstName}"
           android:onClick="@{handlers::onClickFriend}"/>

形式二:

public class Presenter {
    public void onSaveClick(Task task){}
}

<data>
    <variable name="task" type="com.android.example.Task" />
    <variable name="presenter" type="com.android.example.Presenter" />
</data>
<Button 
    android:layout_width="wrap_content" 
    android:layout_height="wrap_content"
    android:onClick="@{() -> presenter.onSaveClick(task)}" />

注意:上述onClick方法会传递一个view参数,你可以选择接收或不接受,如下所示:

public class Presenter {
    public void onSaveClick(View view, Task task){}
}

android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}"

形式一也可以按照形式二的方式写,例如:

<TextView
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        android:layout_marginBottom="30dp"
        android:onClick="@{(view) -> simpleViewModel.autoChangeNoNotify(view)}"
        android:layout_width="wrap_content"
        android:minWidth="120dp"
        android:layout_height="30dp"/>

另外,关联的方法如果有返回值,则必须保持一致。而且,如果请求的对象的值为null,如上面的simpleViewModel为null,系统也会自动帮助处理,而避免报空指针异常,引用类型返回 null,int 返回 0,boolean 返回 false,等等。
此外,还可以根据情况,返回不同的结果

android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"

注意:
1)内部表达式不宜太复杂,否则会使得布局代码难以阅读。

2)如果不同配置(例如横向或纵向)有不同的布局文件,则变量会合并在一起。这些布局文件之间不得存在有冲突的变量定义。

3)在生成的绑定类中,每个描述的变量都有一个对应的 setter 和 getter。在调用 setter 之前,这些变量一直采用默认的托管代码值,例如引用类型采用 null,int 采用 0,boolean 采用 false,等等。

2.5 特殊情况 include

我们可以通过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">
   <data>
       <variable name="user" type="com.example.User"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <include layout="@layout/name"
           bind:user="@{user}"/>
       <include layout="@layout/contact"
           bind:user="@{user}"/>
   </LinearLayout>
</layout>

3.数据类

上面来看,布局绑定的值来自与数据类,如果数据类还没初始化,那么布局中值将会使用该类型的默认值,null则不显示内容。那么如果我们数据类实例设置了数据,那布局怎么知道要刷新界面呢?这就要用到可观察的数据对象了。使用了可观察的数据对象之后,数据的任何变动,都能及时反映到界面上。

3.1 通过实现Observable接口

通过类实现Observable接口来允许注册监听器。系统为我们封装了BaseObservable类,我们实现该类,并在对应的set方法中调用notifyPropertyChanged方法,以及在get方法上添加@Bindable注解。

private static class User extends BaseObservable {
    private String firstName;
    private String lastName;

    @Bindable
    public String getFirstName() {
        return this.firstName;
    }

    @Bindable
    public String getLastName() {
        return this.lastName;
    }

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

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

数据绑定在模块包中生成一个名为 BR 的类,该类包含用于数据绑定的资源的 ID(注意:需要get对应方法有@Bindable注解才能使用)

3.2 使用已有可观察对象

系统提供了以下直接使用的Observable类来使用,避免每次都需要写大量set/get方法,并在内部增加notifyPropertyChanged操作或注解。
ObservableBoolean、ObservableByte、ObservableChar、ObservableShort、ObservableInt、ObservableLong、ObservableFloat、ObservableDouble、ObservableParcelable

例如:

public class Chapter {
    public final ObservableInt visibleObservable = new ObservableInt();
    public int visible;
}

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text='@{chapter.name + "extend"}'
    android:visibility="@{chapter.visibleObservable == 1 ? View.VISIBLE : View.INVISIBLE}"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />


Chapter chapter = new Chapter();
mainBinding.setChapter(chapter);//mainBinding是一个binding对象
chapter.visibleObservable.set(1);

使用了这种方式后,变量必须设置为只读属性 public final ,并通过get/set方法操作数据。
此外,还有可观察的集合,就不再赘述了。

4.绑定类

在布局文件中绑定了视图属性和相关的数据类之后,通过make project构建项目,便会在编译期动态生成一个ViewDataBinding类,该类名与布局文件名关联。在前面我们已经说过,可以通过生成类的inflate或者DataBindingUtil来获取实例。常见形式有:

 ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);


ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);

MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);


View viewRoot = LayoutInflater.from(this).inflate(layoutId, parent, attachToParent);
ViewDataBinding binding = DataBindingUtil.bind(viewRoot);

在生成类中,我们可以直接引用布局文件中定义的id控件,这种方式避免了通过findViewById来获取view。
另外,生成类可以直接通过set/get方法来设置和获取数据。

4.1特殊情况

1)ViewStub

官方文档

或者这篇文章中关于ViewStub部分的介绍

2)RecyclerView的ViewHolder的绑定

在RecyclerView的adapter中,onBindViewHolder中如果还不知道数据类的具体类型,这时可以通过动态变量来完成。通过BindingViewHolder来获取ViewDataBinding类,并调用setVariable方法类设置item,注意:前提是我们约定了每一项布局中对应的数据类variable变量名为item。
UnknownTypeAdapter.java

@Override
public void onBindViewHolder(@NonNull BindingHolder holder, int position) {
    //约定绑定的数据类变量名为item。
    holder.getBinding().setVariable(BR.item, mDatas.get(position));
    holder.getBinding().executePendingBindings();
}

4.3 自定义绑定类名和位置

通过给data标签设置class属性来决定生成的绑定类名和位置

在当前模块的 databinding 包中生成 ContactItem 绑定类:
<data class="ContactItem">
    …
</data>

您可以在类名前添加句点和前缀,从而在其他文件包中生成绑定类。以下示例在模块包中生成绑定类:
<data class=".ContactItem">
    …
</data>

您还可以使用完整软件包名称来生成绑定类。以下示例在 com.example 包中创建 ContactItem 绑定类
<data class="com.example.ContactItem">
    …
</data>

5.绑定适配器

一般情况下,我们视图属性绑定数据类时,数据类中包含了该属性的set/get方法,同时视图中包含了对应的视图属性的set/get方法,绑定类会自动帮我们把视图属性和数据类属性绑定。例如,下面的数据变量chapter所对应的类中包含了name属性,同时也存在对应的set/get方法(如果是public类型或者ObservableXX类,则没有),而text属性对应了视图的setText和getText方法。

android:text="@{chapter.name}"

但也有例外:
1)即使不存在具有给定名称的属性,数据绑定也会起作用。然后,您可以使用数据绑定为任何 setter 创建特性。例如,支持类 DrawerLayout 没有任何属性,但有很多 setter。以下布局会自动将 setScrimColor(int) 和 setDrawerListener(DrawerListener) 方法分别用作 app:scrimColor 和 app:drawerListener 特性的 setter:

<android.support.v4.widget.DrawerLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:scrimColor="@{@color/scrim}"
        app:drawerListener="@{fragment.drawerListener}">

2)如果属性名和setter方法不匹配,这时我们可以通过@BindingMethods注解来绑定属性名和setter方法。该注释使用在类上,且可以包含多个 BindingMethod 注释,每个注释对应一个绑定规则。 例如,下面使得视图属性tint与视图的setImageTintList绑定

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

3)如果视图不存在对应的set方法时,可以在视图中创建静态方法,结合@BindingAdapter注解,并传入视图view实例,来修改相关属性,例如,paddingLeft没有对应的setPaddingLeft方法。

@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
  view.setPadding(padding,
                  view.getPaddingTop(),
                  view.getPaddingRight(),
                  view.getPaddingBottom());
}

第一个参数必须为对应的视图实例,后面的参数对应值类型。上面在注解中指明了对应的视图属性。我们可以传入多个值参数,例如:

示例

@BindingAdapter(value = {"imageUrl","errorDrawable"},requireAll = false)
public static void loadImage(ImageView imageView, String url, Drawable error){
    Glide.with(imageView.getContext()).load(url).error(error).into(imageView);
}

第二个参数对应了app:imageUrl,第三个参数对应了app:error,这里使用了app的命名空间。同时可以设置requireAll来决定布局文件中是否需要设置所有值才调用到该方法,例如上面设置false,那么可以只设置一个,也会调用到上面的loadImage方法中来。例如:

<ImageView app:imageUrl="@{venue.imageUrl}" app:error="@{@drawable/venueError}" />

注意:
1)数据绑定库在匹配时会忽略自定义命名空间。
2)出现冲突时,绑定适配器会替换默认的数据绑定适配器。
3)由于BindingAdapter注解的是静态方法,因此通常用一个类统一管理。

5.1 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());
   }
}

5.2 BindingAdapter的设置监听方法

前面通过onClick属性设置监听点击事件对应的方法时,本质是View.OnClickListener接口中的onClick方法。实际上,在布局文件中指定该视图的相关监听方法时,必须对应的是具有一种抽象方法的接口或抽象类。例如:

示例

@BindingAdapter("android:onLayoutChange")
public static void setOnLayoutChangeListener(View view, View.OnLayoutChangeListener oldValue,
       View.OnLayoutChangeListener newValue) {
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
    if (oldValue != null) {
      view.removeOnLayoutChangeListener(oldValue);
    }
    if (newValue != null) {
      view.addOnLayoutChangeListener(newValue);
    }
  }
}


<View android:onLayoutChange="@{() -> handler.layoutChanged()}"/>

还有一种特殊情况是,如果该监听接口或者抽象类有多个抽象方法时,该如何指定呢?如下接口,有两个方法。

@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewDetachedFromWindow {
  void onViewDetachedFromWindow(View v);
}

@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewAttachedToWindow {
  void onViewAttachedToWindow(View v);
}

这时需要在视图中对不同的方法设置不同的视图属性,并在BindingAdapter注解的静态方法中处理接口或抽象类的注册和解注册,使得这两个方法合并到一个接口或抽象类中。例如:

@BindingAdapter({"android:onViewDetachedFromWindow", "android:onViewAttachedToWindow"}, requireAll=false)
public static void setListener(View view, OnViewDetachedFromWindow detach, OnViewAttachedToWindow attach) {
    if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1) {
        OnAttachStateChangeListener newListener;
        if (detach == null && attach == null) {
            newListener = null;
        } else {
            newListener = new OnAttachStateChangeListener() {
                @Override
                public void onViewAttachedToWindow(View v) {
                    if (attach != null) {
                        attach.onViewAttachedToWindow(v);
                    }
                }
                @Override
                public void onViewDetachedFromWindow(View v) {
                    if (detach != null) {
                        detach.onViewDetachedFromWindow(v);
                    }
                }
            };
        }

        OnAttachStateChangeListener oldListener = ListenerUtil.trackListener(view, newListener,
                R.id.onAttachStateChangeListener);
        if (oldListener != null) {
            view.removeOnAttachStateChangeListener(oldListener);
        }
        if (newListener != null) {
            view.addOnAttachStateChangeListener(newListener);
        }
    }
}

上面操作中处理了旧监听器的解注册。

5.3自定义转换

官方文档

6.双向数据绑定

默认情况下,我们只需要在之前绑定的表达式前添加一个=号,就能实现双向绑定,需要数据类中按照之前的规定添加@Bindable注解到getter上,同时有相关的setter方法(通过ObservableField的类似)

<CheckBox
    android:id="@+id/rememberMeCheckBox"
    android:checked="@={viewmodel.rememberMe}"
/>

但有一种情况,如果我们通过绑定适配器来完成,那么这时视图的属性就会出现和视图内部的setter方法不对应的情况,例如上面例子中的

@BindingAdapter(value = {"imageUrl","errorDrawable"},requireAll = false)
public static void loadImage(ImageView imageView, String url, Drawable error){
    Glide.with(imageView.getContext()).load(url).error(error).into(imageView);
}

这个时候,我们可以布局文件中绑定数据时,通过app:imageUrl和app:errorDrawable属性配置图片地址和错误状态时的placeholder。那么这种情况下,ImageView中并没有setImageUrl和setErrorDrawable方法,那么这个时候,就没法实现双向绑定了,因为无法从视图上获取imageUrl和errorDrawable,编译器将会报错提示没有对应的方法存在。
这种情况下,我们一般需要自定义控件来完成从视图获取现有视图上的数据。前面数据驱动视图的时候,在数据类中的setter方法中,我们通过notifyPropertyChanged方法来通知视图刷新,数据绑定通过@BindingAdapter注解的方法找到对应的绑定操作,在通过getter获取数据并刷新数据到视图上。
可见,在数据驱动视图的情况下,有三个必要条件:

1)数据类中setter中内容变动时发出变动通知
2)根据绑定的方法,调用视图类中的setter来准备刷新界面
3)视图通过数据类的getter获取到数据类的数据

那么相反地,如果视图需要驱动数据变动,那么,应该也是类似的:

1)视图类中与数据相关的内容变动时,需要发出变动通知
2)调用数据类的setter来准备写回数据
3)数据类可以通过视图类的getter获取到视图中的现有数据

首先解决第一个问题,当视图中的内容变动时,需要通知给数据类。但值得注意的是,如果这个内容变动来源与数据类,应该避免通知,以避免出现数据类又重新通知给视图类,导致死循环。那这个怎么判断数据是否来源于数据类,如果增加参数这会导致所有地方都需要这样处理。更高效的做法是,通过判断当前的数据变动内容和之前是否一致,如果一致,那么不再刷新视图,这样如果数据变动来源于数据类,即使第一次不同会重新通知回数据类,后续将不会再刷新视图。系统提供了InverseBindingListener接口来通知到数据类,需要我们通过@BindingAdapter注解对应属性的静态方法,并第二个参数以InverseBindingListener作为参数,绑定数据工具会协助我们完成视图驱动数据的工作。例如下面中,我们想要监听的是content属性。

public class MyTextView extends AppCompatTextView {
    private InverseBindingListener mInverseBindingListener;

    @Override
    public void setText(CharSequence text, BufferType type) {
        super.setText(text, type);
        if(mInverseBindingListener != null){
            mInverseBindingListener.onChange();//通知数据类,当前视图内容变动了
        }
    }

    //用于数据变动刷新到视图view
    @BindingAdapter({"content"})
    public static void setContent(MyTextView view,String content){
        String current = view.getText().toString();
        if(content != null && !content.equals(current)){//2
            view.setText(content);
        }
    }
    
    @BindingAdapter(value = {"contentAttrChanged"},requireAll = false)
    public static void setContentChanged(MyTextView view, InverseBindingListener listener){//1
        view.mInverseBindingListener = listener;
    }
}

上面注释1处,定义了一个方法,并通过@BindingAdapter指明了该方法所对应的监听属性变动值(该值由一般我们监听的属性+"AttrChanged"组成,且与后面的getter方法指定的event对应),这样当绑定工具就能够找到该方法,给视图变动设置监听器。注释2处判断数据是否相同,避免陷入死循环。

看第二个问题前,先看看之前在上面例子中,loadImage方法使用了数据刷新图片内容到ImageView上,那相应地,如果双向绑定,也就是说图片显示的地址变动了,应该给数据类设置新值。由于ImageView并没有存储图片地址,因此如果需要真正双向绑定,仅仅使用ImageView是无法完成的,而需要自定义控件,并能通过绑定工具拿到数据类setter方法来写回数据。

第三个问题中,需要通过@InverseBindingAdapter注解来指明通过视图获取内容的方法。

@InverseBindingAdapter(attribute = "content",event = "contentAttrChanged")
public static String getContent(MyTextView view){
    return view.getText().toString();
}

最后需要注意,别忘了在布局文件中添加双向绑定表达式(增加=号)。

public class MyTextView extends AppCompatTextView {
    private InverseBindingListener mInverseBindingListener;

    @Override
    public void setText(CharSequence text, BufferType type) {
        super.setText(text, type);
        if(mInverseBindingListener != null){
            mInverseBindingListener.onChange();//通知数据类,当前视图内容变动了
        }
    }

    //用于数据变动刷新到视图view
    @BindingAdapter({"content"})
    public static void setContent(MyTextView view,String content){
        String current = view.getText().toString();
        if(content != null && !content.equals(current)){//避免进入死循环
            view.setText(content);
        }
    }

    @InverseBindingAdapter(attribute = "content",event = "contentAttrChanged")
    public static String getContent(MyTextView view){
        return view.getText().toString();
    }

    @BindingAdapter(value = {"contentAttrChanged"},requireAll = false)
    public static void setContentChanged(MyTextView view, InverseBindingListener listener){
        view.mInverseBindingListener = listener;
    }
}

上面方式中,都是在静态方法中添加注解完成的,还有另一种方式,直接在自定义类内部定义与属性名关联(在上面的方式中,方法名没有限定与属性值相关)的方法,这时需要通过@InverseBindingMethods类注解指定属性值、设置监听器和获取视图值的方法,而且必须严格按照注解中的值设定方法名。例如上面例子中可以改成如下:

@InverseBindingMethods(@InverseBindingMethod(type = MyTextView.class,
event = "contentAttrChanged",//方法名必须是setContentAttrChanged
method = "getContent",//方法名必须是getContent
attribute = "content"))//方法名必须是setContent
public class MyTextView extends AppCompatTextView {
    private InverseBindingListener mInverseBindingListener;

    @Override
    public void setText(CharSequence text, BufferType type) {
        super.setText(text, type);
        if(mInverseBindingListener != null){
            mInverseBindingListener.onChange();//通知数据类,当前视图内容变动了
        }
    }

    public void setContent(String content){
        String current = getText().toString();
        if(content != null && !content.equals(current)){
            setText(content);
        }
    }

    public String getContent(){
        return getText().toString();
    }

    public void setContentAttrChanged(InverseBindingListener listener){
        if(listener != null){
            mInverseBindingListener = listener;
        }else{
            mInverseBindingListener = null;
        }
    }
}

上述两种方式的源码

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