Android DataBinding 全面解析【使用篇】

目录

一、基础概念

[1.1 主要优势](#1.1 主要优势)

[1.2 环境配置](#1.2 环境配置)

二、基础使用

[2.1 简单例子](#2.1 简单例子)

[2.1.1 User 类](#2.1.1 User 类)

[2.1.2 布局文件 activity_main.xml](#2.1.2 布局文件 activity_main.xml)

标签

[标签 - 导入类](#标签 - 导入类)

[标签 - 声明数据变量](#标签 - 声明数据变量)

[2.1.3 MainActivity.java](#2.1.3 MainActivity.java)

[2.2 核心讲解](#2.2 核心讲解)

[2.2.1 数据对象类型](#2.2.1 数据对象类型)

(1)基础数据对象 (POJO)

[(2)可观察字段 (ObservableFields) - 最简单直接](#(2)可观察字段 (ObservableFields) - 最简单直接)

[(3)可观察对象 (BaseObservable) - 适合复杂业务](#(3)可观察对象 (BaseObservable) - 适合复杂业务)

[(4)可观察集合 (Observable Collections) - 用于动态数据](#(4)可观察集合 (Observable Collections) - 用于动态数据)

[2.2.2 绑定类型](#2.2.2 绑定类型)

[(1)单向绑定 语法:@{}](#(1)单向绑定 语法:@{})

[(2)双向绑定 语法:@={}](#(2)双向绑定 语法:@={})

(3)事件绑定

[2.2.3 注解 BindingAdapter](#2.2.3 注解 BindingAdapter)

(1)图片加载场景

(2)自定义视图属性场景

(3)网络请求和数据加载场景

(4)格式化与转换场景

(5)动画和效果场景

[2.2.4 注解 BindingConversion](#2.2.4 注解 BindingConversion)

(1)颜色相关转换

(2)字符串相关转换

[(3) 数值类型转换](#(3) 数值类型转换)

(4)日期和时间转换

(5)集合类型转换


系列入口导航:Android Jetpack 概述

一、基础概念

DataBinding 是 Android Jetpack 的一部分,它允许你在布局文件中直接绑定 UI 组件和数据源,减少样板代码,提高开发效率。

1.1 主要优势

  1. 减少 findViewById 调用

  2. 自动空安全处理

  3. 支持表达式语言

  4. 双向数据绑定支持

  5. LiveData、ViewModel完美集成

1.2 环境配置

在 app/build.gradle 中添加:

java 复制代码
android {
    ...
    buildFeatures {
        dataBinding true
    }
}

二、基础使用

我们先从一个简单的例子开始(使用普通 POJO),来看看 DataBinding 的基本使用,后续我们一点点的扩展。

2.1 简单例子

2.1.1 User 类

java 复制代码
package com.example.databindingdemo;

public class User {
    private String name;
    private int age;
    
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public String getName() { return name; }
    public int getAge() { return age; }
    
    public void setName(String name) { this.name = name; }
    public void setAge(int age) { this.age = age; }
}

这里定义一个普通的Bean类,方便大家理解。

2.1.2 布局文件 activity_main.xml

XML 复制代码
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <!-- 导入View类,用于VISIBLE/GONE常量 -->
        <import type="android.view.View" />
        
        <!-- 定义一个变量,类型是我们的User类 -->
        <variable
            name="user"
            type="com.example.databindingdemo.User" />
    </data>

    <!-- 原来的根布局(可以是任何布局) -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:gravity="center"
        android:padding="20dp">

        <!-- 显示用户名 -->
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.name}"
            android:textSize="24sp"
            android:textStyle="bold" />

        <!-- 显示年龄 -->
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{String.valueOf(user.age)}"
            android:textSize="20sp"
            android:layout_marginTop="10dp" />

        <!-- 根据年龄显示不同文本 -->
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.age >= 18 ? '成年人' : '未成年人'}"
            android:textSize="18sp"
            android:textColor="@{user.age >= 18 ? @android:color/holo_green_dark : @android:color/holo_red_dark}"
            android:layout_marginTop="20dp" />

        <!-- 年龄大于18才显示这个TextView -->
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="已成年,可以进入"
            android:textSize="16sp"
            android:visibility="@{user.age >= 18 ? View.VISIBLE : View.GONE}"
            android:layout_marginTop="10dp" />

        <!-- 更新按钮 -->
        <Button
            android:id="@+id/btnUpdate"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="增加年龄"
            android:layout_marginTop="30dp" />

    </LinearLayout>
</layout>

接下来的知识点只是点下,后面会做更详细更全面的讲解。

<data>标签

<data>标签是DataBinding布局文件的"数据声明区 ",就像是连接Java/Kotlin世界和XML世界的桥梁。它位于<layout>标签内部,在实际UI布局之前

XML 复制代码
<layout>
    <data>
        <!-- 这里定义数据和导入 -->
    </data>
    
    <!-- 这里才是UI布局 -->
    <LinearLayout>
        ...
    </LinearLayout>
</layout>
<import>标签 - 导入类

<import>标签让你可以在XML中使用Java/Kotlin类

XML 复制代码
<data>
    <!-- 导入系统类 -->
    <import type="android.view.View" />
    
    <!-- 导入自定义类 -->
    <import type="com.example.util.StringUtils" />
    
    <!-- 导入后可以给类起别名 -->
    <import 
        type="com.example.model.User" 
        alias="AppUser" />
</data>

导入后就可以在绑定表达式中直接使用这些类,比如我们后面看到 "@{user.age >= 18 ? View.VISIBLE : View.GONE}" 。

<variable>标签 - 声明数据变量

<variable>标签定义了可以在XML中访问的数据对象:

XML 复制代码
<data>
    <variable
        name="user"           <!-- 变量名,在XML中使用 -->
        type="com.example.User" />  <!-- 完整类名 -->
</data>

在XML中可以直接访问其对应的属性

XML 复制代码
<TextView
    android:text="@{user.name}" />  <!-- 访问user对象的name属性 -->
<TextView
    android:text="@{String.valueOf(user.age)}" />

2.1.3 MainActivity.java

java 复制代码
package com.example.databindingdemo;

import androidx.appcompat.app.AppCompatActivity;
import androidx.databinding.DataBindingUtil;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;

// 自动生成的绑定类,名字是布局文件名转驼峰命名 + "Binding"
import com.example.databindingdemo.databinding.ActivityMainBinding;

public class MainActivity extends AppCompatActivity {

    private ActivityMainBinding binding;
    private User user;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        // 方法1:使用DataBindingUtil.setContentView(推荐)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        
        // 方法2:也可以这样写
        // binding = ActivityMainBinding.inflate(getLayoutInflater());
        // setContentView(binding.getRoot());
        
        // 创建User对象
        user = new User("张三", 20);
        
        // 将user对象设置给布局
        binding.setUser(user);
        
        // 设置按钮点击事件(传统方式,也可以绑定)
        binding.btnUpdate.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 修改数据
                int newAge = user.getAge() + 1;
                user.setAge(newAge);
                
                // 关键:普通POJO需要重新设置或通知更新
                binding.setUser(user);  // 重新设置整个对象
                // 或者:binding.invalidateAll();  // 强制刷新所有绑定
                
                Toast.makeText(MainActivity.this, 
                    "年龄已更新为: " + newAge, 
                    Toast.LENGTH_SHORT).show();
            }
        });
    }
}

我们首先看到的是 ActivityMainBinding ,DataBinding会根据布局文件名自动生成对应的绑定类:

布局文件名 生成的绑定类名 说明
activity_main.xml ActivityMainBinding 单词首字母大写,去掉下划线
item_user.xml ItemUserBinding 同上规则
fragment_profile.xml FragmentProfileBinding 同上规则
dialog_confirm.xml DialogConfirmBinding 同上规则

然后,直接设置布局并获取绑定 DataBindingUtil.setContentView(this, R.layout.activity_main) ,在Activity场景推荐使用这个方法。

接下来就是User实体类赋值给 binding , binding.setUser(user)这行代码是根据你定义了的<variable> 生成的,get方法原理也是一样的,具体怎么生成的以后的章节会讲解。

XML 复制代码
 <variable
    name="user"
    type="com.example.databindingdemo.User" />

假如这里改为name="user1 " ,生成的方法就会是 setUser1()【 set + 变量名(首字母大写)】

2.2 核心讲解

根据上面提到的内容进行针对讲解。

2.2.1 数据对象类型

(1)基础数据对象 (POJO)

这是最入门、最简单的形式。你可以使用任何普通的Java对象(POJO)作为数据源。上面的例子就是使用这种方式。数据绑定是一次性的 。当数据对象的值发生变化时,UI不会自动更新。如果你需要更新UI,必须重新对整个binding对象设置数据

对应着上面例子的代码,执行了 user.setAge(newAge) 数据没有发生改变,如果不重新调用 binding.setUser(user),界面上的年龄不会变化。接下来我们介绍的对象,完全避免了这种情况。

为了实现数据与UI的自动同步更新,必须使用可观察的数据对象。DataBinding提供了三种主要方式

(2)可观察字段 (ObservableFields) - 最简单直接

如果数据类的字段不多,这是最便捷的方式。它将每个字段包装成一个特殊的可观察对象。

核心特点:字段变为public final,通过get()和set()读写。当set()被调用时,UI会自动更。

java 复制代码
import androidx.databinding.ObservableField;
import androidx.databinding.ObservableInt;

public class User {
    // public final 修饰,泛型指定类型
    public final ObservableField<String> name = new ObservableField<>();
    public final ObservableInt age = new ObservableInt(); // 避免装箱

    public User(String name, int age) {
        this.name.set(name);
        this.age.set(age);
    }
}

在Activity中使用

java 复制代码
User user = new User("张三", 20);
binding.setUser(user);

// 直接修改字段的值,UI 会自动更新
user.name.set("李四"); // TextView 立刻显示 "李四"

ObservableInt 没有自动装箱的过程,性能是比 ObservableField 更好的,下面的例子很清晰的展现了这一点

java 复制代码
// ❌ 使用泛型ObservableField,会有自动装箱
public class User {
    // 每次set/get都会发生自动装箱/拆箱
    public ObservableField<Integer> age = new ObservableField<>();
    
    public void updateAge(int newAge) {
        age.set(newAge);  // int → Integer(自动装箱)
        int value = age.get();  // Integer → int(自动拆箱)
    }
}

// ✅ 使用ObservableInt,没有自动装箱
public class User {
    // ObservableInt内部直接存储int,没有装箱过程
    public ObservableInt age = new ObservableInt<>();
    
    public void updateAge(int newAge) {
        age.set(newAge);  // 直接存储int值
        int value = age.get();  // 直接返回int值
    }
}
类名 对应Java类型 使用示例
ObservableBoolean boolean ObservableBoolean isVip = new ObservableBoolean();
ObservableByte byte ObservableByte flag = new ObservableByte();
ObservableChar char ObservableChar grade = new ObservableChar();
ObservableShort short ObservableShort count = new ObservableShort();
ObservableInt int ObservableInt age = new ObservableInt();
ObservableLong long ObservableLong timestamp = new ObservableLong();
ObservableFloat float ObservableFloat price = new ObservableFloat();
ObservableDouble double ObservableDouble height = new ObservableDouble();
ObservableField<T> 任意对象 ObservableField<String> name = new ObservableField<>();
ObservableParcelable<T> Parcelable对象 ObservableParcelable<User> user = new ObservableParcelable<>();
ObservableArrayList<T> ArrayList<T> ObservableArrayList<String> list = new ObservableArrayList<>();
ObservableArrayMap<K, V> ArrayMap<K, V> ObservableArrayMap<String, Object> map = new ObservableArrayMap<>();
(3)可观察对象 (BaseObservable) - 适合复杂业务

让数据类继承BaseObservable并在getter上添加@Bindable注解,在setter中通知变更。这种方式的结构更清晰,适合包含复杂逻辑的类

核心特点 :通过注解和手动通知,粒度可以控制到具体哪个属性更新

java 复制代码
import androidx.databinding.BaseObservable;
import androidx.databinding.Bindable;
// 注意:BR 类是 DataBinding 自动生成的,类似 R 文件
import com.example.BR;

public class User extends BaseObservable {
    private String name;
    private int age;

    @Bindable // 标记这个getter对应的字段是可观察的
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
        // 通知系统 BR.name 这个属性变了,UI 中用到它的地方都要更新
        notifyPropertyChanged(BR.name);
    }

    @Bindable
    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
        notifyPropertyChanged(BR.age);
    }

    // 如果需要一次性更新所有属性,可以调用 notifyChange();
}

布局文件的用法和基础对象完全一样,@{user.name}会自动找到getter。

(4)可观察集合 (Observable Collections) - 用于动态数据

适用于列表或Map这种结构不确定的数据。

  • ObservableArrayMap:键值对形式,键通常是String
java 复制代码
// Java
ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put("name", "张三");
user.put("age", 20);
binding.setUser(user);
XML 复制代码
<!-- 布局文件中 -->
<TextView android:text="@{user['name']}" />
<TextView android:text="@{String.valueOf(user['age'])}" />
  • ObservableArrayList:列表形式,通过索引访问
java 复制代码
// Java
ObservableArrayList<Object> user = new ObservableArrayList<>();
user.add("张三"); // index 0
user.add(20);     // index 1
binding.setUser(user);
XML 复制代码
<!-- 布局文件中 -->
<TextView android:text='@{user[0]}' />
<TextView android:text='@{String.valueOf(user[1])}' />
数据对象类型 实现方式 UI自动更新 适用场景 代码侵入性
基础POJO 普通Java类 不支持 静态页面,或数据设置后不再变化
可观察字段 ObservableField / ObservableInt 支持 字段较少、结构简单的数据模型
可观察对象 继承 BaseObservable + @Bindable 支持 业务逻辑复杂、字段较多的数据模型
可观察集合 ObservableArrayMap / ObservableArrayList 支持 数据格式动态变化(如后端返回的未知结构)

2.2.2 绑定类型

(1)单向绑定 语法:@{}

这是最基础、最常用的绑定方式。在 Java 代码中修改数据对象(必须是可观察对象),UI 会自动刷新。

XML 复制代码
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{user.name}" />

这里就先讲使用,后续章节再讲解原理。

(2)双向绑定 语法:@={}
XML 复制代码
<EditText
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@={viewModel.userName}" />

双向绑定要求数据对象必须是可观察的,否则无法将 UI 的变化写回数据。

双向绑定 @={} 实际上是两个操作的合体

  • 单向绑定:数据变化时更新 EditText。
  • 事件监听:监听 EditText 的文本变化(addTextChangedListener),当文本改变时,调用数据对象的 setter。

DataBinding 自动为我们生成了这个监听器的代码,省去了手动编写的麻烦 。在使用双向绑定时,特别是自定义双向绑定时,需要注意避免无限循环

  1. UI 变化 → 调用数据对象的 setter

  2. 数据对象的 setternotifyChange → UI 更新

  3. UI 更新 → 触发监听器 → 调用 setter → ...(循环)

DataBinding 内部有机制避免这种情况(通过比较新旧值),但在自定义InverseBindingListener 时需要特别注意。

控件 属性 绑定类型
EditText android:text 双向
CheckBox android:checked 双向
RadioButton android:checked 双向
Switch android:checked 双向
SeekBar android:progress 双向
RatingBar android:rating 双向
(3)事件绑定

事件绑定虽然也是单向的(UI → 代码),但它的作用不同,用于处理用户操作

  • 方法引用:方法签名必须与监听器方法完全一致。
java 复制代码
public class EventHandler {
    public void onButtonClick(View view) {
        // 处理点击
    }
}
XML 复制代码
<Button
    android:onClick="@{handler::onButtonClick}" />
  • **监听器绑定(Lambda 表达式):**更灵活,可以在表达式中写简单的逻辑,或者调用带参数的方法。
java 复制代码
public class EventHandler {
    public void onItemClick(String itemId) {
        // 处理点击,带参数
    }
}
XML 复制代码
<Button
    android:onClick="@{() -> handler.onItemClick(item.id)}" />
<!-- 也可以传入 View 对象 -->
<Button
    android:onClick="@{(view) -> handler.onItemClick(view, item.id)}" />
特性 单向绑定 @{} 双向绑定 @={} 事件绑定 :: / ->
数据流向 数据 → UI 数据 ↔ UI UI → 方法
典型控件 TextViewImageView EditTextCheckBox Button
数据要求 可观察对象 可观察对象 普通方法
更新时机 数据变化 数据或 UI 变化 用户操作时
适用场景 展示数据 表单输入 响应点击、长按

2.2.3 注解 BindingAdapter

BindingAdapter 用于自定义属性绑定逻辑,主要解决标准 XML 属性无法满足的需求。

(1)图片加载场景

这是最经典的 BindingAdapter 使用场景,用于统一处理图片加载库。

场景描述:项目中统一使用 Glide 加载图片,需要处理 URL、占位图、错误图、圆角、圆形等多种情况。

java 复制代码
public class ImageBindingAdapters {
    
    // 场景1:基础图片加载 - 只需传入 URL
    @BindingAdapter("imageUrl")
    public static void loadImage(ImageView view, String url) {
        if (TextUtils.isEmpty(url)) {
            view.setImageResource(R.drawable.ic_default);
            return;
        }
        Glide.with(view.getContext())
            .load(url)
            .placeholder(R.drawable.ic_placeholder)
            .error(R.drawable.ic_error)
            .into(view);
    }
    
    // 场景2:带占位图的图片加载 - 可自定义占位图
    @BindingAdapter(value = {"imageUrl", "placeholder"}, requireAll = false)
    public static void loadImage(ImageView view, String url, Drawable placeholder) {
        RequestOptions options = new RequestOptions();
        if (placeholder != null) {
            options.placeholder(placeholder);
        }
        
        Glide.with(view.getContext())
            .load(url)
            .apply(options)
            .error(R.drawable.ic_error)
            .into(view);
    }
    
    // 场景3:圆形头像 - 专门用于用户头像
    @BindingAdapter("circleImage")
    public static void loadCircleImage(ImageView view, String url) {
        Glide.with(view.getContext())
            .load(url)
            .placeholder(R.drawable.ic_default_avatar)
            .error(R.drawable.ic_default_avatar)
            .circleCrop()
            .into(view);
    }
    
    // 场景4:圆角图片 - 用于卡片等场景
    @BindingAdapter(value = {"imageUrl", "radius"}, requireAll = false)
    public static void loadRoundImage(ImageView view, String url, float radius) {
        float radiusInPx = TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_DIP, 
            radius, 
            view.getResources().getDisplayMetrics()
        );
        
        Glide.with(view.getContext())
            .load(url)
            .placeholder(R.drawable.ic_placeholder)
            .transform(new RoundedCorners((int) radiusInPx))
            .into(view);
    }
    
    // 场景5:高斯模糊背景图 - 用于背景层
    @BindingAdapter("blurImage")
    public static void loadBlurImage(ImageView view, String url) {
        Glide.with(view.getContext())
            .load(url)
            .transform(new BlurTransformation(25)) // 25是模糊程度
            .into(view);
    }
}

布局中使用

XML 复制代码
<!-- 普通图片 -->
<ImageView
    android:layout_width="100dp"
    android:layout_height="100dp"
    app:imageUrl="@{product.imageUrl}" />

<!-- 圆形头像 -->
<ImageView
    android:layout_width="50dp"
    android:layout_height="50dp"
    app:circleImage="@{user.avatarUrl}" />

<!-- 圆角图片 -->
<ImageView
    android:layout_width="match_parent"
    android:layout_height="200dp"
    app:imageUrl="@{banner.imageUrl}"
    app:radius="@{8}" />

<!-- 模糊背景 -->
<ImageView
    android:layout_width="match_parent"
    android:layout_height="200dp"
    app:blurImage="@{background.imageUrl}" />

当我们在 XML 布局中使用了自定义属性(比如 app:imageUrl)DataBinding 就会去找对应的、带有 @BindingAdapter 注解的方法,然后把视图和数据传进去,由这个方法来决定如何处理这个属性。

(2)自定义视图属性场景

用于扩展系统视图的功能,添加自定义行为。

java 复制代码
public class CustomViewBindingAdapters {
    
    // 场景6:自动调整字体大小适应TextView宽度
    @BindingAdapter("autoFitText")
    public static void setAutoFitText(TextView view, boolean autoFit) {
        if (autoFit) {
            view.getViewTreeObserver().addOnGlobalLayoutListener(
                new ViewTreeObserver.OnGlobalLayoutListener() {
                    @Override
                    public void onGlobalLayout() {
                        int availableWidth = view.getWidth() - view.getPaddingLeft() - view.getPaddingRight();
                        // 自动计算并设置合适的字体大小
                        float textSize = calculateFitTextSize(view.getText().toString(), availableWidth);
                        view.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
                        
                        view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                    }
                });
        }
    }
    
    // 场景7:设置加载状态 - 用于按钮的加载状态切换
    @BindingAdapter(value = {"loading", "loadingText"}, requireAll = false)
    public static void setLoadingState(Button button, boolean isLoading, String loadingText) {
        if (isLoading) {
            button.setEnabled(false);
            button.setText(loadingText != null ? loadingText : "加载中...");
            // 可以添加进度条等
        } else {
            button.setEnabled(true);
            // 恢复原始文本
        }
    }
    
    // 场景8:设置密码可见性切换 - 用于登录注册页面
    @BindingAdapter("passwordVisible")
    public static void setPasswordVisible(EditText editText, boolean visible) {
        int selection = editText.getSelectionEnd();
        if (visible) {
            editText.setTransformationMethod(null); // 显示密码
        } else {
            editText.setTransformationMethod(PasswordTransformationMethod.getInstance()); // 隐藏密码
        }
        // 保持光标位置
        editText.setSelection(selection);
    }
    
    // 场景9:格式化货币显示
    @BindingAdapter("currencyFormat")
    public static void setCurrencyFormat(TextView view, double amount) {
        DecimalFormat formatter = new DecimalFormat("¥#,##0.00");
        view.setText(formatter.format(amount));
    }
    
    // 场景10:根据评分显示星星
    @BindingAdapter(value = {"rating", "maxRating"}, requireAll = false)
    public static void setRating(LinearLayout container, float rating, float maxRating) {
        container.removeAllViews();
        int starCount = Math.round(rating);
        for (int i = 0; i < maxRating; i++) {
            ImageView star = new ImageView(container.getContext());
            if (i < starCount) {
                star.setImageResource(R.drawable.ic_star_filled);
            } else {
                star.setImageResource(R.drawable.ic_star_empty);
            }
            container.addView(star);
        }
    }
    
    private static float calculateFitTextSize(String text, int maxWidth) {
        // 计算合适字体大小的算法
        return 16f; // 简化返回
    }
}

布局中使用

XML 复制代码
<!-- 自动调整字体 -->
<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@{product.title}"
    app:autoFitText="@{true}" />

<!-- 加载状态按钮 -->
<Button
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="登录"
    app:loading="@{viewModel.isLoggingIn}"
    app:loadingText="@{'登录中...'}" />

<!-- 密码可见性 -->
<EditText
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@={viewModel.password}"
    app:passwordVisible="@{viewModel.showPassword}" />

<!-- 货币显示 -->
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:currencyFormat="@{product.price}" />

<!-- 评分显示 -->
<LinearLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    app:rating="@{product.rating}"
    app:maxRating="@{5}" />
(3)网络请求和数据加载场景

用于处理异步数据加载和状态显示。

java 复制代码
public class DataLoadingBindingAdapters {
    
    // 场景11:根据加载状态显示不同视图
    @BindingAdapter(value = {"loadingState", "loadingView", "errorView", "contentView"}, requireAll = false)
    public static void handleLoadingState(ViewGroup container, 
                                           LoadingState state,
                                           View loadingView,
                                           View errorView,
                                           View contentView) {
        
        // 隐藏所有视图
        loadingView.setVisibility(View.GONE);
        errorView.setVisibility(View.GONE);
        contentView.setVisibility(View.GONE);
        
        // 根据状态显示对应视图
        switch (state) {
            case LOADING:
                loadingView.setVisibility(View.VISIBLE);
                break;
            case ERROR:
                errorView.setVisibility(View.VISIBLE);
                break;
            case SUCCESS:
                contentView.setVisibility(View.VISIBLE);
                break;
        }
    }
    
    // 场景12:自动加载更多(用于RecyclerView分页)
    @BindingAdapter("onLoadMore")
    public static void setOnLoadMore(RecyclerView recyclerView, Runnable onLoadMore) {
        recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                
                LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
                int visibleItemCount = layoutManager.getChildCount();
                int totalItemCount = layoutManager.getItemCount();
                int firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition();
                
                // 当滚动到底部时触发加载更多
                if ((visibleItemCount + firstVisibleItemPosition) >= totalItemCount
                        && firstVisibleItemPosition >= 0) {
                    if (onLoadMore != null) {
                        onLoadMore.run();
                    }
                }
            }
        });
    }
    
    // 场景13:下拉刷新绑定
    @BindingAdapter("onRefresh")
    public static void setOnRefresh(SwipeRefreshLayout layout, Runnable onRefresh) {
        layout.setOnRefreshListener(() -> {
            onRefresh.run();
            layout.setRefreshing(false); // 刷新完成后停止动画
        });
    }
    
    public enum LoadingState {
        LOADING, SUCCESS, ERROR
    }
}
(4)格式化与转换场景

用于数据展示前的格式化处理。

java 复制代码
public class FormattingBindingAdapters {
    
    // 场景14:时间戳格式化
    @BindingAdapter("formatDate")
    public static void setFormattedDate(TextView view, long timestamp) {
        Date date = new Date(timestamp);
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault());
        view.setText(sdf.format(date));
    }
    
    // 场景15:相对时间显示(几分钟前、几小时前)
    @BindingAdapter("relativeTime")
    public static void setRelativeTime(TextView view, long timestamp) {
        long now = System.currentTimeMillis();
        long diff = now - timestamp;
        
        String result;
        if (diff < 60 * 1000) {
            result = "刚刚";
        } else if (diff < 60 * 60 * 1000) {
            result = (diff / (60 * 1000)) + "分钟前";
        } else if (diff < 24 * 60 * 60 * 1000) {
            result = (diff / (60 * 60 * 1000)) + "小时前";
        } else {
            result = (diff / (24 * 60 * 60 * 1000)) + "天前";
        }
        view.setText(result);
    }
    
    // 场景16:数字格式化(千位分隔符)
    @BindingAdapter("numberFormat")
    public static void setNumberFormat(TextView view, long number) {
        DecimalFormat formatter = new DecimalFormat("#,###");
        view.setText(formatter.format(number));
    }
    
    // 场景17:文件大小格式化
    @BindingAdapter("fileSize")
    public static void setFileSize(TextView view, long bytes) {
        String[] units = {"B", "KB", "MB", "GB", "TB"};
        int unitIndex = 0;
        double size = bytes;
        
        while (size >= 1024 && unitIndex < units.length - 1) {
            size /= 1024;
            unitIndex++;
        }
        
        DecimalFormat df = new DecimalFormat("#.##");
        view.setText(df.format(size) + " " + units[unitIndex]);
    }
    
    // 场景18:手机号脱敏显示
    @BindingAdapter("privacyPhone")
    public static void setPrivacyPhone(TextView view, String phone) {
        if (phone != null && phone.length() == 11) {
            String privacyPhone = phone.substring(0, 3) + "****" + phone.substring(7);
            view.setText(privacyPhone);
        } else {
            view.setText(phone);
        }
    }
}
(5)动画和效果场景

用于添加视觉效果和动画。

java 复制代码
public class AnimationBindingAdapters {
    
    // 场景19:渐入渐出动画
    @BindingAdapter(value = {"fadeIn", "duration"}, requireAll = false)
    public static void setFadeIn(View view, boolean shouldFade, int duration) {
        if (shouldFade && view.getVisibility() == View.VISIBLE) {
            view.setAlpha(0f);
            view.animate()
                .alpha(1f)
                .setDuration(duration > 0 ? duration : 300)
                .start();
        }
    }
    
    // 场景20:闪烁动画(用于强调)
    @BindingAdapter("blink")
    public static void setBlink(View view, boolean shouldBlink) {
        if (shouldBlink) {
            ObjectAnimator animator = ObjectAnimator.ofFloat(view, "alpha", 1f, 0.3f, 1f);
            animator.setDuration(1000);
            animator.setRepeatCount(ValueAnimator.INFINITE);
            animator.start();
            view.setTag(R.id.animator_tag, animator);
        } else {
            ObjectAnimator animator = (ObjectAnimator) view.getTag(R.id.animator_tag);
            if (animator != null) {
                animator.cancel();
            }
            view.setAlpha(1f);
        }
    }
    
    // 场景21:点击缩放效果
    @BindingAdapter("clickScale")
    public static void setClickScale(View view, boolean enable) {
        if (enable) {
            view.setOnTouchListener((v, event) -> {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        v.animate().scaleX(0.95f).scaleY(0.95f).setDuration(100).start();
                        break;
                    case MotionEvent.ACTION_UP:
                    case MotionEvent.ACTION_CANCEL:
                        v.animate().scaleX(1f).scaleY(1f).setDuration(100).start();
                        break;
                }
                return false;
            });
        }
    }
}

2.2.4 注解 BindingConversion

BindingConversion 用于自动类型转换,当绑定的数据类型与属性期望的类型不一致时,自动调用转换方法。

(1)颜色相关转换

场景描述:XML 中 background 属性期望 Drawable 类型,但经常需要传入颜色值。

java 复制代码
public class ColorConversions {
    
    // 场景1:颜色资源ID -> ColorDrawable
    @BindingConversion
    public static ColorDrawable convertColorToDrawable(int colorResId) {
        return new ColorDrawable(colorResId);
    }
    
    // 场景2:颜色字符串 -> ColorDrawable
    @BindingConversion
    public static ColorDrawable convertColorStringToDrawable(String colorHex) {
        if (colorHex == null) return null;
        try {
            int color = Color.parseColor(colorHex);
            return new ColorDrawable(color);
        } catch (IllegalArgumentException e) {
            return null;
        }
    }
    
    // 场景3:整型颜色值 -> ColorStateList
    @BindingConversion
    public static ColorStateList convertColorToColorStateList(int color) {
        return ColorStateList.valueOf(color);
    }
}

布局中使用

XML 复制代码
<!-- 自动将颜色资源ID转换为Drawable -->
<View
    android:background="@{@color/primary_color}" />
    <!-- 自动调用 convertColorToDrawable -->

<!-- 自动将颜色字符串转换为Drawable -->
<View
    android:background="@{'#FF0000'}" />
    <!-- 自动调用 convertColorStringToDrawable -->

<!-- 自动将颜色值转换为ColorStateList -->
<Button
    android:backgroundTint="@{@color/button_color}" />
    <!-- 自动调用 convertColorToColorStateList -->

BindingConversion 的核心工作原理就是根据输入类型和输出类型的匹配来判断是否进行转换

假如出现相同参数类型,相同返回类型,这在 Java 语法层面就通不过,因为方法签名重复。

java 复制代码
public class DuplicateConflict {
    
    // 两个完全一样的方法签名
    @BindingConversion
    public static String intToString(int value) {
        return "方法1: " + value;
    }
    
    @BindingConversion
    public static String intToString(int value) {  // ❌ 语法错误!
        return "方法2: " + value;
    }
}
(2)字符串相关转换

场景描述:处理字符串与各种类型之间的转换。

java 复制代码
public class StringConversions {
    
    // 场景4:字符串 -> Uri
    @BindingConversion
    public static Uri convertStringToUri(String uriString) {
        return uriString != null ? Uri.parse(uriString) : null;
    }
    
    // 场景5:字符串 -> SpannableString(带样式)
    @BindingConversion
    public static SpannableString convertStringToSpannable(String text) {
        SpannableString spannable = new SpannableString(text);
        // 可以添加默认样式
        spannable.setSpan(new StyleSpan(Typeface.BOLD), 0, text.length(), 
                          Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        return spannable;
    }
    
    // 场景6:字符串数组 -> 逗号分隔字符串
    @BindingConversion
    public static String convertArrayToString(String[] array) {
        if (array == null || array.length == 0) return "";
        return TextUtils.join(", ", array);
    }
    
    // 场景7:数字字符串 -> 数字
    @BindingConversion
    public static int convertStringToInt(String numberStr) {
        try {
            return Integer.parseInt(numberStr);
        } catch (NumberFormatException e) {
            return 0;
        }
    }
}

布局中使用

XML 复制代码
<!-- 字符串自动转Uri -->
<ImageView
    android:src="@{'https://example.com/image.jpg'}" />
    <!-- 自动调用 convertStringToUri -->

<!-- 字符串数组自动转逗号分隔文本 -->
<TextView
    android:text="@{user.tags}" />
    <!-- 如果user.tags是String[],自动转成"tag1, tag2, tag3" -->

<!-- 数字字符串自动转数字 -->
<ProgressBar
    android:max="@{'100'}"
    android:progress="@{'50'}" />
    <!-- 自动调用 convertStringToInt -->
(3) 数值类型转换

场景描述:处理不同数值类型之间的自动转换。

java 复制代码
public class NumberConversions {
    
    // 场景8:dp值 -> px值
    @BindingConversion
    public static int convertDpToPx(float dp) {
        return (int) TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_DIP, 
            dp, 
            Resources.getSystem().getDisplayMetrics()
        );
    }
    
    // 场景9:sp值 -> px值
    @BindingConversion
    public static int convertSpToPx(float sp) {
        return (int) TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_SP, 
            sp, 
            Resources.getSystem().getDisplayMetrics()
        );
    }
    
    // 场景10:布尔值 -> 可见性
    @BindingConversion
    public static int convertBooleanToVisibility(boolean visible) {
        return visible ? View.VISIBLE : View.GONE;
    }
    
    // 场景11:整型 -> 布尔值(用于条件判断)
    @BindingConversion
    public static boolean convertIntToBoolean(int value) {
        return value != 0;
    }
    
    // 场景12:浮点数 -> 百分比字符串
    @BindingConversion
    public static String convertFloatToPercentage(float value) {
        return String.format(Locale.getDefault(), "%.1f%%", value * 100);
    }
}

布局中使用

XML 复制代码
<!-- 自动将dp转换为px -->
<View
    android:layout_width="@{100}"
    android:layout_height="@{200}" />
    <!-- 自动将100/200解释为100dp/200dp -->

<!-- 布尔值自动转可见性 -->
<TextView
    android:visibility="@{user.isVIP}" />
    <!-- 自动将true转为VISIBLE,false转为GONE -->

<!-- 数值自动转百分比 -->
<TextView
    android:text="@{0.75}" />
    <!-- 显示为 "75.0%" -->
(4)日期和时间转换

场景描述:处理日期对象与显示字符串之间的转换。

java 复制代码
public class DateTimeConversions {
    
    // 场景13:Date -> 格式化字符串
    @BindingConversion
    public static String convertDateToString(Date date) {
        if (date == null) return "";
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
        return sdf.format(date);
    }
    
    // 场景14:时间戳 -> 时间字符串
    @BindingConversion
    public static String convertTimestampToTime(long timestamp) {
        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm", Locale.getDefault());
        return sdf.format(new Date(timestamp));
    }
    
    // 场景15:Calendar -> 日期字符串
    @BindingConversion
    public static String convertCalendarToString(Calendar calendar) {
        if (calendar == null) return "";
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日", Locale.getDefault());
        return sdf.format(calendar.getTime());
    }
}

布局中使用

XML 复制代码
<!-- Date对象自动格式化为字符串 -->
<TextView
    android:text="@{user.birthday}" />
    <!-- 自动显示为 "1990-01-01" -->

<!-- 时间戳自动转时间 -->
<TextView
    android:text="@{message.timestamp}" />
    <!-- 自动显示为 "14:30" -->
(5)集合类型转换

场景描述:处理集合与字符串之间的转换。

java 复制代码
public class CollectionConversions {
    
    // 场景16:List -> 逗号分隔字符串
    @BindingConversion
    public static String convertListToString(List<String> list) {
        if (list == null || list.isEmpty()) return "";
        return TextUtils.join("、", list);
    }
    
    // 场景17:Map -> 格式化的键值对字符串
    @BindingConversion
    public static String convertMapToString(Map<String, Object> map) {
        if (map == null || map.isEmpty()) return "";
        StringBuilder builder = new StringBuilder();
        for (Map.Entry<String, Object> entry : map.entrySet()) {
            if (builder.length() > 0) builder.append(";");
            builder.append(entry.getKey()).append(":").append(entry.getValue());
        }
        return builder.toString();
    }
    
    // 场景18:Set -> 排序后的字符串
    @BindingConversion
    public static String convertSetToString(Set<String> set) {
        if (set == null || set.isEmpty()) return "";
        List<String> list = new ArrayList<>(set);
        Collections.sort(list);
        return TextUtils.join("、", list);
    }
}
特性 BindingAdapter BindingConversion
适用场景 需要执行复杂逻辑、需要多个参数、需要访问View 简单的类型转换、不需要额外参数
参数数量 支持多个参数 只能有一个参数
返回值 void(直接操作View) 返回转换后的值
调用时机 属性设置时调用 类型不匹配时自动调用
典型用途 图片加载、自定义行为、动画 颜色转Drawable、数值单位转换、日期格式化
相关推荐
黄林晴2 小时前
Compose Multiplatform 1.10 发布:里程碑式更新!
android
流星白龙2 小时前
【MySQL】19.MySQL用户管理
android·mysql·adb
匆忙拥挤repeat2 小时前
Android Compose 可组合项的生命周期、副作用API
android
hnlgzb3 小时前
目前编写安卓app的话有哪几种设计模式?
android·设计模式·kotlin·android jetpack·compose
studyForMokey4 小时前
【Android面试】Fragment生命周期专题
android·microsoft·面试
Android系统攻城狮5 小时前
Android tinyalsa深度解析之pcm_plugin_open调用流程与实战(一百七十四)
android·pcm·tinyalsa·音频进阶手册
用户622386252175 小时前
Android 列表控件实战:从 ListView 到 RecyclerView,仿今日头条 HeadLine 项目全解析
android
呦呼4575 小时前
Android 仿今日头条项目分析
android
Android系统攻城狮6 小时前
Android tinyalsa深度解析之pcm_params_set_max调用流程与实战(一百七十)
android·pcm·tinyalsa·android音频进阶
怀化纱厂球迷6 小时前
android车载应用动画-仿窗帘式下拉显示!Android 实现跟手裁剪动画 + RecyclerView 列表展示
android·java