DataBinding 进阶篇四 双向数据绑定

3,017 阅读4分钟

DataBinding 基础篇一
DataBinding 进阶篇二 BaseObservable
DataBinding 进阶篇三 BindingAdapter以及BindingConversion
DataBinding 进阶篇四 双向数据绑定

六:双向绑定

所谓双向绑定,就是做到数据改变的时候,UI视图会更新。而当UI发生改变的时候,通知数据更新。
官方已经将常用控件的部分属性实现了双向绑定 blockchain

6.1:使用系统为我们支持的双向绑定

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

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

    <data>
        <import type="com.example.jetpackdatabindingtestapp.ui.model.Teacher"/>
        <variable
            name="teacher"
            type="Teacher" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:id="@+id/tv_teacher_name"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            android:text="@{teacher.firstName}"
            android:singleLine="true"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

        <EditText
            android:id="@+id/et_input_name"
            app:layout_constraintTop_toBottomOf="@+id/tv_teacher_name"
            android:layout_width="match_parent"
            android:text="@={teacher.firstName}"
            android:layout_height="50dp"/>

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

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

class BindingAdapterActivity :AppCompatActivity(){
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = DataBindingUtil.setContentView<ActivityBindingadapterTestBinding>(this,R.layout.activity_bindingadapter_test)
        binding.teacher = Teacher().apply {
            firstName.set("chenDaDa")
        }
    }
}
class Teacher{
    val firstName = ObservableField<String>()
}

上面EditText是继承TextView,由于TextView,DataBinding已经为我们自动实现了android:text这个属性的双向绑定的功能,主要实现类是TextViewBindingAdapter。我们来看看,具体是如何实现的反向绑定。
一:得知道如何去拿到当前的View的属性值

// 使用 @InverseBindingAdapter 对从视图中读取值的方法进行注释:
@InverseBindingAdapter(attribute = "android:text", event = "android:textAttrChanged")
public static String getTextString(TextView view) {
    return view.getText().toString();
}

二:得知道View的这个属性值,何时发生改变了

// 在文本变化地方,textAttrChanged.onChange();通知框架数据发生变化
@BindingAdapter(value = { ..., "android:textAttrChanged"}, requireAll = false)
public static void setTextWatcher(TextView view, ...,
        final InverseBindingListener textAttrChanged) {
    final TextWatcher newValue;
    ...
    newValue = new TextWatcher() {
        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            ...
        }
        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {
            ...
            //收到事件 通知框架发生变化
            if (textAttrChanged != null) {
                textAttrChanged.onChange();
            }
        }
        @Override
        public void afterTextChanged(Editable s) {
            ...
        }
    };
    ...
    if (newValue != null) {
        view.addTextChangedListener(newValue);
    }
}

对于EditText来说,数据发生变化会调用TextChangedListener的onTextChanged方法,而DataBinding定义了一个InverseBindingListener,在onTextChanged的时候将事件回调出去。 InverseBindingListener接口具体实现代码由框架生成,作用是是获取控件属性的当前值,然后用此值更新数据
总结实现反向绑定关键:

  • 使用@InverseBindingAdapter注解一个静态方法返回控件属性当前值
  • 监听属性值的改变,并使用InverseBindingListener将改变回调
  • 使用@InverseBindingMethod注解一个类,声明反向绑定针对的控件、属性、inverse事件名(可缺省)、控件内获取属性当前值(可缺省)。例如:
@InverseBindingMethods({@InverseBindingMethod(
     type = android.widget.TextView.class,
     attribute = "android:text",
     event = "android:textAttrChanged",
     method = "getText")})
 public class MyTextViewBindingAdapters

6.2:通过@InverseBindingAdapter跟@InverseBindingMethod自定义反向绑定

我们自定义一个CustomTimeView,代码如下:

package com.example.jetpackdatabindingtestapp.ui.widget

import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.text.TextPaint
import android.util.AttributeSet
import android.view.View
import androidx.databinding.BindingAdapter
import androidx.databinding.InverseBindingMethod
import androidx.databinding.InverseBindingMethods

@InverseBindingMethods(value = [
    InverseBindingMethod(type = com.example.jetpackdatabindingtestapp.ui.widget.CustomTimeView::class,attribute = "android:time",event = "android:timeAttrChanged",method = "getCustomTime()")
])
class CustomTimeView :View{

    var time = "2020-01-30"
        set(value) {
            field = value
            timeChangeList?.onTimeChange(time)
            invalidate()
        }
    var timeChangeList:ITimeChangeListener?=null
    var paint = Paint()
    var textPaint = TextPaint()

    constructor(context: Context):this(context,null,0)
    constructor(context: Context, attrs: AttributeSet?):this(context,attrs,0)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr:Int=0):super(context,attrs,0)

    init {
        paint.textSize = 100f
        paint.isAntiAlias = true
        textPaint.textSize = 100f
        textPaint.measureText(time)
    }

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        val y = textPaint.fontMetrics.descent-textPaint.fontMetrics.ascent
        canvas?.drawText(time,20f,y,paint)
    }

    fun getCustomTime():String{
        return time
    }

}
  • 这个CustomTimeView很简单,有个time属性,有个ITimeChangeListener监听器
  • CustomTimeViewdrawText绘制出time的内容。并且在setTime方法里,调用重绘。
  • ITimeChangeListener是time改变的监听器,在setTime里会调用该监听
  • 用InverseBindingMethods跟InverseBindingMethod指明反绑定的类,属性,监听事件,获取time的方法 xml文件:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>
        <import type="com.example.jetpackdatabindingtestapp.ui.model.TimeModel"/>
        <variable
            name="timeModel"
            type="TimeModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:id="@+id/tv_show_time"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            android:text="@{timeModel.timeObservable}"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

        <Button
            android:id="@+id/btn_changetime"
            app:layout_constraintStart_toStartOf="parent"
            android:text="改变时间"
            app:layout_constraintTop_toBottomOf="@+id/tv_show_time"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

        <com.example.jetpackdatabindingtestapp.ui.widget.CustomTimeView
            android:id="@+id/customTimeView"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/btn_changetime"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_width="wrap_content"
            android:time="@={timeModel.timeObservable}"
            android:layout_height="wrap_content"/>

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
  • 三个控件,第一个TextView显示当前的时间timeModel.timeObservable。
  • 第二个是改变时间的按钮
  • 第三个是CustomTimeView,把该view的时间属性time的值,赋值给数据timeModel.timeObservable activity类:
class TimeTestActivity :AppCompatActivity(){

    val format = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = DataBindingUtil.setContentView<ActivityTimetestBinding>(this, R.layout.activity_timetest)
        binding.timeModel = TimeModel().apply {
            timeObservable.set(format.format(Date()))
        }
        binding.btnChangetime.setOnClickListener{
            binding.customTimeView.time = format.format(Date())
        }
    }
}
class TimeModel :BaseObservable(){
    val timeObservable = ObservableField<String>()
}

binding.customTimeView.time = format.format(Date())去改变CustomTimeView的time值,由于time值改变会反过来改变TimeModel的timeObservable属性值,TimeModel改变又会去通知TextView会自动更新文本。这样就自定义实现类双向绑定

6.3 避免无限循环

通过图例我们可以发现,如果事件驱动反向绑定成功后,数据会发生变化,按正常逻辑来讲,将继续触发单向绑定,如此一来将陷入无限循环中。

为中断这种循环,通常的做法是在更新UI前,校验新旧数据是否相同,如果相同则不进行刷新动作。比如TextView,其内部的setText方法并不会检验新旧数据的一致性问题,所以在TextViewBindingAdapter内重新绑定了android:text属性,添加校验逻辑。

@BindingAdapter("android:text")
public static void setText(TextView view, CharSequence text) {
    //校验
    final CharSequence oldText = view.getText();
    if (text == oldText || (text == null && oldText.length() == 0)) {
        return;
    }
    ...
    view.setText(text);
}