DataBinding 基础篇一
DataBinding 进阶篇二 BaseObservable
DataBinding 进阶篇三 BindingAdapter以及BindingConversion
DataBinding 进阶篇四 双向数据绑定
六:双向绑定
所谓双向绑定,就是做到数据改变的时候,UI视图会更新。而当UI发生改变的时候,通知数据更新。
官方已经将常用控件的部分属性实现了双向绑定
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);
}