文章

DataBinding单向绑定(one way)和双向绑定(two way)

DataBinding单向绑定(one way)和双向绑定(two way)

单向绑定(one way):(数据刷新视图:数据→UI)

单向绑定是指数据源改变之后会立马通知 XML 进行赋值改变,刷新 UI。下面的几种可以单向绑定:

  1. ObservableFields 扩展的属性
    1. ObservableInt
    2. ObservableField<T>
  2. BaseObservable 自定义属性
  3. ViewModel+ObservableField 扩展的属性
  4. ViewModel+LiveData

ObservableInt

ObservableField

  • xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        <variable
            name="cat"
            type="me.hacket.databindingdemos.demo1.Cat" />
        <import type="android.view.View" />
    </data>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <TextView
            android:id="@+id/tv_name_ob"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{cat.name}"
            android:textSize="20sp"
            android:visibility="@{cat.isShowName()?View.VISIBLE:View.INVISIBLE}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
        <Button
            android:id="@+id/bt_change"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="change" />
    </LinearLayout>
</layout>
  • 代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Cat {
    // 猫的名字用 ObservableField 包裹
    var name: ObservableField<String> = ObservableField<String>()
    // 是否显示猫的名字 用 ObservableBoolean
    var isShowName = ObservableBoolean()
}

val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
    val cat = Cat()
    cat.name = ObservableField("fff")
    cat.isShowName = ObservableBoolean(true)
    binding.cat = cat
    binding.btChange.setOnClickListener {
        cat.name.set("ObservableField 改变的咖啡猫")
        cat.isShowName.set(!cat.isShowName.get())
    }

ViewModel+ObservableField

  • xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        <variable
            name="vm"
            type="me.hacket.databindingdemos.demo1.CatViewModel" />
      	<import type="android.view.View" />
    </data>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <TextView
            android:id="@+id/tv_name_ob2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{vm.name}"
            android:textSize="20sp"
            android:visibility="@{vm.isShowName()?View.VISIBLE:View.INVISIBLE}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
        <Button
            android:id="@+id/bt_change2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="change ViewModel" />
    </LinearLayout>
</layout>
  • 代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class CatViewModel : ViewModel() {
    // 猫的名字用 ObservableField 包裹
    var name: ObservableField<String> = ObservableField<String>("hacket hhhh")
    // 是否显示猫的名字 用 ObservableBoolean
    var isShowName = ObservableBoolean(false)
    fun change() {
        name.set("ViewModel中改变的咖啡猫")
        isShowName.set(!isShowName.get())
    }
}

private fun singleTest2(binding: ActivityMainBinding) {
    val catViewModel = ViewModelProvider(this)[CatViewModel::class.java]
    binding.vm = catViewModel
    binding.lifecycleOwner = this
    binding.btChange2.setOnClickListener {
        catViewModel.change()
    }
}

ViewModel+LiveData 平时用的最多

  • xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        <variable
            name="userViewModel"
            type="me.hacket.databindingdemos.demo1.UserViewModel" />
        <import type="android.view.View" />
    </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="@{userViewModel.userData.name}" />
        <Button
            android:id="@+id/bt_change3"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="change by LiveData" />
    </LinearLayout>
</layout>
  • 代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
data class User(var name: String = "", var age: Int = 0, var address: String)
class UserViewModel : ViewModel() {
    var userData: MutableLiveData<User> = MutableLiveData()
    init {
        userData.value = User("张三", 24, "杭州")
        // 延迟3秒后修改数据,UI自动更新
        Thread {
            SystemClock.sleep(3000)
            userData.value!!.name = "李四"
            userData.postValue(userData.value)
        }.start()
    }
    fun change() {
        val copy = userData.value?.copy()
        copy?.name = "hacket"
        userData.value = copy
    }
    override fun onCleared() {
        Log.i("hacket", "onCleared:ViewModel 即将销毁")
    }
}

// 使用
private fun singleTest3(binding: ActivityMainBinding) {
    val userViewModel = ViewModelProvider(this)[UserViewModel::class.java]
    binding.userViewModel = userViewModel
    binding.lifecycleOwner = this
    binding.btChange3.setOnClickListener {
        userViewModel.change()
    }
}

双向绑定 two way UI→数据,UI→数据

什么是双向绑定?

在 TextView 中,我们通过 dataBinding 把实体中的数据放到 TextView 中展示,这是从实体到 view 方向上的绑定;当 TextView 的数据发生改变时,比如我们手动输入了一些数据,我们通过 dataBinding 把 view 中的数据设置到对应的实体类的字段中,这是从 view 到实体类方向上的绑定,整合起来就是双向绑定。
2ez94
使用双向绑定的场景并不多

双向绑定不足?

  1. 死循环绑定:因为数据源改变会通知 view 刷新,而 view 改变又会通知数据源刷新,这样一直循环往复,就形成了死循环绑定。
  2. 数据源中的数据有时需要经过转换才能在 view 中展示,而 view 中展示的内容也需要经过转换才能绑定到对应的数据源上。

官方提供的支持双向绑定的 View

gnfgj

EditText 支持的双向绑定

拿 android:text 举例:当 EditText 的输入内容改变时,会同时同步到变量 teacher,绑定变量的方式比单向绑定多了一个等号: android:text="@={teacher.name}"

@={} 表示法(其中重要的是包含 “=” 符号)可接收属性的数据更改并同时监听用户更新

DataBinding 已经为我们自动实现了 android:text 这个属性的双向绑定的功能,主要实现类是 TextViewBindingAdapter。

自定义 View 双向绑定实现步骤

完全的双向数据绑定三个重要函数:

  1. setter(数据到视图):自定义@BindingAdapter,setter,用于解决循环更新死循环的问题
  2. getter(视图到数据):自定义一个@InverseBindingAdapter,用于解决 getter 问题,其 event 一般和 3 中的 InverseBindingListener 的属性名保持一致
  3. notify(通过 DataBinding 视图已经刷新可以更新更新数据 Model 了):自定义@BindindAdapter 解决 View 数据更新后,其 xxxAttrChanged 对应一个 InverseBindingLister,用于通知 DataBinding View 的数据已经更新,可以更新数据源了

setter 函数:

1
2
3
4
5
6
@BindingAdapter("android:bindName")
fun TextView.setBindName(name:String?) {
    if (name.isNullOrEmpty() && name != text) {
        text = name
    }
}

getter 函数:

1
2
3
4
5
6
@InverseBindingAdapter(attribute = "android:bindName", event = "cus_event")
fun TextView.getBindName() : String {
	// 这里你可以对视图上的数据进行处理最终设置给Model层
    return text.toString()
}

  • 不允许存在更多参数
  • 返回值类型必须是绑定的数据类型

notify 函数:视图变化后要通知 DataBinding 开始设置 Model 层,同样要用到 @BindingAdapter,不同的是参数要求要有 InverseBindingListener,一般为 xxxAttrChanged

1
2
3
4
5
6
7
8
9
10
@BindingAdapter("cus_event")
fun TextView.notifyBindName( inverseBindingListener: InverseBindingListener){

  // 这个函数是监听TextWatch 官方源码 当然不同的需求不同的监听器
   doAfterTextChanged {
       inverseBindingListener.onChange() // 这行代码执行即通知数据刷新
   }

}

双向绑定示例

LiveData 双向绑定

比如 xml 里面 Textview 和 EditText 用的是一个 Model 的 nameLiveData ,此时你会看出来,TextView 单向绑定,EditText 双向绑定,当输入内容的时候 TextView 也会改变
ezsv0

  • xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
  	xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        <variable
          name="viewmodel"
          type="me.hacket.databindingdemos.demo2.DataViewModel" />
    </data>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <TextView
            android:id="@+id/tv_name_view_model"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="20dp"
            android:text="@{viewmodel.nameLiveData}"
            android:textSize="20sp" />
        <EditText
            android:id="@+id/et_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="20dp"
            android:text="@={viewmodel.nameLiveData}"
            android:textSize="20sp" />
    </LinearLayout>
</layout>

这里 android:text="@{viewmodel.nameLiveData}" 对 text 进行设置
在 Edittext 中可以使用 android:text="@={viewmodel.nameLiveData}" 进行双向绑定,关键是这个 = 号;

  • 代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ViewModel + LiveData
class DataViewModel : ViewModel() {
    val nameLiveData = MutableLiveData<String>()
}

// 使用
class BindTwoDemoActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding =
            DataBindingUtil.setContentView<ActivityBindTwoDemoBinding>(
                this,
                R.layout.activity_bind_two_demo
            )
        binding.lifecycleOwner = this
        val viewModel = ViewModelProvider(this)[DataViewModel::class.java]
        binding.viewmodel = viewModel
    }
}

双向绑定相关注解

@InverseBindingMethods 和@InverseBindingMethod

作用

  • @InverseBindingMethods 注解的作用与 @BindingMethods 类似,但是@InverseBindingMethods 是视图变更数据 (get 函数), 而 BindingMethods 是数据到视图 (set 函数)

如果说 BindingMethods 是关联 setter 方法和自定义属性, 那么 InverseBindingMethods 就是关联 getter 方法和自定义属性;

  • 如果说 BindingMethods 是关联 setter 方法和自定义属性, 那么 InverseBindingMethods 就是关联 getter 方法和自定义属性;setter 是更新视图的时候使用,而 getter 方法是更新数据时候使用的
  • @InverseBindingMethod 与@InverseBindingMethods 需要结合@BindingAdapter 注解才能发挥作用

@InverseBindingMethod 注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Target(ElementType.ANNOTATION_TYPE)
public @interface InverseBindingMethod {
    /**
     * 控件的类字节码
     */
    Class type();
    /**
     * 自定义的属性
     */
    String attribute();
    /**
     * nitify函数的名称 即用于通知数据更新的函数
     */
    String event() default "";
    /**
     * 控件自身的函数名称, 如果省略即自动生成为 {attribute}AttrChange
     */
    String method() default "";
}

  • type Class 类型,必填。如:SeekBar.class
  • attribute String 类型,必填。 如:android:progress
  • event String 类型,非必填,属性值的生成规则以及作用和@InverseBindingAdapter 中的 event 一样
  • method String 类型,非必填,比如 SeeBar,它有 android:progress 属性,也有 getProgress() 方法,所以对于 SeekBar 的 android:progress 属性,不需要明确指定 method
1
2
3
4
5
6
7
8
9
@InverseBindingMethods(
    InverseBindingMethod(
        type = CusView::class,
        attribute = "android:bindName",
        method = "getName", event = "cus_event"
    )
)
object Adapter {
}
  • 如果 attribute 属性值属于不存在的属性,则需要再创建一个 @BindingAdapter 自定义属性来处理

案例
但是到底该怎么结合@BindingAdapter 注解呢?换句话说@BindingAdapter 注解该怎么配合呢? 我们看一段代码,通过这段代码来讲解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@InverseBindingMethods({
    @InverseBindingMethod(type = RatingBar.class, attribute = "android:rating"),
}) // 1
public class RatingBarBindingAdapter {
    @BindingAdapter("android:rating") 
    public static void setRating(RatingBar view, float rating) {
        if (view.getRating() != rating) { // 2、防止死循环
            view.setRating(rating);
        }
    }

    @BindingAdapter(value = {"android:onRatingChanged", "android:ratingAttrChanged"},
        requireAll = false)
    public static void setListeners(RatingBar view, final OnRatingBarChangeListener listener,
                                    final InverseBindingListener ratingChange) {
        if (ratingChange == null) {
            view.setOnRatingBarChangeListener(listener);
        } else {
            view.setOnRatingBarChangeListener(new OnRatingBarChangeListener() {
                @Override
                public void onRatingChanged(RatingBar ratingBar, float rating, boolean fromUser) {
                    if (listener != null) {
                        listener.onRatingChanged(ratingBar, rating, fromUser);
                    }
                    ratingChange.onChange();
                }
            });
        }
    }
}
  1. 这段代码定义了 RatingBar 类的 android:rating 属性,但是没有定义 eventmethod 属性,既然没有定义,那么就采用默认值,我们就可以知道:event 的属性值为 android:ratingAttrChanged,method 的属性值为 getRating。这里就是需要@BindingAdapter 注解配合的一个地方。
  2. 为什么需要@BindingAdapter,防止死循环:RatingBar 中默认已经提供 android:rating 属性了,但是为什么还要用@BindingAdapter 重复定义一个一模一样的呢?原因就是为了防止死循环绑定。我们在上面通过@InverseBindingMethod 注解指定了 android:rating 属性需要支持双向绑定,那么自然要防止死循环绑定问题。 当我们通过@BindingAdapter 定义一个一模一样的 android:rating 属性时,一旦在布局文件中对这个属性使用了 dataBinding 表达式,那么 dataBinding 就会调用这里的 “setRating”方法,如果使用的dataBinding表达式是双向绑定表达式@={}`,那么就可以避免死循环绑定。这里是需要@BindingAdapter 注解配合的另一个地方。

@InverseBindingAdapter 视图通知数据刷新的

作用:

  • 仅作用于方法,方法必须为公共静态方法
  • 方法的第一个参数必须为 View,TextView
  • 用于双向绑定
  • 需要和@BindingAdapter 配合

注解说明

1
2
3
4
public @interface InverseBindingAdapter {
    String attribute();
    String event() default "";
}
  • attribute String 类型,必填,当值发生变化时,要从哪个属性中检索这个变化的值,如 android:text
  • event String 类型,非必填;如果填写,则使用填写的内容作为 event 的值;如果不填,在编译时会根据 attribute 的属性名再加上后缀 “AttrChanged” 生成一个新的属性 xxxAttrChanged 作为 event 的值,举个例子:attribute 属性的值为 “android:text”,那么默认会在 “android:text” 后面追加 “AttrChanged” 字符串,生成 “android:textAttrChanged” 字符串作为 event 的值
  • event 属性的作用: 当 View 的值发生改变时用来通知 dataBinding 值已经发生改变了。开发者一般需要使用@BindingAdapter 创建对应属性来响应这种改变。

案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class TabHostBindingAdapter {

    @InverseBindingAdapter(attribute = "android:currentTab")
    public static int getCurrentTab(TabHost view) {
        return view.getCurrentTab();
    }

    @InverseBindingAdapter(attribute = "android:currentTab")
    public static String getCurrentTabTag(TabHost view) {
        return view.getCurrentTabTag();
    }

    @BindingAdapter("android:currentTab")
    public static void setCurrentTab(TabHost view, int tab) {
        if (view.getCurrentTab() != tab) {
            view.setCurrentTab(tab);
        }
    }

    @BindingAdapter("android:currentTab")
    public static void setCurrentTabTag(TabHost view, String tabTag) {
        if (view.getCurrentTabTag() != tabTag) {
            view.setCurrentTabByTag(tabTag);
        }
    }

    @BindingAdapter(value = {"android:onTabChanged", "android:currentTabAttrChanged"},
            requireAll = false)
    public static void setListeners(TabHost view, final OnTabChangeListener listener,
            final InverseBindingListener attrChange) {
        if (attrChange == null) {
            view.setOnTabChangedListener(listener);
        } else {
            view.setOnTabChangedListener(new OnTabChangeListener() {
                @Override
                public void onTabChanged(String tabId) {
                    if (listener != null) {
                        listener.onTabChanged(tabId);
                    }
                    attrChange.onChange();
                }
            });
        }
    }
}

@InverseMethod 双向数据转换

  • 作用于方法
  • 用于双向绑定

@InverseMethod 注解解释:
@InverseMethod 注解是一个相对独立的注解,不需要其他注解的配合就能用,它的作用是为某个方法指定一个相反的方法,value 为必填属性,用来存放与当前方法对应的相反方法。

  • InverseMethod 注解:该注解修饰的就是正方法,而 InverseMethod 接收一个参数 value,定义的就是反方法的名称

特点:

  • 正方法与反转方法的参数数量必须相同
    • 正方法的最后一个参数的类型与反转方法的返回值必须相同
    • 正方法的返回值类型与反方法的最后一个参数类型相同
  • 正方法: 是刷新视图的时候使用 (决定视图显示数据) 会回调两次
  • 反方法: 是刷新数据的时候使用 (决定实体存储数据)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
object Bind {
    // 正方法,@InverseMethod注解中的value定义的是反方法
    @JvmStatic
    @InverseMethod("intToString")
    fun stringToInt(value: String): Int {
        "stringToInt value=$value".logw()
        return try {
            Integer.parseInt(value)
        } catch (e: Exception) {
            -1
        }
    }
    // 反方法
    @JvmStatic
    fun intToString(value: Int): String {
        "intToString value=$value".logd()
        return value.toString()
    }
}

作用:
在一些特殊的业务场景中,可以大大简化我们的代码;数据源的数据需要进行转换后才能在 view 中展示,view 中展示的内容也需要经过转换才能绑定到对应的数据源上
案例 1:App 显示的订单类型和数据源的存储的类型不一致

在一些约车或者外卖等类型的 APP 中,都有订单类型这个字段,以约车 APP 为例,订单有立即单,预约单,接机单等其他订单类型,用户在提交订单后,在用户的订单列表或详情中是可以看到订单类型的,比如 “ 立即单 “,但是在服务端,存储立即单这个字段的时候,并不是直接存储 “ 立即单 “ 这几个字的,而是以字典表的形式来存储的,比如 “OT00001” 代表立即单,在开发中,我们肯定不能把 “OT00001” 展示到界面上给用户看吧,但是服务端给我们返回的 json 中就是 “OT00001”,所以我们在接收到 “OT00001” 时要把 “OT00001” 转换成 “ 立即单 “ 展示到界面上给用户看,这就是数据源中的数据需要经过转换才能在 view 中展示; 而如果用户修改了订单类型,然后提交到服务端去修改,我们肯定是以 “OT00001” 的形式提交到服务端的,但是用户在输入时却是以 “ 立即单 “ 的形式输入的,所以在提交服务端时,我们需要把 “ 立即单 “ 转换为 “OT00001” 再去提交到服务端,这就是 view 中展示的内容也需要经过转换才能绑定到对应的数据源上。

如果不使用 dataBinding,这些转换时机以及逻辑都要我们自己掌握,但是使用了 dataBinding 之后,这些操作都变得自动化,在你设置 “OT00001” 时,会自动转换为 “ 立即单 “ 在界面上展示,而当你输入 “ 立即单 “ 时,对应的实体类字段会自动变为 “OT00001”,这会大大节省我们的开发成本。

  1. 使用 @InverseMethod 定义转换方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class InverseMethodDemo {
    @InverseMethod("orderTypeToString")
    public static String stringToOrderType(String value) {
        if (value == null) {
            return null;
        }
        switch (value) {
            case "立即单":
                return AppConstants.ORDER_TYPE_1;
            case "预约单":
                return AppConstants.ORDER_TYPE_2;
            case "接机单":
                return AppConstants.ORDER_TYPE_3;
            case "送机单":
                return AppConstants.ORDER_TYPE_4;
            case "半日租单":
                return AppConstants.ORDER_TYPE_5;
            case "全日租单":
                return AppConstants.ORDER_TYPE_6;
            default:
                return null;
        }
    }
    public static String orderTypeToString(String code) {
        if (code == null) {
            return null;
        }
        switch (code) {
            case AppConstants.ORDER_TYPE_1:
                return "立即单";
            case AppConstants.ORDER_TYPE_2:
                return "预约单";
            case AppConstants.ORDER_TYPE_3:
                return "接机单";
            case AppConstants.ORDER_TYPE_4:
                return "送机单";
            case AppConstants.ORDER_TYPE_5:
                return "半日租单";
            case AppConstants.ORDER_TYPE_6:
                return "全日租单";
            default:
                return null;
        }
    }
}
  1. XML 中使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?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>
        <import type="com.qiangxi.databindingdemo.databinding.method.InverseMethodDemo"/>
        <variable
            name="orderTypeCode"
            type="String"/>
    </data>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <EditText
            android:id="@+id/textView"
            android:layout_width="match_parent"
            android:layout_height="55dp"
            android:gravity="center_horizontal"
            android:text="@={InverseMethodDemo.orderTypeToString(orderTypeCode)}"/>

    </LinearLayout>
</layout>
  • 当使用 mBinding.setOrderTypeCode("OT00001") 时,EditText 中会自动展示 “ 立即单 “
  • 当 EditText 中的内容修改为 “ 预约单 “ 时,orderTypeCode 字段值会自动变为 “OT00002”

注意

  • 转换方法中要对参数进行判空,不然会引起空指针异常
  • 记得使用双向绑定表达式,不然转换方法不起作用,双向绑定表达式的写法为 @={}

原理:当 EditText 中的内容修改为 “ 预约单 “ 时,orderTypeCode 字段值会自动变为 “OT00002”。这一步 dataBinding 是如何做到的?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private androidx.databinding.InverseBindingListener tv3androidTextAttrChanged = new androidx.databinding.InverseBindingListener() {
    @Override
    public void onChange() {
        // Inverse of InverseMethodDemo.orderTypeToString(ordercode.get())
        //         is ordercode.set((int) InverseMethodDemo.stringToOrderType(callbackArg_0))
        java.lang.String callbackArg_0 = androidx.databinding.adapters.TextViewBindingAdapter.getTextString(tv3);
        // localize variables for thread safety
        // ordercode
        androidx.databinding.ObservableInt ordercode = mOrdercode;
        // ordercode != null
        boolean ordercodeJavaLangObjectNull = false;
        // ordercode.get()
        int ordercodeGet = 0;
        // InverseMethodDemo.orderTypeToString(ordercode.get())
        java.lang.String inverseMethodDemoOrderTypeToStringOrdercode = null;

        ordercodeJavaLangObjectNull = (ordercode) != (null);
        if (ordercodeJavaLangObjectNull) {
            // 第1次调用stringToOrderType(String)
            me.hacket.assistant.samples.google.architecture.databinding.inverse.InverseMethodDemo.stringToOrderType(callbackArg_0);
            // 第2次调用stringToOrderType(String)
            ordercode.set(((int) (me.hacket.assistant.samples.google.architecture.databinding.inverse.InverseMethodDemo.stringToOrderType(callbackArg_0))));
        }
    }
};
// TextViewBindingAdapter.java
@InverseBindingAdapter(attribute = "android:text", event = "android:textAttrChanged")
public static String getTextString(TextView view) {
    return view.getText().toString();
}
  • executeBindings() 中调用了 TextViewBindingAdapter.setTextWatcher,将 tv3androidTextAttrChanged(是一个InverseBindingListener) 传入了,这里会监听 EditText 的文本变化
  • 当 EditText 的文本内容发生变化时,DataBinding 会调用 TextViewBindingAdapter 中的 getTextString() 方法获取当前输入框的文本内容
  • 然后通过@InverseMethod 注解标记的转换方法 InverseMethodDemo.stringToOrderType("预约单"); 拿到对应的编码 “OT00002”
  • 为了让 orderTypeCode 字段值变为 “OT00002”,dataBinding 会调用 mBinding.setOrderTypeCode("OT00002") 真正的把 “OT00002” 赋值给 orderTypeCode 字段;这里 stringToOrderType(String) 会被调用 2 次

双向绑定总结

只要自定义双向绑定,都必须要有@BindingAdapter 注解的参与。

  • @InverseBindingMethod@InverseBindingMethods + @BindingAdapter 可以实现双向绑定
  • @InverseBindingAdapter + @BindingAdapter 也可以实现双向绑定
  • @InverseMethod 是一个相对独立的注解,功能强大。

高级绑定

动态变量

有一些不可知的 binding 类。例如,RecyclerView.Adapter 可以用来处理不同布局,这样的话它就不知道应该使用哪一个 binding 类。而在 onBindViewHolder(VH, int) ) 的时候,binding 类必须被赋值。
在这种情况下,RecyclerView 的布局内置了一个 item 变量。 BindingHolder 有一个 getBinding 方法,返回一个 ViewDataBinding 基类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerAdapter.BindingHolder> {

    private static final String ACTION_PRE = "hacket.databinding.action.";

    private String[] mType = new String[]{
            "Combine",
            "NormalObject",
            "Observer",
            "ObserverField",
            "ObserverCollection",
            "ViewStub",
            "Event",
            "AttributeSetters",
            "Converters",
            "Demo",
            "TwoWay"
    };

    private List<RecyclerItem> mRecyclerItemList = new ArrayList<>();

    public RecyclerAdapter() {
        mRecyclerItemList.clear();
        for (String str : mType) {
            RecyclerItem mRecyclerItem = new RecyclerItem(str, ACTION_PRE + str);
            mRecyclerItemList.add(mRecyclerItem);
        }
    }

    @Override
    public BindingHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        DatabindingRecyclerItemBinding binding =
                DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()),
                        R.layout.databinding_recycler_item, parent, false);

        Presenter presenter = new Presenter();
        binding.setPresenter(presenter);

        BindingHolder holder = new BindingHolder(binding.getRoot());
        holder.setBinding(binding);
        return holder;
    }

    @Override
    public void onBindViewHolder(BindingHolder holder, int position) {
        // 动态绑定变量
        holder.getBinding().setVariable(BR.item, mRecyclerItemList.get(position));
        holder.getBinding().executePendingBindings();
    }

    @Override
    public int getItemCount() {
        return mRecyclerItemList.size();
    }

    public class BindingHolder extends RecyclerView.ViewHolder {
        private DatabindingRecyclerItemBinding binding;

        public BindingHolder(View itemView) {
            super(itemView);
        }

        public DatabindingRecyclerItemBinding getBinding() {
            return binding;
        }

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

executePendingBindings()

当你改变了数据以后 (在你设置了 Observable 观察器的情况下) 会马上刷新 ui,但是会在下一帧才会刷新 UI,存在一定的延迟时间。在这段时间内 hasPendingBindings() 会返回 true。如果想要同步 (或者说立刻) 刷新 UI 可以马上调用 executePendingBindings()

后台线程

只要数据不是容器类,你可以直接在后台线程做数据变动。Data binding 会将变量/字段转为局部量,避免同步问题。

DataBinding 实战

自定义 View 的 setter/listener/单向绑定

setter 属性访问

  • xml 中写的属性要和自定义 view 中的 setter 名称参数对应上;对应不上用 @BindingMethods@BindingAdapter 来映射
  • 如果要传递多个参数,用 @BindingAdapter 来定义多个参数
1
2
3
4
5
6
7
8
9
10
11
12
@JvmStatic
@BindingAdapter(
    value = ["listener","bindType", "bindValue", "availBindValue"],
    requireAll = false
)
fun bind(
    view: AccountBindView,
    listener: AccountBindView.OnAccountBindClickListener?,
    type: String, //  类型,NotificationSubscribeType.EMAIL
    bindValue: ObservableField<String>,
    availBindValue: ObservableField<String>,
)
  • @BindingAdapter 中的 value 可以定义多个参数,value 名不需要和 bind 方法的参数名一致,但是要保持顺序一致,不然对应不上;requireAll 表示所有的参数都必须,不传递的就是 null,要注意判空,避免 NPE
  • 如果参数是常量,也要用 @{} 包裹上,不然匹配不上,特别是 requireAll=true 的
    • 字符串:app:bindType="@{email}
    • 数字:app:bindType="@{1}"
  • @BindingAdapter 自定义的属性的基本类型可以用 ObservableField<T>ObservableXXX
    • setter 是 String,可以用 String 或 ObservableField<String>
1
2
3
4
5
6
7
@BindingAdapter(
    value = ["bind2"],
    requireAll = false
)
fun bind2(view: AccountBindView, bindText: String) {
    view.setAccount(bindText)
}

在 xml 中可以使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<me.hacket.assistant.samples.ui.customview.cases.multilangmultiline.AccountBindView
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:layout_marginTop="@dimen/dk_dp_5"
  android:background="@color/blue_400"
  app:bind2="@{viewModel.account}" />

<me.hacket.assistant.samples.ui.customview.cases.multilangmultiline.AccountBindView
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:layout_marginTop="@dimen/dk_dp_5"
  android:background="@color/blue_400"
  app:bind2="@{viewModel.account2}" />

var account = ObservableField<String>("ObservableField hacket")  
var account2 = "hacket"

xml 中写 Listener

View 单个 Listener,单个方法的 Listener

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@JvmStatic
@BindingAdapter(
    value = ["listener","bindType", "bindValue", "availBindValue"],
    requireAll = false
)
fun bind(
    view: AccountBindView,
    listener: AccountBindView.OnAccountBindClickListener?,
    type: String, //  类型,NotificationSubscribeType.EMAIL
    bindValue: ObservableField<String>,
    availBindValue: ObservableField<String>,
) {
    if (bindValue.get().isNullOrEmpty() && availBindValue.get().isNullOrEmpty()) {
        return
    }
    view.bind(type, bindValue, availBindValue)
    view.setOnAccountBindClickListener(listener)
}
<me.hacket.assistant.samples.ui.customview.cases.multilangmultiline.AccountBindView
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:layout_marginTop="@dimen/dk_dp_5"
  android:background="@color/amber_50"
  app:account_change="change"
  app:account_label=""
  app:account_name="shengfanzeng@gmail.com"
  app:availBindValue="@{viewModel.availableValue}"
  app:bindType="@{`email`}"
  app:bindValue="@{viewModel.bindValue}"
  app:listener="@{(v,type,subText)->viewModel.onChangedClick(v,type,subText)}" />
  • click 代码
1
2
3
4
fun onChangedClick(v: View, type: String, subText: String?) {
    toast("onChangedClick")
    "onChangedClick v=$v, type=$type, subText=$subText".logw()
}

View 单个 Listener,多个方法需要实现的 Listener

如果一个 Listener 中有多个需要实现的方法,上面 xml 写法会报错:Cannot assign callback expression to 'app:listener'
DataBinding 中 xml 的 Listener 只能用只有一个方法的接口,多个方法需要拆成多个接口

  • view 的接口
1
2
3
4
5
6
7
8
interface OnAccountBindClickListener {
    fun onAccountBindClick(
            v: View,
            type: String,
            subscribeText: String?
    )
    fun onTextCopyed(v: View, text: String)
}
  • 拆分成多个接口
1
2
3
4
5
6
7
8
9
10
interface OnBindClickListener {
    fun onAccountBindClick(
            v: View,
            type: String,
            subscribeText: String?
    )
}
interface OnTextCopyedListener {
    fun onTextCopyed(v: View, text: String)
}
  • 定义@BindingAdapter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@BindingAdapter(
        value = ["bindType", "bindValue", "availBindValue", "onBindClickListener", "onTextCopyedListener"],
        requireAll = false
)
fun bind(
        view: AccountBindView,
        type: String, //  类型,NotificationSubscribeType.EMAIL
        bindValue: ObservableField<String>,
        availBindValue: ObservableField<String>,
        onBindClickListener: OnBindClickListener?,
        onTextCopyedListener: OnTextCopyedListener?,
) {
    Log.i(
            "account",
            "bind type=$type, bindValue=${bindValue.get()}, availBindValue=${availBindValue.get()}, onBindClickListener=$onBindClickListener, onTextCopyedListener=${onTextCopyedListener}"
    )
    if (bindValue.get().isNullOrEmpty() && availBindValue.get().isNullOrEmpty()) {
        return
    }
    view.bind(type, bindValue, availBindValue)
    view.setOnAccountBindClickListener(object : AccountBindView.OnAccountBindClickListener {
        override fun onAccountBindClick(v: View, type: String, subscribeText: String?) {
            onBindClickListener?.onAccountBindClick(v, type, subscribeText)
        }

        override fun onTextCopyed(v: View, text: String) {
            onTextCopyedListener?.onTextCopyed(v, text)
        }
    })
}
  • xml 中使用
1
2
3
4
5
6
7
8
9
10
11
12
13
<me.hacket.assistant.samples.ui.customview.cases.multilangmultiline.AccountBindView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="@dimen/dk_dp_5"
    android:background="@color/amber_50"
    app:account_change="change"
    app:account_label=""
    app:account_name="shengfanzeng@gmail.com"
    app:availBindValue="@{viewModel.availableValue}"
    app:bindType="@{`email`}"
    app:bindValue="@{viewModel.bindValue}"
    app:onBindClickListener="@{(v,type,subText)->viewModel.onChangedClick(v,type,subText)}"
    app:onTextCopyedListener="@{(v,text)->viewModel.onTextCopyed(v,text)}" />
  • ViewModel
1
2
3
4
5
6
7
8
9
10
class AccountViewModel : ViewModel() {
    fun onChangedClick(v: View, type: String, subText: String?) {
        toast("onChangedClick")
        "onChangedClick v=$v, type=$type, subText=$subText".logw()
    }
    fun onTextCopyed(v: View, text: String) {
        CompatUtil.copyToClipboard(text)
        toast("onTextCopyed $text")
    }
}

View 有多个 Listener

ListenerUtil.trackListener 把已经添加的 Listener 移除掉,具体可参考:TextViewBindingAdapter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@BindingAdapter(value = {"android:beforeTextChanged", "android:onTextChanged",
                         "android:afterTextChanged", "android:textAttrChanged"}, requireAll = false)
publi void setTextWatcher(TextView view, final BeforeTextChanged before,
      final OnTextChanged on, final AfterTextChanged after, final InverseBindingListener textAttrChanged) {
    final TextWatcher newValue;
    if (before == null && after == null && on == null && textAttrChanged == null) {
        newValue = null;
    } else {
        newValue = new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
                if (before != null) {
                    before.beforeTextChanged(s, start, count, after);
                }
            }

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

            @Override
            public void afterTextChanged(Editable s) {
                if (after != null) {
                    after.afterTextChanged(s);
                }
            }
        };
    }
    final TextWatcher oldValue = ListenerUtil.trackListener(view, newValue, R.id.textWatcher);
    if (oldValue != null) {
        view.removeTextChangedListener(oldValue);
    }
    if (newValue != null) {
        view.addTextChangedListener(newValue);
    }
}

双向绑定

遇到的问题

  1. 自定义 view 的路径有中文

fvhr4

  1. String 区分 java.lang.String 和 kotlin.String?
  2. XXXBinding on a null object reference

binding 的命名涉及到了关键字,如 field

1
2
3
4
5
Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'void me.hacket.assistant.google.databinding.ActivityCustomViewTwoWayBinding.setData(androidx.databinding.ObservableInt)' on a null object reference
                                                                                                at me.hacket.assistant.samples.google.architecture.databinding.example.customviewtwoway.CustomViewTwoWayActivity.onCreate(CustomViewTwoWayActivity.kt:22)
                                                                                                at android.app.Activity.performCreate(Activity.java:8273)
                                                                                                at android.app.Activity.performCreate(Activity.java:8237)
                                                                                                at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1341)

自定义 View 双向绑定

实现的效果

  1. 点击变更颜色为绿色的按钮后,两个 View 变更为绿色背景
  2. 点击下面方块后,变成蓝色

obk2m

主要源码解释

  1. @InverseBindingMethods
1
2
3
4
5
6
7
8
@InverseBindingMethods({
    @InverseBindingMethod(
        type = ColorChangeView.class,
        attribute = "color",
        event = "colorAttrChanged",    // 不是必须的,仅作标记用
        method = "getColor"     // 不是必须的,仅作标记用
    )
})

从 attribute 获取 getter 时调用;默认就是 getXXX;event 用于当 View 的值发生改变时用来通知 dataBinding 值已经发生改变了,默认就是 xxxAttrChanged,需配合@BindingAdapter 使用

  1. @InverseBindingAdapter
1
2
3
4
@InverseBindingAdapter(attribute = "color", event = "colorAttrChanged")
public static int getColor(ColorChangeView view) {
    return view.getColor();
}
  1. @BindingAdapter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@BindingAdapter(value = {"colorChangeListener", "colorAttrChanged"}, requireAll = false)
public static void setColorListener(
        ColorChangeView view,
        final ColorChangeListener listener,
        final InverseBindingListener colorChange) {
    ColorChangeListener newValue = (view1, color) -> {
        if (listener != null) {
            listener.onColorChange(view1, color);
        }
        if (colorChange != null) {
            colorChange.onChange();
        }
    };
    ColorChangeListener oldValue =
            ListenerUtil.trackListener(view, newValue, view.getId());

    if (oldValue != null) {
        view.setOnColorChangeListener(null);
    }
    view.setOnColorChangeListener(newValue);
}
  • 定义 Listener,当 View 的 color 变化时,通过 InverseBindingLister 告知 DataBinding 颜色已经变化了

完整源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
@InverseBindingMethods({
        @InverseBindingMethod(
                type = ColorChangeView.class,
                attribute = "color",
                event = "colorAttrChanged",    // 不是必须的,仅作标记用
                method = "getColor"     // 不是必须的,仅作标记用
        )
})
public class ColorChangeView extends View {

    private int mColor;
    private ColorChangeListener mColorChangeListener;

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

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

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

    public void setColor(int color) {
        if (mColor != color) {
            mColor = color;
            setBackgroundColor(color);
            if (mColorChangeListener != null) {
                mColorChangeListener.onColorChange(this, color);
            }
        }
    }

    public int getColor() {
        return mColor;
    }

    public void setOnColorChangeListener(ColorChangeListener listener) {
        mColorChangeListener = listener;
    }

    @InverseBindingAdapter(attribute = "color", event = "colorAttrChanged")
    public static int getColor(ColorChangeView view) {
        return view.getColor();
    }

    @BindingAdapter("color")
    public static void setColor(ColorChangeView view, int color) {
            view.setColor(color);
//        if (color != view.getColor()) {
//            view.setColor(color);
//        }
    }

    @BindingAdapter(value = {"colorChangeListener", "colorAttrChanged"}, requireAll = false)
    public static void setColorListener(
            ColorChangeView view,
            final ColorChangeListener listener,
            final InverseBindingListener colorChange) {
        ColorChangeListener newValue = (view1, color) -> {
            if (listener != null) {
                listener.onColorChange(view1, color);
            }

            if (colorChange != null) {
                colorChange.onChange();
            }
        };
        ColorChangeListener oldValue =
                ListenerUtil.trackListener(view, newValue, view.getId());

        if (oldValue != null) {
            view.setOnColorChangeListener(null);
        }
        view.setOnColorChangeListener(newValue);
    }
}

// String和Color的互转
object ColorInverse {

    @InverseMethod("colorToString")
    @JvmStatic
    @ColorInt
    fun stringToColor(colorStr: String): Int {
        return Color.parseColor(colorStr)
    }

    @JvmStatic
    fun colorToString(@ColorInt color: Int): String {
        return String.format("#%06X", 0xFFFFFF and color)
    }
}
  • xml 代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

        <import type="androidx.databinding.ObservableField" />

        <import type="me.hacket.assistant.samples.google.architecture.databinding.twoway.custom.ColorInverse" />

        <variable
            name="color"
            type="ObservableField&lt;String>" />

    </data>

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

        <Button
            android:id="@+id/btn_change_color"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="更改颜色为绿色" />

        <TextView
            android:id="@+id/tv_info"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:layout_marginBottom="10dp" />


        <TextView
            android:id="@+id/tv_text"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:background="@{ColorInverse.stringToColor(color)}"
            android:text="这是测试的TextView" />

        <me.hacket.assistant.samples.google.architecture.databinding.twoway.custom.ColorChangeView
            android:id="@+id/two_way_view"
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:layout_margin="@dimen/dp_10"
            app:color="@={ColorInverse.stringToColor(color)}" />

    </LinearLayout>
</layout>
  • 使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class ColorChangeTwoWayActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val observableField = ObservableField<String>("#FF0000")

        val binding = DataBindingUtil.setContentView<ActivityColorChangeTwoWayBinding>(
            this,
            R.layout.activity_color_change_two_way
        )

        binding.color = observableField

        binding.btnChangeColor.setOnClickListener {
            observableField.set("#00FF00")
        }
        binding.twoWayView.setOnClickListener {
            binding.twoWayView.color = Color.BLUE
        }

        observableField.addOnPropertyChangedCallback(object :
                androidx.databinding.Observable.OnPropertyChangedCallback() {
                override fun onPropertyChanged(
                    sender: androidx.databinding.Observable?,
                    propertyId: Int
                ) {
                    val of = sender as ObservableField<String>
                    binding.tvInfo.text = "onPropertyChanged: ${of.get()}"
                }
            })
    }
}

双向绑定 View 实战 2

判断一个 View 是否需要双向绑定?

就看这个 View 更新 UI 时,数据源是否需要变更

完整代码

  • 自定义 View
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class MyView(context: Context, attr: AttributeSet) : View(context, attr) {

    var number = 0
    set(value) {
            field = value
            invalidate()
        }

    private val onNumberChangeListenerList = ArrayList<OnNumberChangeListener>()

    private val paint = Paint()

    init {
        setOnClickListener {
            number++
            invalidate()
            for (item in onNumberChangeListenerList) {
                Log.i("hacket", "MyView onChange number=$number")
                item.onChange(number)
            }
        }
    }

    @SuppressLint("DrawAllocation")
    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        canvas!!
        paint.color = Color.RED
        canvas.drawRect(Rect(0, 0, width, height), paint)
        paint.color = Color.YELLOW
        paint.textSize = resources.displayMetrics.density * 20
        canvas.drawText(number.toString(), width / 2f, height / 2f, paint)
    }

    fun addOnNumberChangeListener(listener: OnNumberChangeListener) {
        onNumberChangeListenerList.add(listener)
    }

    fun removeOnNumberChangeListener(listener: OnNumberChangeListener) {
        onNumberChangeListenerList.remove(listener)
    }

    interface OnNumberChangeListener {
        fun onChange(number: Int)
    }
}

  • TwoWayBind
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
object TwoWayBind {

    /**
     * 用于把数据设置到View上,这里还需要添加判断数据是否重复,重复了就return,不然有概率会死循环
     */
    @JvmStatic
    @BindingAdapter("number")
    fun setNumber(view: MyView, number: Int) {
        if (view.number == number) {
            return
        }
        "BindingAdapter setNumber=$number".logi()
        view.number = number
    }

    /**
     * 用于给框架提供数据,也就是要返回用于数据双向绑定的值。
     */
    @JvmStatic
    @InverseBindingAdapter(attribute = "number")
    fun getNumber(view: MyView): Int {
        "BindingAdapter getNumber=${view.number}".logi()
        return view.number
    }

    /**
     * 用于给框架设置数据变化监听,当监听到变化时,框架就会调用getNumber()来获取数据并应用到ViewMode上。
     * (方法内部调用了一个ListenerUtil.trackListener()方法,这是官方的推荐的写法,用于监听器类型是集合的情况下,如果是set/get之类的那就直接set新的监听器即可。)
     */
    @BindingAdapter("numberAttrChanged")
    @JvmStatic
    fun setNumberListener(view: MyView, listener: InverseBindingListener?) {
        val newListener = object : MyView.OnNumberChangeListener {
            override fun onChange(number: Int) {
                "TwoWayBind onChange=$number".logw()
                listener?.onChange()
            }
        }
        val oldListener = ListenerUtil.trackListener(view, newListener, R.id.onNumberChangeListener) //  <item name="onNumberChangeListener" type="id" />
        oldListener?.apply {
            view.removeOnNumberChangeListener(this)
        }
        view.addOnNumberChangeListener(newListener)
    }
}

效果:

wae0p

  • 第一个方块写死了数字
  • 第二个方块是单向绑定,所以点击 view 更新 ui 时,数据源并没有改变
  • 第三个方块是双向绑定,所以当点击 view 更新 UI 时,数据源也跟着变了,对应的第二个方块也跟着变了
本文由作者按照 CC BY 4.0 进行授权