这篇笔记主要记录绑定适配器和双向绑定的基本用法,这篇文章会包含完整的代码和运行效果。
绑定适配器
绑定适配器的作用就是给布局文件(xml)和特定方法之间起到相互绑定的作用的注解。 如下示例(其中invertColorBackground这个app的前缀是可以省略的,而且不省略AndroidStudio还会报警告:Application namespace for attribute app:colorValue will be ignored.):
@BindingAdapter("invertColorBackground")
fun setupInvertColorBackground(invertBackgroundColorView: View, colorValue: Int) {
...
}
@BindingAdapter("android:text")
fun setText(view: TextView, text: CharSequence) {
...
}
对应的xml应用方式:
<TextView
...
android:text="..."
app:invertColorBackground="@{color}" />
转换器
转换器用于将xml中其他的数据类型转换成绑定适配器需要的数据类型。如下定义:
@BindingConversion
fun convertColorToDrawable(color: ObservableInt) = ColorDrawable(color.get())
这个就是将xml中为ObservableInt的数据类型转换为ColorDrawable的数据类型,示例如下:
<variable name="color" type="androidx.databinding.ObservableInt" />
...
<View
...
android:background="@{color}" />
这样就可以将ObservableInt直接设置为背景色了。
双向绑定
双向绑定的作用是在数据绑定的基础上,增加视图的变化时更新数据的功能。 如下示例:
<TextView
...
app:colorValue="@={color}" />
是的没错,就是多了一个‘=’号。这样就是双向绑定的使用方法。那么如何定义呢?一共有三个步骤:
- 定义绑定适配器(BindingAdapter),数据改变时通知视图更新
@BindingAdapter(value = ["colorValue"])
fun setupColorValue(tvColorValue: TextView, colorValue: Int) {
tvColorValue.setTextColor(colorValue)
tvColorValue.text = String.format("0x%08x", colorValue).toUpperCase(Locale.getDefault())
}
- 定义反向绑定适配器(InverseBindingAdapter),定义视图更新时如何更新数据
@InverseBindingAdapter(attribute = "colorValue")
fun onColorValueChanged(tvColorValue: TextView): Int {
return tvColorValue.currentTextColor
}
- 定义何时需要通知DataBinding去更新数据(注意,这里用的也是BindingAdapter注解,而且一定需要 AttrChanged 作为适配器中 colorValue 的后缀,不然会报错!)
@BindingAdapter(value = ["colorValueAttrChanged"])
fun setOnColorValueChanged(tvColorValue: TextView, attrChanged: InverseBindingListener) {
tvColorValue.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
Log.d(TAG, "beforeTextChanged: s = $s")
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
Log.d(TAG, "onTextChanged: s = $s")
}
override fun afterTextChanged(s: Editable?) {
Log.d(TAG, "afterTextChanged: s = $s")
attrChanged.onChange()
}
})
}
这个示例实现了如下双向更新的功能:
- 当颜色值(colorValue)更新时,更新字体颜色和文字显示
- 当前字体颜色更新时,更新颜色值(colorValue)
完整代码示例
下面的代码会实现一个包含四个控件的视图:
- 功能开关按钮:用于开启和关闭自动变色,以及控制是否可以变色
- 手动变色按钮:用于直接让变色控件变色
- 变色控件:用于显示当前颜色
- 颜色提示控件:用于显示当前颜色值是多少
运行效果如下图:
用到了以下知识点:
- 绑定适配器
- 双向绑定适配器
- DataBinding中如何应用merge布局
绑定适配器的定义
- 实现了TextView的文字于颜色值的双向绑定
- 根据颜色值显示其反色的背景
@BindingAdapter(value = ["colorValue"])
fun setupColorValue(tvColorValue: TextView, colorValue: Int) {
tvColorValue.setTextColor(colorValue)
tvColorValue.text = String.format("0x%08x", colorValue).toUpperCase(Locale.getDefault())
}
@InverseBindingAdapter(attribute = "colorValue")
fun onColorValueChanged(tvColorValue: TextView): Int {
return tvColorValue.currentTextColor
}
@BindingAdapter(value = ["colorValueAttrChanged"])
fun setOnColorValueChanged(tvColorValue: TextView, attrChanged: InverseBindingListener) {
tvColorValue.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
Log.d(TAG, "beforeTextChanged: s = $s")
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
Log.d(TAG, "onTextChanged: s = $s")
}
override fun afterTextChanged(s: Editable?) {
Log.d(TAG, "afterTextChanged: s = $s")
attrChanged.onChange()
}
})
}
@BindingAdapter(value = ["invertColorBackground"])
fun setupInvertColorBackground(invertBackgroundColorView: View, colorValue: Int) {
val invertColor = (0xFF0000 - (colorValue and 0xFF0000))
.or(0xFF00 - (colorValue and 0xFF00))
.or(0xFF - (colorValue and 0xFF))
Log.d(TAG, "setupInvertColorBackground: invertColor = $invertColor")
invertBackgroundColorView.setBackgroundColor(Color.BLACK or invertColor)
}
@BindingConversion
fun convertColorToDrawable(color: ObservableInt) = ColorDrawable(color.get())
布局的实现
- activity_binding_adapter.xml 显示控制按钮
- merge_layout.xml 显示颜色控件
activity_binding_adapter.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:bind="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable name="btnColorSwitch" type="androidx.databinding.ObservableBoolean" />
<variable name="color" type="androidx.databinding.ObservableInt" />
<variable name="onCheckedChangeListener" type="android.widget.CompoundButton.OnCheckedChangeListener" />
<variable name="onClickListener" type="android.view.View.OnClickListener" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".BindingAdapterActivity">
<ToggleButton
android:id="@+id/btnSwitch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="@={btnColorSwitch}"
android:onCheckedChanged="@{onCheckedChangeListener}"
app:onCheckedChangeListener="@{onCheckedChangeListener}"/>
<Button
android:id="@+id/btnChangeColor"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/change_color"
app:onClickListener="@{onClickListener}"/>
<include
layout="@layout/merge_layout"
android:id="@+id/includeMergeLayout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
bind:color="@{color}" />
</LinearLayout>
</layout>
merge_layout.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable name="color" type="androidx.databinding.ObservableInt" />
</data>
<merge>
<View
android:id="@+id/vColor"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="@{color}" />
<TextView
android:id="@+id/tvColorValue"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textAlignment="center"
app:invertColorBackground="@{color}"
app:colorValue="@={color}" />
</merge>
</layout>
Activity的实现
- 每隔1.5秒自动改变color的颜色值
- 实现开关控制
- 实现单击变色
class BindingAdapterActivity : AppCompatActivity(), CompoundButton.OnCheckedChangeListener, View.OnClickListener{
lateinit var activityBindingAdapterBinding: ActivityBindingAdapterBinding
val color = ObservableInt(Color.WHITE)
val btnColorSwitch = ObservableBoolean(false)
val random = Random()
var lastRandom = 0
val DELAY_TO_CHANGE_COLOR = 1500L
val mHandler = Handler(Looper.getMainLooper())
val colorChanger = object : Runnable {
override fun run() {
color.set(randomColor())
mHandler.postDelayed(this, DELAY_TO_CHANGE_COLOR)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activityBindingAdapterBinding = DataBindingUtil.setContentView(this, R.layout.activity_binding_adapter)
activityBindingAdapterBinding.onCheckedChangeListener = this
activityBindingAdapterBinding.onClickListener = this
activityBindingAdapterBinding.color = color
activityBindingAdapterBinding.btnColorSwitch = btnColorSwitch
}
override fun onResume() {
super.onResume()
if (btnColorSwitch.get()) startChangeColor()
}
override fun onPause() {
if (btnColorSwitch.get()) stopChangeColor()
super.onPause()
}
override fun onCheckedChanged(buttonView: CompoundButton?, isChecked: Boolean) {
if (isChecked) {
startChangeColor()
} else {
stopChangeColor()
}
}
override fun onClick(v: View) {
when (v.id) {
R.id.btnChangeColor -> {
if (btnColorSwitch.get()) {
mHandler.removeCallbacks(colorChanger)
setupColorValue(activityBindingAdapterBinding.includeMergeLayout.tvColorValue, randomColor())
mHandler.postDelayed(colorChanger, DELAY_TO_CHANGE_COLOR)
} else {
Log.d(TAG, "onCreate: btnColorSwitch = false")
}
}
}
}
private fun startChangeColor() {
mHandler.removeCallbacks(colorChanger)
mHandler.postDelayed(colorChanger, DELAY_TO_CHANGE_COLOR)
}
private fun stopChangeColor() {
mHandler.removeCallbacks(colorChanger)
}
private fun randomColor(): Int {
val nextInt = random.nextInt(7)
lastRandom = if (lastRandom == nextInt) (nextInt + 1) % 7 else nextInt
return when (lastRandom) {
0 -> Color.WHITE
1 -> Color.RED
2 -> Color.BLUE
3 -> Color.YELLOW
4 -> Color.CYAN
5 -> Color.MAGENTA
else -> Color.BLACK
}
}
}
可以看到配置视图数据和状态等操作全部都交给了DataBinding,而这里只需要负责给数据赋值,实现变更逻辑。这样我们可以将流程设计和具体实现给分开,让代码逻辑更加清晰,代码管理更加方便。