这篇笔记主要记录绑定适配器和双向绑定的基本用法,这篇文章会包含完整的代码和运行效果。
绑定适配器的作用就是给布局文件(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(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 = ["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
<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>
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,而这里只需要负责给数据赋值,实现变更逻辑。这样我们可以将流程设计和具体实现给分开,让代码逻辑更加清晰,代码管理更加方便。