本内容主要介绍如何使用 Android 架构组件中的 Data Binding

  Android DataBinding 的官方介绍文档:https://developer.android.com/topic/libraries/data-binding/

一、简介

  Data Binding 库是一个支持库,它允许您在 App 中通过声明方式(而不是编程方式)将布局中的 UI 组件和数据源进行绑定。

  Data Binding 库具备灵活性和兼容性,可以在 Android 4.0(API 14)以及更高版本中使用

  在 Android Gradle 插件的 1.5.0 以及更高版本中支持 Data Binding 库,不过推荐使用最新版本。

1.1 构建环境

  为了在您的 App 中使用 Data Binding,需要在您的 App 模块的 build.gradle 文件中添加 dataBinding 元素,如下所示:

android {    ...    dataBinding {        enabled = true    }}

注意:即使您 App 模块没有直接使用 Data Binding,但是如果它的依赖库使用了 Data Binding,那么也需要像上面一样配置 dataBinding 元素。


1.2 Binding 类的新 Data Binding 编译器

  在 Android Gradle 插件的 3.1.0-alpha06 版本中包含了一个生成绑定类的新 Data Binding 编译器。新的编译器逐步创建绑定类,这在大多数情况下加快了构建过程。

  以前版本的 Data Binding 编译器在编译代码时生成绑定类。如果您的代码编译失败,您可能会收到多个报告绑定类无法找到的错误。新的 Data Binding 编译器将在编译代码之前生成绑定类,来避免这些错误。

  要启用新的 Data Binding 编译器,请在 gradle.properties 文件中添加以下选项:

android.databinding.enableV2=true

  你还可以通过在 gradle 命令中添加以下参数来启用新的编译器:

-Pandroid.databinding.enableV2=true

注意:Android Gradle 插件版本 3.1 中的 Data Binding 编译器不向后兼容。。然而,Android Gradle 3.2 版本中的新编译器与先前版本生成的绑定类兼容。在 3.2 版本中,默认启用新编译器。


二、Layout 和 Binding 表达式

  表达式语言允许您编写表达式,用于处理 View 分发的事件。Data Binding 库将自动产生一个类,用于将布局中的 View 与数据对象绑定。

  Data binding 布局文件略有不同,最外面是 layout 的根标记,里面包含一个 data 元素和一个 view 根元素。这个 view 元素实际上就是非 Data binding 布局文件的根元素。下面的代码显示了一个示例布局文件:

<?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.firstName}"/>       <TextView android:layout_width="wrap_content"           android:layout_height="wrap_content"           android:text="@{user.lastName}"/>   LinearLayout>layout>

  在 data 元素中定义了 user 变量,在布局中可以使用它。

  布局中的表达式使用 "@{}" 语法写入属性中。上面的 TextView 的 text 被设置为 user 变量的 firstName 属性。

注意:布局表达式应该小而简单,因为它们不能进行单元测试,并且 IDE 对其支持有限。您可以使用自定义的 Binding Adapter 来简化布局表达式。


2.1 数据对象

  假设你有一个 POJO(Plain Old Java Object) 来描述 User 实体:

public class User {    public final String firstName;    public final String lastName;    public User(String firstName, String lastName) {        this.firstName = firstName;        this.lastName = lastName;    }}

  这类对象持有的数据永不改变。也可以使用遵循一组约定的对象,如下面的示例所示:

public class User {    private final String firstName;    private final String lastName;    public User(String firstName, String lastName) {        this.firstName = firstName;        this.lastName = lastName;    }    public String getFirstName() {        return this.firstName;    }    public String getLastName() {        return this.lastName;    }}

  从 Data Binding 的角度来看,这两个类是等价的。用于 android:text 属性的表达式 @{user.firstName} 访问第一个类的 firstName 字段和第二个类的 getFirstName() (用 firstName() 替换也可以)方法。

2.2 绑定数据 (与“生成的绑定类”重复)

  为每个布局文件生成一个 Binding 类。默认情况下,基于布局文件的名称给这个 Binding 类命名,采用“帕斯卡命名法”(即大驼峰命名法)并添加 Binding 后缀。比如,布局文件的名称为 activity_main.xml,则对应的 Binding 类的名称为 ActivityMainBinding。这个 Binding 类拥有从布局属性(例如 user 变量)到布局中的 View 的所有 Binding,并知道如何为绑定表达式赋值。推荐在 inflate 布局时创建 Binding,如下面的代码所示:

@Overrideprotected void onCreate(Bundle savedInstanceState) {    super.onCreate(savedInstanceState);    ActivityMainBinding binding            = DataBindingUtil.setContentView(this, R.layout.activity_main);    User user = new User("Test", "User");    binding.setUser(user);}

  你也可以使用 LayoutInflater 获取 View,如下面的代码所示:

ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());

  如果在 Fragment、ListView 或 RecyclerView Adapter 中使用 Data Binding,您可以使用 Binding 类的 inflate() 方法或 DataBindingUtil 类。如下面的代码所示:

ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);// orListItemBinding binding = DataBindingUtil.inflate(        layoutInflater, R.layout.list_item, viewGroup, false);

2.3 表达式语言

2.3.1 共同特征

  在表达式语言中,您可以使用如下运算符和关键字:

  • 数学的 + - / * %
  • 字符串连接 +
  • 逻辑 && ||
  • 二进制 & | ^
  • 一元 + - ! ~
  • 移动 >> >>> <<
  • 比较 == > < >= <=(注意:< 需要转义为 <
  • instanceof
  • 分组 ()
  • 文字 - 字符、字符串、数字、null
  • Cast
  • 方法调用
  • 字段访问
  • 数组访问 []
  • 三元操作符 ?:

例子:

android:text="@{String.valueOf(index + 1)}"android:visibility="@{age > 13 ? View.GONE : View.VISIBLE}"android:transitionName='@{"image_" + id}'
2.3.2 不支持的操作(Missing operations)

  在表达式语法中,不能使用以下操作:

  • this
  • super
  • new
  • 显式通用调用
2.3.3 空合并运算符(Null coalescing operator)

  以下两种方法等效:

android:text="@{user.displayName ?? user.lastName}"
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
2.3.4 属性引用

  表达式可以按照以下格式引用类的属性,引用方法对字段、getter 和 ObservableField 对象都是相同。

android:text="@{user.lastName}"
2.3.5 避免空指针异常

  生成的 Data Binding 代码会自动检查 null 值,从而避免空指针异常。例如,在表达式 @{user.name} 中,如果 usernulluser.name 将被赋为默认的 null。如果您引用 user.age,因为 ageint 类型,那它的默认值为 0

2.3.6 集合

  为了方便,可以使用 [] 运算符访问常用集合(比如 array、list、稀疏 list 和 map)。

<data>    <import type="android.util.SparseArray"/>    <import type="java.util.Map"/>    <import type="java.util.List"/>    <variable name="list" type="List<String>"/>    <variable name="sparse" type="SparseArray<String>"/>    <variable name="map" type="Map<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]}"

注意:需要转义 < 字符,来确保 XML 语法正确。所以需要使用 List<String> 来代替 List

  您还可以使用 object.key 表示法引用 map 中的值。例如,可以使用 @{map.key} 代替 map[key]

2.3.7 字符串文字(String literals)

  您可以使用单引号括起属性值,在表达式中使用双引号,如下面代码所示:

android:text='@{map["firstName"]}'

  您也可以使用双引号括起属性值,在表达式中使用反引号(`),注意不是单引号:

android:text="@{map[`firstName`]}"
2.3.8 资源

  在表达式中,您可以使用如下语法访问资源:

android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"

  格式字符串和复数可以通过参数进行计算:

android:text="@{@string/nameFormat(firstName, lastName)}"android:text="@{@plurals/banana(bananaCount)}"

  当复数有多个参数时,应传递所有参数:

android:text="@{@plurals/orange(orangeCount, orangeCount)}"

  有些资源需要指定显示类型,如下表所示:

Type Normal reference Expression reference
String[] @array @stringArray
int[] @array @intArray
TypedArray @array @typedArray
Animator @animator @animator
StateListAnimator @animator @stateListAnimator
color int @color @color
ColorStateList @color @colorStateList

2.4 事件处理

  Data Binding 允许您编写表达式来处理 View 分发的事件(例如,onClick() 方法)。事件的属性名由监听器(Listener)方法的名称确定,不过也存在一些例外。例如,View.OnClickListener 有一个 onClick() 方法,因此这个事件的属性是 android:onClick

  对于点击事件,有一些特殊的事件处理程序需要一个不同于 android:onClick 的属性来避免冲突。您可以使用以下属性来避免这些类型的冲突:

监听器设置者(Listener setter) 属性
SearchView setOnSearchClickListener(View.OnClickListener) android:onSearchClick
ZoomControls setOnZoomInClickListener(View.OnClickListener) android:onZoomIn
ZoomControls setOnZoomOutClickListener(View.OnClickListener) android:onZoomOut

  您可以使用以下机制来处理事件:

  • 方法引用:在表达式中,您可以引用符合监听器方法签名的方法。当表达式的值为方法引用时,Data Binding 将方法引用和所有者对象包装在监听器中,并在目标 View 上设置该监听器。如果表达式的值为 null,Data Binding 不会创建监听器,并将目标 View 的监听器设置为 null
  • Listener 绑定:即 lambda 表达式,在事件发生时进行计算。Data Binding 总是会创建一个监听器,并将它设置给 View。当分发事件时,监听器计算 lambda 表达式。
2.4.1 方法引用

  事件可以直接绑定到处理方法,类似于可以将 android:onClick 分配给 Activity 中的一个方法。绑定表达式是在编译时处理,如果该方法不存在或者其签名不正确,则会出现编译错误。

  在方法应用中,当数据被绑定时会创建实际的监听器实现。

  若要将事件分配给它的处理程序,请使用普通的绑定表达式,该表达式的值就是要调用的方法名。例如:

public class MyHandlers {    public void onClickFriend(View view) { ... }}

  绑定表达式可以将 View 的点击监听器分配给 onClickFriend() 方法,如下所示:

<?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.firstName}"           android:onClick="@{handlers::onClickFriend}"/>   LinearLayout>layout>

注意:表达式中方法的签名必须与监听器对象中方法的签名完全匹配。即方法的参数必须与事件监听器的参数匹配,并且返回相同的类型(除非返回的是 void)。

2.4.2 监听器绑定

  监听器绑定是在事件发生时运行的绑定表达式。它们与方法引用类似,但是它们允许您运行任意的数据绑定表达式。这个特性在 Android Gradle 插件的 2.0 以及更高版本中支持。

  在方法引用中,方法的参数必须与事件监听器的参数匹配。在监听器绑定中,只要您的返回值必须与监听器的返回值匹配(除非返回的是 void)。例如,考虑下面的拥有 onSaveClick() 方法的 presenter 类:

public class Presenter {    public void onSaveClick(Task task){}}

  然后,可以将点击事件绑定到 onSaveClick() 方法,如下所示:

<?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)}" />    LinearLayout>layout>

  当在表达式中使用回调时,Data Binding 会自动创建必要的监听器并为事件注册它。当 View 触发事件时,Data Binding 会计算给定的表达式。与常规绑定表达式一样,在计算监听器表达式时,将得到 null,并且是线程安全的。

  在上面的例子中,我们没有定义传递给 onClick(View)view 参数。监听器绑定为监听器参数提供了两种选择:忽略方法的所有参数,或命名所有参数。如果您想命名参数,可以在表达式中使用它们。例如,上面的表达式可以写成下面的形式:

android:onClick="@{(view) -> presenter.onSaveClick(task)}"

  或者,如果您想在表达式中使用参数,则可以按照下面的方式工作:

public class Presenter {    public void onSaveClick(View view, Task task){}}
android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}"

  您可以使用带有多个参数的 lambda 表达式:

public class Presenter {    public void onCompletedChanged(Task task, boolean completed){}}
<CheckBox android:layout_width="wrap_content"      android:layout_height="wrap_content"      android:onCheckedChanged="@{(cb, isChecked) -> presenter.completeChanged(task, isChecked)}" />

  如果您正在监听的事件返回的值的类型不是 void,那么您的表达式也必须返回相同类型的值。例如,如果您想监听长按事件,那么表达式应返回布尔值。

public class Presenter {    public boolean onLongClick(View view, Task task) { }}
android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"

  如果由于 null 对象而无法计算表达式,那么 Data Binding 将返回该类型的默认值。例如,引用类型为 nullint0booleanfalse 等等。

  如果需要使用带判断的表达式(例如,三元表达式),您可以将 void 作为符号使用。

android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"

  监听器表达式功能非常强大,并且可以使您的代码易于阅读。另一方面,包含复杂表达式的监听器会使您的布局难以阅读和维护。这些表达式应该像将可用数据从 UI 传递到回调方法一样简单。您应该在回调方法中实现业务逻辑。

2.5 Import、Variable 和 Include

  Data Binding 库提供 Import、Variable 和 Include 等特性。Import 使得很容易在布局文件中引用类。Variable 允许您描述可用于 Binding 表达式的属性。Include 让您在整个 App 中重用复杂布局。

2.5.1 Import
2.5.1.1 如何导入类

  Import 允许您在布局文件中轻松引用类,就在代码中一样。在 data 元素中可以包含 0 个或多个 import 元素。下面的示例将 View 类导入布局文件:

<data>    <import type="android.view.View"/>data>
2.5.1.2 类型别名

  当存在类名冲突时,可以为一个类设置别名。下面的示例为 com.example.real.estate.View 设置别名 Vista

<import type="android.view.View"/><import type="com.example.real.estate.View"        alias="Vista"/>

  您可以使用 Vista 引用 com.example.real.estate.View

2.5.1.3 使用导入的类
  • 用作变量和表达式的类型引用

    下面的示例展示了将 UserList 用作变量的类型:

    <data>    <import type="com.example.User"/>    <import type="java.util.List"/>    <variable name="user" type="User"/>    <variable name="userList" type="List<View>"/>data>
  • 进行类型强制转换

    下面的示例将 connnection 属性强制转换为 User 类型:

    <TextView   android:text="@{((User)(user.connection)).lastName}"   android:layout_width="wrap_content"   android:layout_height="wrap_content"/>
  • 引用导入类的 static 字段和方法

    下面的示例如何引用 View 类的 VISIBLEGONE 常量:

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

    下面的示例引用了 MyStringUtils 类的 capitalize 方法:

    <data>    <import type="com.example.MyStringUtils"/>    <variable name="user" type="com.example.User"/>data><TextView   android:text="@{MyStringUtils.capitalize(user.lastName)}"   android:layout_width="wrap_content"   android:layout_height="wrap_content"/>
2.5.2 Variable(变量)

  可以在 data 元素中添加多个 variable 元素。每个 variable 元素描述一个可以在布局上设置的属性,可以在 Binding 表达式中使用这个 variable。下面的示例描述了 userimagenote 变量:

<data>    <import type="android.graphics.drawable.Drawable"/>    <variable name="user" type="com.example.User"/>    <variable name="image" type="Drawable"/>    <variable name="note" type="String"/>data>

  在编译时检查变量类型,因此如果一个变量实现了 Observable 接口或者是一个可观察集合,则应该反映在类型中。如果变量是没有实现 Observable 接口的基类和接口,则不会观察这个变量。

  当各种配置(例如,横屏和竖屏)使用不同布局文件时,Variable 就会被组合起来。因此在这些布局文件中不能出现变量命名冲突。

  在生成的 Binding 类中,每个 Variable 都有 setter 和 getter 方法。在调用 setter 方法前,变量拥有默认值(例如,引用类型为 nullint 类型为 0boolean 类型为 false)。

  将会自动生成一个名为 context 的特殊 Variable。这个 context 的值是通过根 View 的 getContext() 方法获取。如果显示声明了一个名为 context 的 Variable,将会覆盖自动生成的 context 变量。

2.5.3 Include

  可以将 Variable 传入 Include 的布局文件中。例如:

<?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="testUser" type="com.example.User"/>   data>   <LinearLayout       android:orientation="vertical"       android:layout_width="match_parent"       android:layout_height="match_parent">       <include layout="@layout/name"           bind:user="@{testUser}"/>       <include layout="@layout/contact"           bind:user="@{testUser}"/>   LinearLayout>layout>

  使用 bind:user="@{testUser}" 可以将变量 testUser 传递给 name.xmlcontact.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>       <include layout="@layout/name"           bind:user="@{user}"/>       <include layout="@layout/contact"           bind:user="@{user}"/>   merge>layout>

三、使用可观察的数据对象

  可观察性是指一个对象在其数据发生变化时通知其他对象的能力。Data Binding 库允许您使对象(Object)、字段(Field)或集合(Collection)可观察。

  任何 plain-old 对象(POJO)都可用于 Data Binding,但是修改对象不会自动更新 UI。Data Binding 能够让数据对象拥有当数据发生变化时通知其他对象的能力,即监听器(Listener)。有三种不同类型的可观察类:对象、字段和集合。

  当可观察数据对象被绑定到 UI 后,如果这个数据对象的属性发生变化,UI 将自动更新。

3.1 可观察字段

  提供泛型 ObservableField 类和一些使字段可观察的原始具体类:ObservableBooleanObservableByteObservableCharObservableShortObservableIntObservableLongObservableFloatObservableDoubleObservableParcelable

  可观察字段是具有单个字段的自包含可观察对象。原始的版本在访问期间避免装箱和解箱。要使用此机制,请在 Java 编程语言中创建一个 public final 属性,如下面的例子所示:

private static class User {    public final ObservableField<String> firstName = new ObservableField<>();    public final ObservableField<String> lastName = new ObservableField<>();    public final ObservableInt age = new ObservableInt();}

  使用 get()get() 方法访问字段值,如下所示:

user.firstName.set("Google");int age = user.age.get();

注意:在 Android Studio 3.1 以及更高版本中,可以使用 LiveData 对象替换可观察字段。


3.2 可观察集合

  一些 App 使用动态结构来保存数据。可观察集合允许访问使用 Key 的这类结构。

3.2.1 ObservableArrayMap

  当 Key 是引用类型(例如,String)时,可以使用 ObservableArrayMap。如下面的例子所示:

ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();user.put("firstName", "Google");user.put("lastName", "Inc.");user.put("age", 17);

  在布局文件中,可以使用 String Key 找到 Map,如下所示:

<data>    <import type="android.databinding.ObservableMap"/>    <variable name="user" type="ObservableMap"/>data><TextView    android:text="@{user.lastName}"    android:layout_width="wrap_content"    android:layout_height="wrap_content"/><TextView    android:text="@{String.valueOf(1 + (Integer)user.age)}"    android:layout_width="wrap_content"    android:layout_height="wrap_content"/>
3.2.2 ObservableArrayList

  当 Key 是整数时,可以使用 ObservableArrayMap。如下所示:

ObservableArrayList<Object> user = new ObservableArrayList<>();user.add("Google");user.add("Inc.");user.add(17);

  在布局中,可以通过索引访问列表,如下所示:

<data>    <import type="android.databinding.ObservableList"/>    <import type="com.example.my.app.Fields"/>    <variable name="user" type="ObservableList"/>data><TextView    android:text='@{user[Fields.LAST_NAME]}'    android:layout_width="wrap_content"    android:layout_height="wrap_content"/><TextView    android:text='@{String.valueOf(1 + (Integer)user[Fields.AGE])}'    android:layout_width="wrap_content"    android:layout_height="wrap_content"/>   

3.3 可观察对象

  实现 Observable 接口的类允许注册监听器,该监听器希望当可观察对象的属性变更时得到通知。

  该 Observable 接口具备添加和移除监听器的机制,但您必须决定何时发送通知。为了简化开发,Data Binding 库提供了 BaseObservable 类,该类实现了监听器注册机制。实现 BaseObservable 的数据类负责在属性发生变化时发出通知。通过给 getter 方法添加一个 Bindable 注解和在 setter 方法中调用 notifyPropertyChanged() 来实现,如下面的例子所示:

private static class User extends BaseObservable {    private String firstName;    private String lastName;    @Bindable    public String getFirstName() {        return this.firstName;    }    @Bindable    public String getLastName() {        return this.lastName;    }    public void setFirstName(String firstName) {        this.firstName = firstName;        notifyPropertyChanged(BR.firstName);    }    public void setLastName(String lastName) {        this.lastName = lastName;        notifyPropertyChanged(BR.lastName);    }}

  Data Binding 在模块(module)包中产生一个名为 BR 的类,该类包含了用于数据绑定的资源 ID。在编译期间,Bindable 注解会在 BR 类中会产生一个实体。

  如果数据类的基类不能改变(即不能继承 BaseObservable),可以使用一个 PropertyChangeRegistry 对象进行注册和通知监听器,从而实现 Observable 接口。

3.4 如何使用

  如果想监听一个可观察的数据对象的数据变更,需要手动调用addOnPropertyChangedCallback()removeOnPropertyChangedCallback() 。注意:如果布局文件中使用了可观察的数据对象,Data Binding 库会生成调用这两个函数的代码。

四、生成的绑定类

  Data Binding 库会生成绑定(Binding)类,用于访问布局的变量(Variable)和 View。

  生成的绑定类将布局中的变量和 View 链接起来。所有生成的绑定类都继承至 ViewDataBinding 类。这个绑定类拥有从布局属性(例如 user 变量)到布局中的 View 的所有绑定,并知道如何为绑定表达式赋值。

  为每个布局文件生成一个绑定类。默认情况下,基于布局文件的名称给这个绑定类命名,采用“帕斯卡命名法”(即大驼峰命名法)并添加 Binding 后缀。该类放在 module 包的 databinding 包中。例如,布局文件 contact_item.xml 生成 ContactItemBinding 类。如果 module 包是 com.example.my.app,则这个绑定类放在 com.example.my.app.databinding 包中。

4.1 自定义绑定类名称

  可以通过调整 data 元素的 class 属性,来重命名绑定类或放在不同的包中。例如,下面的布局在当前 module 的 databinding 包中生成 ContactItem 绑定类:

<data class="ContactItem">data>

  您可以在不同的包中生成绑定类,方法是在类名前加一个句点(.)。以下的例子在 module 包中生成 ContactItem 绑定类:

<data class=".ContactItem">data>

  以下例子在 com.example 包中生成 ContactItem 绑定类:

<data class="com.example.ContactItem">data>

4.2 创建绑定对象

  绑定对象应该在 inflate 布局后马上创建,。将对象绑定到布局的最常见方法是使用绑定类的 static 方法。您可以使用绑定类的 inflate() 方法 inflate view 层次并将对象绑定到此视图层次,如以下示例所示:

@Overrideprotected void onCreate(Bundle savedInstanceState) {    super.onCreate(savedInstanceState);    MyLayoutBinding binding = MyLayoutBinding.inflate(getLayoutInflater());}

  inflate() 方法还可以接受一个 ViewGroup 对象,如以下示例所示:

MyLayoutBinding binding = MyLayoutBinding.inflate(        getLayoutInflater(), viewGroup, false);

  如果使用不同的机制 inflate 布局,则可以将其单独绑定,如下所示:

MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);

  有时无法提前知道绑定类型。在这种情况下,可以使用 DataBindingUtil 类创建这个绑定,如下所示:

View viewRoot = LayoutInflater.from(this).inflate(layoutId, parent, attachToParent);ViewDataBinding binding = DataBindingUtil.bind(viewRoot);

  如果您在 FragmentListViewRecyclerView 适配器中使用数据绑定项,您可能更喜欢使用绑定类的 inflate() 方法或 DataBindingUtil 类,如下所示:

ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);// orListItemBinding binding = DataBindingUtil.inflate(        layoutInflater, R.layout.list_item, viewGroup, false);

4.3 带 ID 的 View

  Data Binding 库在 Binding 类中为布局文件中拥有 ID 的每个 View 创建一个不可变字段。例如,Data Binding 库将为以下布局创建 TextView 类型的 firstNamelastName 字段:

<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.firstName}"           android:id="@+id/firstName"/>       <TextView android:layout_width="wrap_content"           android:layout_height="wrap_content"           android:text="@{user.lastName}"           android:id="@+id/lastName"/>   LinearLayout>layout>

  该库在一次传递中从视图层次结构中提取有 ID 的 View。当获取布局文件中的每个 View 时,此机制比使用 findViewById() 更快。

4.4 ViewStub

  与普通 View 不同,ViewStub 对象最开始是一个不可见的 View。当被设置为可见或者被显式调用 inflate () 后,它们会通过 inflate 其他布局来替换自己。

  布局文件中的 ViewStub,在对应的 Binding 类中生成对应的 ViewStubProxy 对象,而不是 ViewStub 对象。当 ViewStub 存在时,可以使用 ViewStubProxy 对象访问;当 ViewStub 已经 inflate 后,可以使用 ViewStubProxy 对象访问 inflate 的 View 层次结构。

  当 inflate 另一个布局时,必须为新布局建立绑定。因此,ViewStubProxy 必须监听 ViewStubOnInflateListener,并在需要时建立绑定。ViewStubProxy 允许您设置一个 OnInflateListener,但是由于同时只能存在一个 Listener,所以将在建立绑定后调用它(实际上就是先调用 ViewStubProxyOnInflateListener,然后再调用您自己设置的 OnInflateListener)。

4.5 Variable

  Data Binding 库为布局文件中的每个 Variable 生成访问器(accessor)方法。例如,下面的布局在 Binding 类中为 userimagenote 变量生成 setter 和 getter 方法:

<data>   <import type="android.graphics.drawable.Drawable"/>   <variable name="user" type="com.example.User"/>   <variable name="image" type="Drawable"/>   <variable name="note" type="String"/>data>

4.6 立即绑定

  当一个变量或可观察对象发生变化时,绑定将在下一帧前执行。如果我们需要立即执行绑定,可以调用 executePendingBindings(),必须在 UI 线程中调用它。

4.7 高级绑定

4.7.1 动态变量

  有时,不知道具体的绑定类。例如,一个 RecyclerView.Adapter 不知道具体的绑定类。但是,在调用 onBindViewHolder() 方法时仍然需要使用绑定值。

  在以下示例中,RecyclerView 绑定的所有布局中有一个 item 变量。BindingHolder 对象有一个 getBinding() 方法,此方法返回一个 ViewDataBinding 基类。

public void onBindViewHolder(BindingHolder holder, int position) {    final T item = items.get(position);    holder.getBinding().setVariable(BR.item, item);    holder.getBinding().executePendingBindings();}

注意:Data Binding 库在 module 包中生成一个名为 BR 的类,该类包含了用于数据绑定的资源 ID。在上面的示例中,库自动生成这个 BR.item 变量。


4.8 后台线程

  您可以在后台线程中更改数据模型,只要它不是一个集合。数据绑定在计算期间对每个变量/字段进行本地化,以避免任何并发问题。

五、绑定适配器

  绑定适配器(Binding Adapter)负责通过适当的框架调用来设置值。比如通过 setText() 方法设置属性值;通过 setOnClickListener() 方法设置事件 Listener。

  Data Binding 库允许您通过适配器指定设置值的方法(实现您自己的绑定逻辑),以及指定返回对象的类型。

5.1 设置属性值

  当绑定变量值发生变化时,生成的绑定类会调用对应的 setter 方法。您可以让 Data Binding 库自动确定方法,或者显式声明方法,或者通过自定义逻辑选择方法。

5.1.1 自动选择方法

  对于一个 example 属性,Data Binding 库会自动尝试查找参数类型兼容的 setExample(arg) 方法。在搜索这个方式时,不需要考虑属性的命名空间,仅仅只需要考虑属性名和参数类型。

  即使给定的属性名不存在,Data Binding 仍可以工作。您可以通过 Data Binding 为任何 setter 方法创建属性。例如,支持类 DrawerLayout 没有任何属性,但是有很多 setter 方法。下面的布局自动使用 setScrimColor(int)setDrawerListener(DrawerListener) 方法作为属性 app:scrimColorapp:drawerListener 的 setter 方法:(有对应的 setter 方法,但是没有对应的属性,可以给其一个自定义属性

<android.support.v4.widget.DrawerLayout    android:layout_width="wrap_content"    android:layout_height="wrap_content"    app:scrimColor="@{@color/scrim}"    app:drawerListener="@{fragment.drawerListener}">
5.1.2 指定自定义方法名

  某些属性的 setter 方法名称不匹配。在这种情况下,可以使用 BindingMethods 注解将属性与 setter 方法进行关联。这个注解是用于类,可以包含多个 BindingMethod 注解。在下面的示例中,属性 android:tintsetImageTintList(ColorStateList) 方法关联:(有对应的 setter 方法,并且有对应的属性,将它们关联

@BindingMethods({       @BindingMethod(type = "android.widget.ImageView",                      attribute = "android:tint",                      method = "setImageTintList"),})public class TestBindingAdapter {    }

  通常情况下,您不需要为 Android Framework 中的类重命名 setter 方法。

5.1.3 提供自定义逻辑

  某些属性需要自定义绑定逻辑。例如,属性 android:paddingLeft 没有与其相关联的 setter 方法。带有 BindingAdapter 注解的 static 绑定适配器方法允许您自定义如何调用属性的 setter 方法。(有对应的属性,但是没有对应的 setter 方法,为其提供一个自定义方法

(1)单个属性

  例如,以下示例显式了 paddingLeft 属性的绑定适配器:

@BindingAdapter("android:paddingLeft")public static void setPaddingLeft(View view, int padding) {    view.setPadding(padding,            view.getPaddingTop(),            view.getPaddingRight(),            view.getPaddingBottom());}

  第一个参数的类型和与属性关联的 View 类型相同;第二个参数的类型和属性的绑定表达式返回的类型相同。

  自定义的绑定适配器将重载 Android Framework 提供的。

(2)多个属性

  您还可以使用接收多个属性的适配器,如以下示例所示:

@BindingAdapter({"imageUrl", "error"})public static void loadImage(ImageView view, String url, Drawable error) {    Picasso.get().load(url).error(error).into(view);}

  您可以在布局中使用这个适配器,如以下示例所示:(注意:@drawable/venueError 引用 App 中的一个资源。使用 @{} 包围该资源使其成为一个有效的绑定表达式。)

<ImageView app:imageUrl="@{venue.imageUrl}" app:error="@{@drawable/venueError}" />

注意:数据绑定库会忽略自定义命名空间进行匹配。

  如果 imageUrlerror 都用于 ImageView 对象,并且 imageUrl 是字符串和 errorDrawable 时,将调用这个适配器。如果您希望在设置任何一个属性时都调用适配器,可以将这个适配器的 requireAll 标志设置为 false,如以下示例所示:

@BindingAdapter(value={"imageUrl", "placeholder"}, requireAll=false)public static void setImageUrl(ImageView imageView, String url, Drawable placeHolder) {  if (url == null) {    imageView.setImageDrawable(placeholder);  } else {    MyImageLoader.loadInto(imageView, url, placeholder);  }}
(3)方法中使用旧值

  绑定适配器方法可以选择在其处理程序中使用旧值。采用旧值和新值的方法应该首先声明属性的所有旧值,然后声明新值,如以下示例所示:

@BindingAdapter("android:paddingLeft")public static void setPaddingLeft(View view, int oldPadding, int newPadding) {  if (oldPadding != newPadding) {      view.setPadding(newPadding,                      view.getPaddingTop(),                      view.getPaddingRight(),                      view.getPaddingBottom());   }}
(4)事件处理

  事件处理程序只能与带有一个抽象方法的接口或抽象类一起使用,如以下示例所示:

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

  在布局中使用这个事件处理程序,如下所示:

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

  当一个监听器有多个方法时,必须将其拆分为多个监听器。例如,View.OnAttachStateChangeListener 有两个方法:onViewAttachedToWindow(View)onViewDetachedFromWindow(View)。库提供了两个接口来区分属性和处理程序:

@TargetApi(VERSION_CODES.HONEYCOMB_MR1)public interface OnViewDetachedFromWindow {  void onViewDetachedFromWindow(View v);}@TargetApi(VERSION_CODES.HONEYCOMB_MR1)public interface OnViewAttachedToWindow {  void onViewAttachedToWindow(View v);}

  因为更改一个监听器也会影响另一个监听器,所以您需要一个适用于任一属性或适用于两者的适配器。您可以在注解中将 requireAll 设置为 false,以指定不是每个属性都必须分配绑定表达式,如以下示例所示:

@BindingAdapter({"android:onViewDetachedFromWindow",                 "android:onViewAttachedToWindow"}, requireAll=false)public static void setListener(View view, OnViewDetachedFromWindow detach,        OnViewAttachedToWindow attach) {    if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1) {        OnAttachStateChangeListener newListener;        if (detach == null && attach == null) {            newListener = null;        } else {            newListener = new OnAttachStateChangeListener() {                @Override                public void onViewAttachedToWindow(View v) {                    if (attach != null) {                        attach.onViewAttachedToWindow(v);                    }                }                @Override                public void onViewDetachedFromWindow(View v) {                    if (detach != null) {                        detach.onViewDetachedFromWindow(v);                    }                }            };        }        OnAttachStateChangeListener oldListener = ListenerUtil.trackListener(                view, newListener, R.id.onAttachStateChangeListener);        if (oldListener != null) {            view.removeOnAttachStateChangeListener(oldListener);        }        if (newListener != null) {            view.addOnAttachStateChangeListener(newListener);        }    }}

  上面的例子比正常情况稍微复杂一些,因为 View 使用 addOnAttachStateChangeListener()removeOnAttachStateChangeListener() 方法代替 OnAttachStateChangeListener 的 setter 方法。android.databinding.adapters.ListenerUtil 类帮助跟踪以前的监听器,以便在绑定适配器中删除它们。

  通过 @TargetApi(VERSION_CODES.HONEYCOMB_MR1) 注解 OnViewDetachedFromWindowOnViewAttachedToWindow,Data Binding 代码生成器知道仅当在 Android 3.1 (API 12) 以及更高版本上运行时才生成此监听器。

5.2 对象转换

5.2.1 自动对象转换

  当从绑定表达式返回对象时,Data Binding 库会选择用于设置属性值的方法。这个对象将会被强制转换为选择方法的参数类型。当使用 ObservableMap 类存储数据时,这是非常有用的,如下面的例子所示:

<TextView   android:text='@{userMap["lastName"]}'   android:layout_width="wrap_content"   android:layout_height="wrap_content" />

注意:您也可以使用 object.key 表示法引用 map 中的值。例如,可以使用 @{userMap.lastName} 代替 userMap["lastName"]

  在表达式中的 userMap 对象返回一个值,该值将自动转换为 setText(CharSequence) 方法的参数类型,此方法用于设置 android:text 属性的值。如果参数类型不明确,您必须在表达式中强制转换返回类型。

5.2.2 自定义转换

  在某些情况下,需要在特定类型之间进行自定义转换。例如 View 的 android:background 属性需要一个 Drawable,但是指定的 color 值是一个整型。

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

  可以通过一个用 BindingConversion 注解的 static 方法将 int 类型转换为 ColorDrawable 类型:

@BindingConversionpublic static ColorDrawable convertColorToDrawable(int color) {    return new ColorDrawable(color);}

  但是,表达式中的类型必须是一致的,即在同一个表达式中不能使用不同的类型,如下面的例子所示:

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

六、将布局 View 绑定到架构组件

  Data Bingding 库能够与架构组件无缝协作,从而进一步简化 UI 的开发。App 中的布局可以绑定到架构组件中的数据,这些数据帮助您管理 UI 控制器的生命周期并通知数据的变更。

6.1 使用 LiveData 将数据变更通知 UI

  您可以使用 LiveData 对象作为数据绑定源,自动通知 UI 数据发生变更。

  与实现 Observable 的对象(如可观察字段)不同,LiveData 对象了解订阅数据变更观察者的生命周期。在 Android Studio 3.1 及更高版本中,在数据绑定代码中可以使用 LiveData 对象替换可观察字段。

  要想一起使用 LiveData 和 绑定类,需要指定一个生命周期所有者来定义 LiveData 对象的范围。下面的例子在实例化绑定类后将 Activity 指定为声明周期所有者:

class ViewModelActivity extends AppCompatActivity {    @Override    protected void onCreate(Bundle savedInstanceState) {        // Inflate view and obtain an instance of the binding class.        UserBinding binding = DataBindingUtil.setContentView(this, R.layout.user);        // Specify the current activity as the lifecycle owner.        binding.setLifecycleOwner(this);    }}

  您可以使用 ViewModel 组件将数据绑定到布局。在 ViewModel 组件中,可以使用 LiveData 对象转换数据或合并多个数据源。下面的例子展示了如何在 ViewModel 中转换数据:

class ScheduleViewModel extends ViewModel {    LiveData username;    public ScheduleViewModel() {        String result = Repository.userName;        userName = Transformations.map(result, result -> result.value);    }}

6.2 使用 ViewModel 管理 UI 相关的数据

  Data Binding 库能够与 ViewModel 组件无缝协作,ViewModel 组件向外提供数据。 一起使用 Data Binding 库和 ViewModel 组件,可以将 UI 逻辑从布局移动到组件中,这样将使得更易于测试。Data Binding 库确保在需要时将 View 与 数据源进行绑定和解绑。剩下的大部分工作用于确保公开正确的数据。

  一起使用 Data Binding 库和 ViewModel 组件的步骤:

  • 实例化继承至 ViewModel 类的组件。
  • 获取绑定类的实例。
  • ViewModel 组件赋给绑定类的一个属性。
class ViewModelActivity extends AppCompatActivity {    @Override    protected void onCreate(Bundle savedInstanceState) {        // Obtain the ViewModel component.        UserModel userModel                = ViewModelProviders.of(getActivity()).get(UserModel.class);        // Inflate view and obtain an instance of the binding class.        UserBinding binding = DataBindingUtil.setContentView(this, R.layout.user);        // Assign the component to a property in the binding class.        binding.viewmodel = userModel;    }}

  在布局中,使用绑定表达式将 ViewModel 组件的属性和方法分配给相应的 View,如下面的示例所示:

<CheckBox    android:id="@+id/rememberMeCheckBox"    android:checked="@{viewmodel.rememberMe}"    android:onCheckedChanged="@{() -> viewmodel.rememberMeChanged()}" />

6.3 使用可观察的 ViewModel 对绑定适配器进行更好的控制

  您可以使用实现 ObservableViewModel 组件来通知其他 App 组件数据发生变更,类似于使用 LiveData 对象。

  在某些情况下,您可能更喜欢使用实现了 Observable 接口的 ViewModel 组件,而不是 LiveData 对象。使用实现了 ObservableViewModel 组件,可以更好地控制 App 中的绑定适配器。例如,此模式使您在数据发生变更时更好地控制通知,还允许您在双向绑定中指定设置属性值的自定义方法。

  要实现可观察的 ViewModel 组件,必须创建一个继承至 ViewModel 并实现 Observable 接口的类。当观察者使用 addOnPropertyChangedCallback()removeOnPropertyChangedCallback() 订阅或取消订阅通知时,您可以提供自定义逻辑。当属性发生变化时,您也可以在 notifyPropertyChanged() 中提供自定义逻辑。下面的代码展示了如何实现一个可观察的 ViewModel

/** * A ViewModel that is also an Observable, * to be used with the Data Binding Library. */class ObservableViewModel extends ViewModel implements Observable {    private PropertyChangeRegistry callbacks = new PropertyChangeRegistry();    @Override    protected void addOnPropertyChangedCallback(            Observable.OnPropertyChangedCallback callback) {        callbacks.add(callback);    }    @Override    protected void removeOnPropertyChangedCallback(            Observable.OnPropertyChangedCallback callback) {        callbacks.remove(callback);    }    /**     * Notifies observers that all properties of this instance have changed.     */    void notifyChange() {        callbacks.notifyCallbacks(this, 0, null);    }    /**     * Notifies observers that a specific property has changed. The getter for the     * property that changes should be marked with the @Bindable annotation to     * generate a field in the BR class to be used as the fieldId parameter.     *     * @param fieldId The generated BR id for the Bindable field.     */    void notifyPropertyChanged(int fieldId) {        callbacks.notifyCallbacks(this, fieldId, null);    }}

七、双向数据绑定

  使用单向数据绑定,您可以给一个属性设置一个值,并且设置一个监听器来响应此属性的变更。

<CheckBox    android:id="@+id/rememberMeCheckBox"    android:checked="@{viewmodel.rememberMe}"    android:onCheckedChanged="@{viewmodel.rememberMeChanged}" />

  双向绑定提供了一种快捷方式:

<CheckBox    android:id="@+id/rememberMeCheckBox"    android:checked="@={viewmodel.rememberMe}" />

  @={} 符号同时接收属性的数据变更和监听用户更新。

7.1 使用自定义属性的双向绑定

  如果您想使用自定义属性的双向数据绑定,您需要使用 @InverseBindingAdapter@InverseBindingMethod 注解。

  例如,如果您想在名为 MyView 的自定义View 中对 time 属性启用双向数据绑定,请完成以下步骤:

  1. 使用 @BindingAdapter 注解 setter 方法:

    @BindingAdapter("time")public static void setTime(MyView view, Time newValue) {    // Important to break potential infinite loops.    if (view.time != newValue) {        view.time = newValue;    }}
  2. 使用 @InverseBindingAdapter 注解 getter 方法:

    @InverseBindingAdapter("time")public static Time getTime(MyView view) {    return view.getTime();}

  此时,Data Binding 知道当数据变更时要做什么(调用被 @BindingAdapter 注解的方法),以及当 View 属性变更时要做什么(调用被 @InverseBindingAdapter 注解的方法)。但是,它不知道属性何时或如何更变。

  为此,您需要在 View 上设置监听器。它可以是与自定义 View 关联的自定义监听器,也可以是通用事件,例如焦点丢失或文本变更。给一个方法添加 @BindingAdapter 注解,该方法用来给此属性变更设置监听器:

@BindingAdapter("app:timeAttrChanged")public static void setListeners(        MyView view, final InverseBindingListener attrChange) {    // Set a listener for click, focus, touch, etc.}

  该监听器有一个 InverseBindingListener 类型的参数。您可以使用这个 InverseBindingListener 告知 Data Binding 系统此属性发生变更。然后,系统将调用被 @InverseBindingAdapter 注解的方法,等等。

注意:每个双向绑定会生成一个合成事件属性。此属性的名称与基本属性相同,但它会添加后缀“AttrChanged”。合成事件属性允许库创建一个使用 @BindingAdapter 注解的方法,该方法将事件监听器关联到适当的 View 实例。

  在实践中,这个监听器包含一些重要的逻辑,包括用于单向数据绑定的监听器。有关示例,请参阅文本属性变更的适配器 TextViewBindingAdapter

7.2 转换器

  如果绑定到 View 的变量需要在显示之前以某种方式进行格式化,转换或更改,则可以使用 Converter 对象。

  例如,一个显示日期的 EditText 对象:

<EditText    android:id="@+id/birth_date"    android:text="@={Converter.dateToString(viewmodel.birthDate)}" />

  这个 viewmodel.birthDate 属性包含一个 Long 类型的值,因此需要使用转换器对其进行格式化。

  因为使用的是双向表达式,所以还需要一个反转转换器,让库知道如何将用户提供的字符串转换回支持的数据类型(在本例中是 Long)。需要给其中的一个转换器添加 @InverseMethod 注解。

public class Converter {    @InverseMethod("stringToDate")    public static String dateToString(EditText view, long oldValue,            long value) {        // Converts long to String.    }    public static long stringToDate(EditText view, String oldValue,            String value) {        // Converts String to long.    }}

7.3 避免双向绑定的无限循环

  在使用双向绑定时,需要注意不要引入无限循环。当用户更改属性时,将调用被 @InverseBindingAdapter 注解的方法,并将该值分配给对应属性。反过来,这将调用被 @BindingAdapter 注解的方法,这将导致调用被 @InverseBindingAdapter 注解的方法,以此类推。

  因此,需要在被 @BindingAdapter 注解的方法中比较新值和旧值来避免陷入无限循环。

7.4 平台内置的双向绑定属性

  平台已经对下表中的属性提供了双向数据绑定支持。关于平台如何提供这种支持的详细信息,请参阅相应绑定适配器的实现:

属性 绑定适配器
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

八、Observable 与 LiveData 对比

  下面列出 Observable 与 LiveData 的简单对比(如果您不了解 LiveData,可以参阅 这里):

  • LiveData 对象了解订阅数据变更观察者的生命周期。在 Android Studio 3.1 及更高版本中,在数据绑定代码中可以使用 LiveData 对象替换可观察字段。
  • LiveData 是一个 abstract 类,Observable 是一个 interface。我们知道在 Java 中是不支持多重继承的,当一个类已经有一个父类时,就无法再继承 LiveData 了,但是可以实现 Observable。

更多相关文章

  1. Android通过json向MySQL中读写数据的方法详解【读取篇】
  2. android 获得屏幕、视图、任务栏、状态栏的高宽以及屏幕的设置
  3. android 中的Main调试方法
  4. Android(安卓)图片处理之图片叠加--Bitmap
  5. 实现图片浏览,改变透明度的方法
  6. Android(安卓)给图标着色的方法
  7. Android(安卓)Ams浅析
  8. Android之开源框架NineOldAndroids动画库
  9. Custom View

随机推荐

  1. Android输入汉字得到拼音
  2. android控件-ImageView使用方法整理
  3. android实现百度地图点击覆盖物(MyLocati
  4. Android(安卓)9.0静默安装与卸载app
  5. Android 网络框架_常用的网络框架
  6. Android 如何修改APK的默认名称
  7. Android“再按一次退出程序”实现
  8. Android的SharedPreferences的使用介绍
  9. Android(安卓)Studio,gradle project sync
  10. eclipse中加入写好的android工程和出现的