Android 界面库 (二) 之 Data binding 详细介绍

1. 简介

        回顾我们在前文《Android 界面库 (一) 之 View binding 简单使用》中学习的 View Binding,这种技术旨在简化 View 与代码之间的绑定过程。View Binding 会在编译时为每个 XML 布局文件生成相应的绑定类(Binding class),该类包含了布局文件中每个有 ID 的 View 的引用,从而避免了频繁手动调用 findViewById() 方法获取 View 对象。

        本篇文章将介绍 View Binding 的进阶版本——Data Binding。Data Binding 也是 Android Jetpack 库的一部分,它不仅会在编译时为布局文件生成相应的绑定类,还具有在布局中绑定数据的高级功能。

        Data Binding 通常用于将 UI 布局元素与逻辑端的数据模型建立连接,从而使 UI 元素能够自动与数据模型的值进行同步更新,实现 UI 与数据的绑定。通过这种方式,开发者可以更专注于数据和业务逻辑,而无需过多关注 UI 的更新。

2. 启用 Data binding

        如果需要在工程项目中启用Data binding,需要先在项目模块级 buid.gradle 文件中将 dataBinding 构建选项设置为 true, 如:

android {
    ...
    buildFeatures {
        dataBinding true
    }
}

3. 使用

3.1. XML布局

        Data binding 的 XML 布局文件跟常规布局文件略有不同,Data binding 布局的根标记以 layout 开头,后跟 data 元素,随后才是原来的常规非绑定布局文件中的根 View。示例布局:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="user" type="com.example.User"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.name}"/>
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{ String.valueOf (user.age)}"/>
   </LinearLayout>
</layout>

其中,data 中的 variable 用于在布局中定义变量,这里的变量是 com.example.User 类型的 user。即示例中:

<variable name="user" type="com.example.User" />

布局中的表达式使用 @{} 语法写入属性。即示例中:

<TextView android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:text="@{user.name}" />

提示:文章下方会详细介绍变量和表达式。

Android Studio自动转成 Data binding布局:

        当你将鼠标移到布局文件中的根元素时,会出现一个黄色灯提示悬浮按钮,点击它然后选择Convert to data binding layout,那么 Android Studio 会把常规布局自动转成 Data binding 绑定布局。如下图所示:

3.2. 布局的变量

        如上述示例,在布局文件中的 data 元素里的 variable 元素就是用于定义布局上的变量。variable 元素可以有多个,它可以在布局文件中的绑定表达式中使用。示例: 

<data>
    <variable name="user" type="com.example.User"/>
    <variable name="name" type="String"/>
    <variable name="age" type="int"/>
</data>

注意:如果各种配置(例如横向或纵向)有不同的布局文件,系统会合并变量。这些布局文件之间不能有冲突的变量定义。

3.2.1 导入类包

        在布局文件中的 data 元素里,可以像 Java/Ktolin 代码导入包一样使用 import 来导入引用类,示例:

<data>
    <import type=" com.example.User "/>
    <import type="java.util.List"/>
    <import type="android.view.View"/>
    <import type="com.example.real.estate.View" alias="Vista"/>
    <import type="com.example.MyStringUtils"/>

    <variable name="user" type=" User"/>
    <variable name="userList" type="List&lt;User>"/>
</data>
  1. 通过导到入了 User 类,那么在下面的 variable 元素定义变量时便可以不再使用完整的类名;
  2. 通过导入 View 类,便可以从下方绑定表达式中引用该类。例如使用 View 类的 VISIBLE 和 GONE 常量:
  3. 当导入的类名冲突时,还可以使用 alias 来给导入的类重命名,然后便可以在布局文件中使用重命名后的类名来引用它字段或方法;
  4. 还可以导入某一个类,然后在表达式中使用该类的静态方法。

使用导入类的表达式示例:

<TextView
   android:text="@{user.name}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:visibility="@{user.age >=18 ? View.VISIBLE : View.GONE}"/>

<TextView
   android:text="@{MyStringUtils.capitalize(user.name)}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

3.2.2 传递变量给子布局

        你可以将变量从包含布局传递到所含子布局的绑定中,方法是在属性中使用应用命名空间和变量名称。示例:

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:bind="http://schemas.android.com/apk/res-auto">
   <data>
       <variable name="user" type="com.example.User"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <include layout="@layout/name_layout"  bind:user="@{user}"/>
   </LinearLayout>
</layout>

name_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="user" type="com.example.User"/>
   </data>
   <LinearLayout
       ……
   </LinearLayout>
</layout>

        示例中在 activity_main.xml 通过 bind:user="@{user}" 将 user 变量传递到 name_layout.xml布局文件中,这样就可以在name_layout.xml布局中也使用user变量。

注意:在主布局和子布局中,变量名称和类型必须一致。否则,数据绑定将无法识别并传递变量。

特别注意:Data binding不支持 include 作为 merge 元素的直接子元素。例如以下是一个错误的示例:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:bind="http://schemas.android.com/apk/res-auto">
   <data>
       <variable name="user" type="com.example.User"/>
   </data>
   <merge>
       <!-- Doesn't work -->
       <include layout="@layout/name_layout"  bind:user="@{user}"/>
   </merge>
</layout>

3.3. 布局的表达式

        如上述示例,在布局 View 中使用的 @{…},便是布局中的表达式。布局表达式有它自己的语言规则,例如它能支持运算符和一些特定的关键字。

3.3.1. 常见表达式运算符和关键字

类型

操作符

数字

+ - / * %

字符串串联

+

逻辑

&& ||

二进制文件

& | ^

二进制位移

>> >>> <<

一元组

+ - ! ~

比较

== > < >= <= (“<”是XML语法关键字,所以需要转义为 &lt;)

数组访问

[ ]

三元运算符

?: (跟Java中的“?:” 一样用法,用作条件判断后选择)

Null 合并

?? (跟Kotlin中的“?:”一样用法,用作前值为空时使用后值)

示例:

android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age > 18 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
android:text="@{user. firstName ?? user.lastName}"
android:text="@{user. firstName != null ? user. firstName: user.lastName}"

注意:任何在布局中的表达式返回值都需要使用 String.valueOf 转换成字符串类型,正如示例,如果存在这样 android:text="@{index + 1}" 是会报异常的。

自动避免空指针:

        生成的Data binding 代码会自动检查 null 值并避免空指针异常。例如,在表达式 @{user.name} 中,如果 user 为 null,则会为user.name分配其默认值 null。如果引用 user.age,其中 age 的类型为 int,则会使用默认值 0。

建议:一般应让布局表达式小而简单,因为它们无法进行单元测试,并且 IDE 支持也有限。

3.3.2. View 引用

        表达式支持使用按 ID 引用布局中的其他View,示例:

<EditText
    android:id="@+id/example_text"
    android:layout_height="wrap_content"
    android:layout_width="match_parent"/>
<TextView
    android:id="@+id/example_output"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{exampleText.text}"/>

3.3.3. 集合

        可以使用 [ ] 运算符访问常见集合,例如数组、列表、稀疏列表和映射。使用这些集合时,需要对其指定泛型类型。示例:

<data>
    <import type="android.util.SparseArray"/>
    <import type="java.util.Map"/>
    <import type="java.util.List"/>
    <variable name="list" type="List&lt;String>"/>
    <variable name="sparse" type="SparseArray&lt;String>"/>
    <variable name="map" type="Map&lt;String, String>"/>
    <variable name="index" type="int"/>
    <variable name="key" type="String"/>
</data>
...
android:text="@{list[index]}"
...
android:text="@{sparse[index]}"
...
android:text="@{map[key]}"
...
android:text="@{map.key}"

“<”是XML关键字,为确保的语法正确, “<”字符是需要进行转义成&lt; 。如上述示例中的 List&lt;String>,而不是 List<String>。

3.3.4. 字符串和资源

        可以使用英文单引号括住属性值,这样就可以在表达式中使用英文双引号,或者使用双引号括住属性值,字符串字面量用反引号 ` 括起来。

        资源的引用,可使用 @xx/xx的语法示例:

android:text="@{age == 0 ? `零` :  @string/no_zero }"
...
android:text='@{age == 0 ? "零" :  @string/no_zero }'
...	
// 带参数的资源
android:text="@{@string/example_resource(user.name, exampleText.text)}"

3.3.5. 默认值

        如果在初始阶段,表达式引用的变量并未初始化完成,这时你又不希望界面显示出空的值,可以使用默认值显示。示例:

<TextView 
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{user.name, default=my_default}"/>

 但是,如果你仅仅是需要在项目的设计阶段显示默认值,则可以使用 tools 属性,而不是默认表达式值。示例:

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tools:text="my_default"/>
</LinearLayout>

3.4. 布局的事件处理

3.4.1 方法引用

        在表达式中,可以引用符合 Listener 方法签名的方法。方法与 android:onClick  分配给 activity 中的方法类似。与 View onClick 属性相比优点是表达式在编译时得到处理。因此如果该方法不存在或其签名不正确,会在编译时期提前知道。示例:

创建 MyHandlers 类:

class MyHandlers {
    // 方法签名必须跟监听器方法签名完全匹配
    fun onClickFriend(view: View) { ... }
}

布局: 

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="handlers" type="com.example.MyHandlers"/>
       <variable name="user" type="com.example.User"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.name}"
           android:onClick="@{handlers::onClickFriend}"/>
   </LinearLayout>
</layout>

3.4.2. 监听器绑定

        监听器绑定是在事件发生时运行的绑定表达式。它们类似于方法引用,但允许运行任意数据绑定表达式。

        在方法引用中,方法的参数必须与事件监听器的参数匹配。在监听器绑定中,只有返回值与监听器的返回值一致即可,除非预期返回值为 void。例如,假设存在以下presenter 类:

class Presenter {
    fun onSaveClick(task: Task){}
    fun onSaveClick2(view: View, task: Task){}
    fun onCompletedChanged(task: Task, completed: Boolean){}
    fun onLongClick(view: View, task: Task): Boolean { }
}

可以将事件绑定到presenter类的方法,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable name="task" type="com.android.example.Task" />
        <variable name="presenter" type="com.android.example.Presenter" />
    </data>
    <LinearLayout 
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <!--忽略方法的所有参数--> 
        <Button 
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{() -> presenter.onSaveClick(task)}" />

        <!--命名所有参数,并使用参数-->
        <Button 
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content"
            android:onClick="@{(view) -> presenter.onSaveClick2(view, task)}" />
        <CheckBox 
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content"
            android:onCheckedChanged="@{(cb, isChecked) -> presenter.completeChanged(task, isChecked)}" />

        <!--带返回值的表达式,如果表达式因presenter对象为空导致而无法求值,则会返回该类型的默认值,这里是Boolean则会返回false--> 
        <Button 
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content"
            android:onLongClick="@{(view) -> presenter.onLongClick(view, task)}" />

        <!--三元表达式,可使用 void --> 
        <Button 
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content"
            android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"

    </LinearLayout>
</layout>

建议:虽然监听器表达式功能强大,可让你的代码更易于阅读。但另一方面,若表达式过于复杂的监听器也会使布局变得更难以阅读和维护。所以请尽量让表达式保持简单,避免使用复杂的监听器。
 

3.5. 生成绑定类

        跟 View binding 一样,当你工程 Gradle 中配置启用 Data binding 后,在工程编译阶段就会为每个布局文件生成对应的绑定类,其中类的默认命名规则是:XML 文件的名称转换为 Pascal 命名规则的大小写形式,并在末尾添加“Binding”。例如,布局文件名为 activity_main.xml,生成的对应绑定类为 ActivityMainBinding。该类位于模块包下的 databinding 包中。例如模块包名为com.example.app,则绑定类的全称就是:com. example.app.databinding.ActivityMainBinding 。

3.5.1. 自定义绑定类名称

        可以通过配置 data 元素的 class 属性来重命名绑定类。如:

<data class=”myData”>
     …
</data>

如果你希望将生成的绑定类放置在自定义的包下可以这样:

<data class=”com.example.app.abc.myData”>
     …
</data>

3.5.2. 代码中创建绑定对象

        生成的绑定类继承自 ViewDataBinding 类,里除了包含了布局文件每个有 ID 的 View 的引用外,还会包含布局中定义的变量引用。每个布局变量都有一个对应的 setter 和 getter。在调用 setter 之前,这些变量会采用默认的托管代码值 null 用于引用类型,0 用于 int,false 用于 boolean,等等。
        假设 com.example.User 类定义如下:

data class User(val name: String, val age: Int)

在 Activity 的 onCreate 方法中,可以执行以下代码来绑定 Activity 和布局文件,以及为布局中的变量赋值:

private lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
    binding.user = User("子云心", 18)
}
或者也可以使用跟 View binding 一样的 LayoutInflater 方式:
binding = ActivityMainBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)

        如果在 Fragment、ListView 或 RecyclerView 的Adapter 内使用 Data binding项,则需要使用绑定类的 inflate() 方法或 DataBindingUtil 类,示例:

val listItemBinding = ListItemBinding.inflate(layoutInflater, viewGroup, false)

或者:        

val listItemBinding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false)

完整的 RecyclerView 的 Adapter 示例:

class MyRecyclerViewAdapter(private val userList: ArrayList<User>, private val context: Context)
    : RecyclerView.Adapter<MyRecyclerViewAdapter.MyViewHolder>() {
    class MyViewHolder(val listItemBinding: ListItemBinding) : RecyclerView.ViewHolder(listItemBinding.root) {
    }
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        val databind = DataBindingUtil.inflate<ListItemBinding>(LayoutInflater.from(context), R.layout.list_item, parent, false)
        val holder = MyViewHolder(databind)
        holder.listItemBinding.itemName.setOnClickListener {
            val position = holder.getAbsoluteAdapterPosition()
            Toast.makeText(it.context, "click ${userList[position].name}", Toast.LENGTH_SHORT).show()
        }
        return holder
    }
    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        holder.listItemBinding.item = userList[position]
    }
    override fun getItemCount(): Int {
        return userList.size
    }
}

4. 单向数据绑定

        单向绑定就是将数据模型绑定到用户界面(UI),使得当数据模型的值变化时,UI能够自动更新以反映这些变化。但是 UI 上的更改不会影响数据模型发生改变。这种方式适用于需要将数据动态地显示在 UI 上的场景,例如仅将某些信息显示在TextView中。

        正常情况下,数据对象的改变是不会自动更新 UI 的,所以如果要实现自动更新机制,就要让数据对象实现监听器,在发生更改时通知其它对象,而使用可观察的数据对象绑定到界面时,当其数据发生更改时,UI 就可以自动更新。

        可观察的数据类实现方式有三种分别是:字段、集合和对象。

4.1. 可观察的数据对象

4.1.1. 可观察字段

        可观察字段就是将数据对象类中的字段的普通类型更改为 Observable 系列的类型,Observable 系列的类型有:

  • ObservableBoolean
  • ObservableByte
  • ObservableChar
  • ObservableShort
  • ObservableInt
  • ObservableLong
  • ObservableFloat
  • ObservableDouble
  • ObservableParcelable<T>
  • ObservableField<T>

        将上面的data class User 使用 Observable 进行一下改造:

class UserObservable {
    val name = ObservableField<String>()
    val age = ObservableInt()
}

注意字段最好使用 val 表示为只读属性,因为它不需要再被修改,访问字段值时,就需要使用 set() 和 get() 访问器方法。如给布局变量赋值:

binding.userObservable = UserObservable().apply {
    name.set("子云心")
    age.set(18)
}

这时,如果布局直接绑定了UserObservable对象,那么就会自动更新。布局如下:

<data>
   <variable name="userObservable" type="com.example.UserObservable"/>
</data>
...
<TextView
    android:id="@+id/tv_name"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{ userObservable.name }"/>

这样就可以不再需要去了调用以下代码:

binding.tvName.setText(“子云心”)

4.1.2. 可观察集合

        可观察集合就是将普通的 List、ArrayList、Map、ArrayMap 变成 Observable 系列集合:ObservableList、ObservableArrayList、ObservableMap、ObservableArrayMap 等。示例:

binding.userMap = ObservableArrayMap<String, Any>().apply {
    put("name", "子云心")
    put("age", 18)
}

布局中将 user 变量换成 ObservableMap 类型的 userMap:

<data>
       <import type="androidx.databinding.ObservableMap"/>
       <variable name="userMap" type="ObservableMap&lt;String, Object>"/>
</data>
...

<TextView
    android:id="@+id/tv_name"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{String.valueOf(userMap.get(`name`))}"/>

4.1.3. 可观察对象

        可观察对象就是让对象的类实现 BaseObservable 接口,类中的属性的 getter 需要分配一个 Bindable 注解,并在 setter 中调用 notifyPropertyChanged() 方法,这样类在属性发生更改时会发出通知。

        将上面的 UserObservable 使用 BaseObservable 再进行一下改造:

class UserObservable: BaseObservable() {
    @get:Bindable
    var name: String = ""
        set(value) {
            field = value
            notifyPropertyChanged(com.example.BR.name)
        }
    @get:Bindable
    var age: Int = 0
        set(value) {
            field = value
            notifyPropertyChanged(com.example.BR.age)
        }
}

给布局变量赋值:

binding.userObservable = UserObservable().apply {
    name = "子云心"
    age = 18
}

布局变量就是 BaseObservable 类型的 userObservable:

<data>
   <variable name=" userObservable " type="com.example. UserObservable "/>
</data>
...

<TextView
    android:id="@+id/tv_name"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{userObservable.name}"/>

        上述示例中的 BR 类是在编译时生成在模块包下的类,其中包含用于 Data binding 的资源的 IDBindable 注解会在编译期间在 BR 类文件中生成一个条目。

注意:如果在编译期间报:Unresolved reference: BR 错误,需要往模块Gradle中进行 kotlin-kapt 插件配置,如:

plugins {
    ...
    id 'kotlin-kapt'
}

4.1.4. 生命周期感知型对象

        在布局中其实还可以直接绑定到数据绑定来源,数据绑定来源会自动通知界面有关数据的变化。这样绑定就能够感知生命周期,并且仅在界面显示在屏幕上时才会触发。数据绑定支持 StateFlowLiveData

提示:关于 StateFlow 和 LiveData 会在后面的 MVVM 系列文章中再详细介绍。

4.2. @BindingAdapter (绑定适配器注解)

        BindingAdapter 绑定适配器是 DataBinding 中用于扩展布局 XML 属性行为的注解,它可以支持布局 XML 中的一个或多个属性进行绑定行为扩展。而且注解值可以是已存在的属性,如android:text,也可以是自定义属性,如:app:imageUrl。虽然其名称叫适配器,实际使用上它更像是一个拦截器。

        BindingAdapter 注解和方法可定义在代码任意地方,注解接收一个字符串参数,用于指定 XML 中的属性名,注解对应的方法必须是一个静态方法(如果你使用 Kotlin 语言,那么需要在包级函数、单例(object)类或伴生对象(companion object)内的方法需另外增加 @JvmStatic 注解),方法名称可以随意定义,方法不需要返回值,第一参数用于确定与属生关联的View的类型,第二个参数用于确定给定属性的绑定达表式中接收的类型。

4.2.1. 已存在属性

布局:

<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@{`子云心`}" />

注解和方法:

@JvmStatic
@BindingAdapter("android:text")
fun setText(view: TextView, txt: String?) {
    view.text = "BindingAdapter_$txt"
}

        示例中 TextView 最终展示的值是:“BindingAdapter_子云心”

4.2.2. 自定义属性

布局:

<ImageView
    android:layout_width="100dp"
    android:layout_height="100dp"
    app:imageUrl="@{`https://www.xxx.com/xxx.png`}"/>

注解和方法:

@JvmStatic
@BindingAdapter("imageUrl")
fun setImageUrl(view: ImageView, url: String?) {
    Picasso.get().load(url).into(view)
}

        示例中自定义了一个名为 imageUrl 的属性,在 setImageUrl 方法中通过工作器线程调用自定义加载器来加载网络图片。

4.2.3. 同时多个自定义属性

布局:

<ImageView
    android:layout_width="100dp"
    android:layout_height="100dp"
    app:imageUrl="@{`https://www.xxx.com/xxx.png`}"
    app:error="@{@drawable/venueError }"/>

注解和方法:

@JvmStatic
@BindingAdapter("imageUrl", "error")
fun loadImage(img: ImageView, url: String?, error:Drawable?) {
    Picasso.get().load(url).error(error).into(view)
}

        上述示例在布局的 XML 中使用的自定义属性,必须同时提供 imageUrl 和 error 绑定方法才能生效被触发。如果你希望只要提供其中一个自定义属性也可以触发绑定逻辑,那么可以在注解方法中增加 requireAll 标志并设置为 false,如:

@JvmStatic
@BindingAdapter(value = ["imageUrl", " error "], requireAll = false)
fun loadImage(img: ImageView, url: String?, error:Drawable?) {
    if(url != null) {
        ……
    } else if (error != null) {
        ……
    } else {
        ……
    }
}

4.2.4. 接受旧值

        绑定适配器方法可以在其处理程序中接受旧值。同时接受旧值和新值的方法参数必须先旧值,随后再新值,如以下示例所示:

@JvmStatic
@BindingAdapter("android:paddingLeft")
fun setPaddingLeft(view: View, oldPadding: Int, newPadding: Int) {
    if (oldPadding != newPadding) {
        view.setPadding(newPadding,
                    view.getPaddingTop(),
                    view.getPaddingRight(),
                    view.getPaddingBottom())
    }
}

        有时要处理事件绑定时,接口的定义是方法 remove 和 add,而不是 setter,所以可以通过接收旧值的方式来先 remove 然后再 add 新值,如:

布局:

<View android:onLayoutChange="@{() -> handler.layoutChanged()}"/>

方法和注解:

@JvmStatic
@BindingAdapter("android:onLayoutChange")
fun setOnLayoutChangeListener(
        view: View,
        oldValue: View.OnLayoutChangeListener?,
        newValue: View.OnLayoutChangeListener?
) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        if (oldValue != null) {
            view.removeOnLayoutChangeListener(oldValue)
        }
        if (newValue != null) {
            view.addOnLayoutChangeListener(newValue)
        }
    }
}

4.3. @BindingConversion (类型转换注解)

        在某些情况下,需要在特定类型之间进行自定义转换。例如,View 的 android:background 属性需要 Drawable,但如果通过表达式进行逻辑处理后,指定的 color 值是整数。这样就会发生类型转换错误,例如正常情况下:

<View
   android:background="@color/white"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

但是,如果进行了表达式逻辑处理后:

<View
   android:background="@{isError ? @color/red : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

这里我们预期是接收一个Drawable 类型的参数,但此处返回了一个 int 类型参数就会发生类型转换错误。像此类情况,需执行类型的转换,那么就需要使用带有 @BindingConversion 注解的静态方法。

        @BindingConversion 注解的使用跟 @BindingAdapter 类似,也是可以将注解和静态方法定义在任意地方,但要注意的是,@BindingConversion 的方法需要返回值,来看看上述示例中使用 @BindingConversion 如何处理类型的转换:

@JvmStatic
@BindingConversion
fun convertColorToDrawable(color: Int): ColorDrawable {
    return ColorDrawable(color)
}

4.3.1. 同时使用 @BindingConversion 和 @BindingAdapter

        如果同时使用 @BindingConversion@BindingAdapter 的话,@BindingConversion 优先级比 @BindingAdapter 高。示例:

布局:

<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@{`子云心`}" />

注解和方法:

@JvmStatic
@BindingAdapter("android:text")
fun setText(view: TextView, txt: String?) {
    view.text = "BindingAdapter_$txt"
}
@JvmStatic
@BindingConversion
fun bindingConversionTest(txt: String?): String {
    return "BindingConversion_$txt"
}

示例中最终 TextView 展示出来的值是:“BindingAdapter_BindingConversion_子云心”,这是因为存在优先级高低,所以先执行了 @BindingConversion 再执行 @ BindingAdapter的方法。

注意:

    @BindingConversion 一般是在类型转换时才使用,像上述示例中,bindingConversionTest 方法接收一个 String 又返回一个新的 String 其实是没有必要的,因为这仅仅用于演示使用,在实际开发中应该尽量避免这样做,因为这样会使业务复杂化,从而可能导致 @BindingAdapter 中预期接收的值被修改。

    所以在使用的时候,要格外注意。如果不熟悉或者非用不可就不要使用 @BindingConversion了。因为 @BindingConversion 看似很高级但是如果项目业务复杂,驾驭不了还很有可能导致 @BindingAdapter 的值被修改从而增加项目的可维护性。

4.4. @BindingMethods(适用于自定义View的绑定注解)

        @BindingMethods 注解和 @BindingAdapter 注解在功能上类似,都是用于在 XML 布局中绑定自定义属性与 View 的行为,但它们的使用场景有一些不同。

        @BindingAdapter 通常是用来自定义已有的 Android View 控件的行为,而 @BindingMethods 则更适用于自定义View 与 Data Binding 库之间的交互。

    @BindingMethods 注解需将它定义在自定义 View 类的头部,注解内可以有多个BindingMethod 子项。

    假设你有一个自定义的圆形按钮 CircularButton,它有一个设置按钮的颜色的方法和一个设置圆形弧度的方法,可以通过 BindingMethod 来指定这两个方法的映射关系:

@BindingMethods(
    BindingMethod(type = CircularButton::class, attribute = "app:buttonColor", method = "setButtonColor"),
    BindingMethod(type = CircularButton::class, attribute = "app:buttonRadian", method = "setButtonRadian"),
)
class CircularButton : Button {
    constructor(context: Context?) : super(context) {
    }
    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
    }
    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
    }
    fun setButtonColor(@ColorInt color: Int) {
        // TODO...
    }
    fun setButtonRadian(radian: Int) {
        // TODO...
    }
}

在 xml布局中,就可以使用 app:buttonColor 和 app:buttonRadian属性来设置按钮的颜色和弧度:

<com.example.CircularButton
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:buttonColor="@color/red"
    app:buttonRadian="10" />

5. 双向绑定        

        双向数据绑定使用了@={} 表示法,即比单向多出一个“=”号,它表示可接收属性的数据更改,并同时能监听数据更新。

5.1. 标准控件的绑定

        回顾使用单向数据绑定,它仅可以获取属性值,若要设置其属性值,一般要通过 View 的监听器的方式对其输入变更后的属性进行响应再处理。结合上述 4.1.3可观察对象和 3.4.2监听器绑定的学习,现在来创建一个通过 EditText 输入值后进行数据和界面的更新单向绑定示例:

        再将上面的UserObservable 再进一步改造,增加一个afterNameChanged 方法用于输入后更新 name 的值:

class UserObservable: BaseObservable() {
    @get:Bindable
    var name: String = ""
        set(value) {
            field = value
            notifyPropertyChanged(com.flyme.auto.user.abc.BR.name)
        }
    @get:Bindable
    var age: Int = 0
        set(value) {
            field = value
            notifyPropertyChanged(com.flyme.auto.user.abc.BR.age)
        }
    fun afterNameChanged(text: Editable) {
        name = text.toString()
    }
}

给布局变量赋值:

binding.userObservable = UserObservable().apply {
    name = "子云心"
    age = 18
}

布局:

<data>
    <variable name=" userObservable " type="com.example. UserObservable "/>
</data>
……
<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@{userObservable.name}"/>
<EditText
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@{userObservable.name}"
    android:afterTextChanged="@{(text) -> userObservable.afterNameChanged(text)}"/>

这个单向绑定示例中,当用户在 EditText 中输入更改后的新值时,由于绑定了值变化监听器,此时 userObservable.afterNameChanged 方法得到调用,方法内对 name 字段进行了设置,又因为 UserObservable 类继承于BaseObservable ,name 字段是可观察字段,所以在 TextView 中是可以立即响应值的变化。

        对于标准控件和常见属性(如 EditText 的 android:text ),其实 DataBinding 库已经内置了支持双向数据绑定,上述示例如果使用双向数据绑定,即通过@={} 表示法,既可获取属性值,也可接收属性值更改同时监听从而更新数据,这样便可简化 EditText 的 afterTextChanged 监听步骤,UserObservable 类中可删除 afterNameChanged 方法,同时布局 View的表达式可以这样变化:

<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@{userObservable.name}"/>
<EditText
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@={userObservable.name}" />

有关双向数据绑定的内置支持如下表所示:

特性

绑定适配器

AdapterView

android:selectedItemPosition
android:selection

AdapterViewBindingAdapter

CalendarView

android:date

CalendarViewBindingAdapter

CompoundButton

android:checked

CompoundButtonBindingAdapter

DatePicker

android:year
android:month
android:day

DatePickerBindingAdapter

NumberPicker

android:value

NumberPickerBindingAdapter

RadioButton

android:checkedButton

RadioGroupBindingAdapter

RatingBar

android:rating

RatingBarBindingAdapter

SeekBar

android:progress

SeekBarBindingAdapter

TabHost

android:currentTab

TabHostBindingAdapter

TextView

android:text

TextViewBindingAdapter

TimePicker

android:hour
android:minute

TimePickerBindingAdapter

5.2. @InverseBindingAdapter(双向绑定适配器注解) 

        对于标准控件和常见属性(如 EditText 的 android:text),DataBinding 库已经内置了支持,不需要额外做处理,但是如果对于自定义属性,要想实现数据双向绑定,就要借助于 @InverseBindingAdapter 注解。示例:

修改上述布局 View的表达式:

<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@{userObservable.name}"/>
<EditText
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:customText="@={userObservable.name}" />

在任意代码中增加如下注释和方法

@BindingAdapter("customText")
fun setCustomText(view: EditText, text: String) {
    // 判断新旧值很有必要,否则会导致无限循环
    if (view.text.toString() != text) {
        view.setText(text)
    }
}
@InverseBindingAdapter(attribute = "customText", event = "customTextAttrChanged")
fun getCustomText(view: EditText): String {
    return view.text.toString()
}
@BindingAdapter("customTextAttrChanged")
fun setCustomTextAttrChanged(view: EditText, listener: InverseBindingListener?) {
    if (listener != null) {
        view.addTextChangedListener(object : TextWatcher {
            override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
            override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
            override fun afterTextChanged(s: Editable) {
                listener.onChange()
            }
        })
    }
}

示例中,同样的当用户在 EditText 中输入更改后的新值时,TextView 中可以立即响应值的变化。

        上面单向绑定时我们了解到 @BindingAdapter 绑定适配器可支持布局 XML 中的自定义属性进行绑定行为扩展。所以此示例中从逻辑端数据模型到 UI 布局中 View 的数据绑定依然是依赖 @BindingAdapter 绑定适配器来支持。

        而当从 UI 布局中 View 控件文本发生数据变化再同步到逻辑端数据模型中,则是@InverseBindingAdapter 的功劳,它在用户对 EditText 的文本变化时,获取新的值更新到customText 属性。并且它的 event 参数指定了一个值变化监听器,该监听器包含一个 InverseBindingListener 参数。当 EditText 的文本变化后,通知数据绑定系统属性已更改。event 参数可以不指定,如果不指定则默认是自定义属性+ Changed。

5.2.1 注意无限循环情况

        在使用双向数据绑定时,需要特别注意不要引入无限循环。当用户更改属性时,系统会调用使用 @InverseBindingAdapter 注解的方法,并将值分配给后备属性。继而会调用使用 @BindingAdapter 注解的方法,从而触发对使用 @InverseBindingAdapter 注解的方法的另一次调用,依此类推。

        因此,通过在 @BindingAdapter 注解的方法中通过判断新值和旧值是否相等,从而来打破可能引起的无限循环情况。

5.2. @InverseBindingMethods(自定义View的双向绑定注解)

    @InverseBindingMethods 注解跟单向绑定的 @BindingMethods 注解一样,它们都是更适用于自定义View,而 @InverseBindingMethods 就是 @BindingMethods 的双向绑定版本,来看看示例:

自定义EditText:

@BindingMethods(
    BindingMethod(type = CustomEditTextView::class, attribute = "customText", method = "setCustomText")
)
@InverseBindingMethods(
    InverseBindingMethod(type = CustomEditTextView::class, attribute = "customText", method = "getCustomText", event = "customTextAttrChanged")
)
class CustomEditTextView: EditText {
     companion object{
        @JvmStatic
        @BindingAdapter("customTextAttrChanged")
        fun setCustomTextListener(view: EditText, textAttrChanged: InverseBindingListener?) {
            if (textAttrChanged != null) {
                view.addTextChangedListener(object : TextWatcher {
                    override fun beforeTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
                    override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
                    override fun afterTextChanged(s: Editable) {
                        textAttrChanged.onChange()
                    }
                })
            }
        }
    }
    constructor(context: Context?) : super(context) {
    }
    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
    }
    fun setCustomText(newText: String?) {
        if (text.toString() != newText) {
            setText(newText)
        }
    }
    fun getCustomText():String {
        return text.toString()
    }
}

布局:

<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@{userObservable.name}"/>
<com.example.CustomEditTextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:customText="@={userObservable.name}"/>

示例中,同样当用户在 CustomEditTextView 中输入更改后的新值时,TextView 中可以立即响应值的变化。

        上面单向绑定时我们了解到 @BindingMethods 注解用于支持自定义视图中属性进行绑定行为扩展。所以此示例中从代码到 View 的数据绑定依然是依赖 @BindingMethods 来支持;

        而当从 View 控件文本发生数据变化再同步到代码中,则是 @InverseBindingMethods 的功劳,它跟 @InverseBindingAdapter 一样,也是需要在用户对 EditText 的文本变化时,获取新的值更新到 customText 属性和它的 event 参数指定了一个值变化监听器用于当 EditText 的文本变化后,通知数据绑定系统属性已更改。

注意:

        因为监听器方法必须为静态方法,所以示例中将方法放置在伴生对象中定义并且添加 @JvmStatic 注解。

5.4 @InverseMethod(双向绑定的类型转换)

        从上面的单向绑定学习时了解到,如果 View 中展示在表达式里某一个非 String 类型的字段时,可以直接使用 String.valueOf 进行类型的转换,如:android:text="@{String.valueOf(index + 1)}";如果通过表达式进行逻辑处理后,发生的类型转换,可以使用  @BindingConversion 注解来转换。

        在双向绑定中遇上此情况,就要需要使用 @InverseMethod 来处理双向绑定时的方法逆变换。请看示例:

创建一个Converter类:

object Converter {
    @JvmStatic
    @InverseMethod("stringToInt")
    fun intToString(value: Int): String {
        return value.toString()
    }
    @JvmStatic
    fun stringToInt(value: String): Int {
        return try {
            value.toInt()
        } catch (e: NumberFormatException) {
            0
        }
    }
}

布局:

<EditText
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@={Converter.intToString(user.age)}"
    android:inputType="number" />

同样的 @InverseMethod 注解也必须为静态方法,以及它接收另一个反向转换方法名称作为参数。

6. 总结

        Data Binding 是官方推荐的视图数据绑定方案。它减少了手动编写代码来实时同步 UI 和数据的工作量,提高了开发效率。自动同步机制不仅减少了人为同步时容易产生的错误,还使代码更容易维护和扩展,因为 UI 逻辑和数据逻辑得到了明确分离。

        更多详细的 Data Binding 介绍,请访问 Android 开发者官网

  • 30
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
很抱歉,由于篇幅限制,无法在此处提供完整的代码。但我可以为您提供一些核心代码片段和步骤: 1. 首先,在 build.gradle 中添加以下依赖: ``` dependencies { implementation 'com.google.android.exoplayer:exoplayer:2.12.1' } ``` 2. 在您的布局文件中创建一个用户界面,包含播放/暂停按钮,歌曲名称、歌手和播放进度条。 ``` <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <TextView android:id="@+id/song_title" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Song Title" /> <TextView android:id="@+id/song_artist" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Song Artist" /> <SeekBar android:id="@+id/playback_seekbar" android:layout_width="match_parent" android:layout_height="wrap_content" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <ImageButton android:id="@+id/play_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/ic_play_arrow_black_24dp" /> <ImageButton android:id="@+id/pause_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/ic_pause_black_24dp" /> </LinearLayout> </LinearLayout> ``` 3. 创建一个数据模型来存储歌曲的信息,例如歌曲名称、歌手、专辑、时长和文件路径。 ``` data class Song( val title: String, val artist: String, val album: String, val duration: Long, val uri: Uri ) ``` 4. 创建一个歌曲列表,并使用 RecyclerView 显示歌曲信息。 ``` class SongListAdapter( private val songs: List<Song>, private val onItemClick: (Song) -> Unit ) : RecyclerView.Adapter<SongListAdapter.ViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val view = LayoutInflater.from(parent.context) .inflate(R.layout.item_song, parent, false) return ViewHolder(view) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { val song = songs[position] holder.titleTextView.text = song.title holder.artistTextView.text = song.artist holder.itemView.setOnClickListener { onItemClick(song) } } override fun getItemCount(): Int = songs.size class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val titleTextView: TextView = itemView.findViewById(R.id.song_title) val artistTextView: TextView = itemView.findViewById(R.id.song_artist) } } ``` 5. 创建一个音乐播放器服务,以便在后台播放音乐,并将其绑定到 Activity 中。 ``` class MusicService : Service() { private lateinit var exoPlayer: SimpleExoPlayer private var currentSong: Song? = null override fun onBind(intent: Intent): IBinder? = MusicBinder() inner class MusicBinder : Binder() { fun getService(): MusicService = this@MusicService } override fun onCreate() { super.onCreate() exoPlayer = SimpleExoPlayer.Builder(this).build() } override fun onDestroy() { super.onDestroy() exoPlayer.release() } fun play(song: Song) { if (song != currentSong) { currentSong = song val mediaItem = MediaItem.fromUri(song.uri) exoPlayer.setMediaItem(mediaItem) exoPlayer.prepare() } exoPlayer.play() } fun pause() { exoPlayer.pause() } fun seekTo(position: Long) { exoPlayer.seekTo(position) } fun getCurrentPosition(): Long = exoPlayer.currentPosition fun getDuration(): Long = exoPlayer.duration fun isPlaying(): Boolean = exoPlayer.isPlaying } ``` 6. 在音乐播放器服务中实现播放、暂停、跳转到下一首和上一首歌曲的功能。 ``` fun play(song: Song) { if (song != currentSong) { currentSong = song val mediaItem = MediaItem.fromUri(song.uri) exoPlayer.setMediaItem(mediaItem) exoPlayer.prepare() } exoPlayer.play() } fun pause() { exoPlayer.pause() } fun seekTo(position: Long) { exoPlayer.seekTo(position) } fun skipToNext() { val currentSongIndex = songs.indexOf(currentSong) val nextSongIndex = (currentSongIndex + 1) % songs.size val nextSong = songs[nextSongIndex] play(nextSong) } fun skipToPrevious() { val currentSongIndex = songs.indexOf(currentSong) val previousSongIndex = if (currentSongIndex == 0) { songs.size - 1 } else { currentSongIndex - 1 } val previousSong = songs[previousSongIndex] play(previousSong) } ``` 7. 在 Activity 中实现与音乐播放器服务的通信,以便更新用户界面和处理用户的输入。 ``` class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private lateinit var musicService: MusicService private var isBound = false private val connection = object : ServiceConnection { override fun onServiceConnected(name: ComponentName?, service: IBinder?) { val binder = service as MusicService.MusicBinder musicService = binder.getService() isBound = true } override fun onServiceDisconnected(name: ComponentName?) { isBound = false } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) val songList = listOf( Song("Song 1", "Artist 1", "Album 1", 180000, Uri.parse("path/to/song1.mp3")), Song("Song 2", "Artist 2", "Album 2", 240000, Uri.parse("path/to/song2.mp3")), Song("Song 3", "Artist 3", "Album 3", 300000, Uri.parse("path/to/song3.mp3")) ) val songListAdapter = SongListAdapter(songList) { song -> musicService.play(song) updateUI() } binding.songList.adapter = songListAdapter binding.playButton.setOnClickListener { musicService.play() updateUI() } binding.pauseButton.setOnClickListener { musicService.pause() updateUI() } binding.playbackSeekbar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { if (fromUser) { musicService.seekTo(progress.toLong()) } } override fun onStartTrackingTouch(seekBar: SeekBar?) {} override fun onStopTrackingTouch(seekBar: SeekBar?) {} }) } override fun onStart() { super.onStart() bindService(Intent(this, MusicService::class.java), connection, Context.BIND_AUTO_CREATE) } override fun onStop() { super.onStop() if (isBound) { unbindService(connection) isBound = false } } private fun updateUI() { binding.playButton.isEnabled = !musicService.isPlaying() binding.pauseButton.isEnabled = musicService.isPlaying() binding.playbackSeekbar.max = musicService.getDuration().toInt() binding.playbackSeekbar.progress = musicService.getCurrentPosition().toInt() } } ``` 以上是一个简单的步骤和代码示例,希望对您有所帮助!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值