Jetpack-生命周期组件库Databinding

Databinding#

本文主要讲了Databinding以下用法:Activity用法、Fragment用法、Recycleview用法、布局引用、更新字段、事件处理等。

参考Android DataBinding 从入门到进阶 - 掘金 (juejin.cn)

用途#

声明式的将视图与数据绑定,而程序式的调用视图组件属性去设置

如程序式的做法

1
2
TextView textView = findViewById(R.id.sample_text);
textView.setText(viewModel.getUserName());

声明式的做法

1
2
3
4
<TextView
android:text="@{viewmodel.userName}" />

数据绑定类.setViewModel(viewmodel)

常见问题#

问题1:使用数据绑定类还是视图绑定类?

解决:

  • 如果只是取代findViewById,可以使用视图绑定类

问题2:

1
If you don't use an inflation method taking a DataBindingComponent, use DataBindingUtil.setDefaultComponent or make all BindingAdapter methods static.

解决:

@BindingAdapter注解声明的方法必须是静态的

问题3:databinding能在子线程直接设置对象的值吗?

写demo验证:起子线程调用databinding设置变量值

button点击事件代码如下,经过验证,每点一下,界面是可以实时刷新的。

1
2
3
4
5
6
7
8
9
10
11
12
public void baseObservableInThread(View view) {
new Thread(new Runnable() {
@Override
public void run() {
Log.e("DatabindingActivity", "accept: ");
// 更新指定字段
user.setUserName("jordan" + 1 + "号");
user.setPassWord("jordan-love-" + 1);
}
}).start();
}

官方文档专门描述了:异步设置数据的前提是,数据源不能是集合。——《生成的绑定类-后台线程

问题4:能替代handler通知主线程更新ui的作用吗?

答:确实不需要handler通知主线程刷新UI了。

问题5:那还需要handler吗?

答:handler仍然可用于多线程通信

问题6:databinding可观察对象发生更改时,界面是在当前帧显示变化还是下一帧显示变化?

答:当可变或可观察对象发生更改时,绑定会按照计划在下一帧之前发生更改。但有时必须立即执行绑定。要强制执行,请使用 executePendingBindings() 方法。

问题7:双向绑定可能会引入无限循环

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

解决:通过比较使用 @BindingAdapter 注释的方法中的新值和旧值,可以打破可能出现的无限循环。

几个重要的类#

DataBindingUtil

  • 为Activity、Fragment、Recyclerview设置布局

ViewDataBinding

  • view级别的DataBinding

优势#

  • 维护简单:不用写视图组件调用findViewById

  • 减少内存泄漏

  • 避免Null指针异常,如果user.name值为空,会默认分配null值

    android:text="@{exampleText.text}"text为空,实际效果为android:text="null"

  • 支持viewstub

  • 布局嵌套:A.xml通过include标签包含了一个B.xml,A可以通过命名 空间的形式将变量传递给B布局

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <?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"
    bind:user="@{user}"/>
    <include layout="@layout/contact"
    bind:user="@{user}"/>
    </LinearLayout>
    </layout>

    缺点是不支持merge

数据类型#

DataBindingUtil

基本用法#

常见API#

DataBindingUtil类 用途
inflate 返回一个数据绑定类对象
setContentView 为组件设置视图;返回一个数据绑定类对象
bind
getBinding 返回一个数据绑定类对象

开启数据绑定类#

需要几个前置条件

  • Android 4.0(API 级别 14)及以上

  • gralde插件版本大于1.5

1
2
3
4
5
6
7
android {
...
dataBinding {
enabled = true
}
}

初始化数据绑定类#

如何根据xml文件,初始化生成对应的数据绑定类呢?答案是有2种初始化方式

第一种直接使用list_item.xml自动生成的ListItemBinding绑定类,该类的静态方法

第二种使用DataBindingUtil.inflate将list_item.xml布局初始化为ListItemBinding绑定类

1
2
3
4
5
6
7
8
// 方式1 item_test_test.xml文件自动生成ItemTestTEstBinding.java
ItemTestTestBinding itemTestTestBinding=ItemTestTestBinding.inflate(getLayoutInflater());
// list_item.xml文件自动生成ListItemBinding.java
ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);


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

重命名绑定类名称#

默认情况下,数据绑定类的名称由布局文件名称生成,生成步骤为

  1. 大写字母开头
  2. 移除下划线
  3. 每个词汇字母开头大小

可以调整数据绑定类的名称,修改方式为在data标签里定义class属性

1
2
3
4
<data class="ContactItem">

</data>

Activity入门#

第一步编写xml

新建一个xml,鼠标放在根布局标签上按住ALt+回车键,可以看到Conver to data binding layout按钮,点击后自动将当前布局转换为数据绑定类布局文件

image-20220707181150815

下面是转换后的布局文件

1
2
3
4
5
6
7
8
9
10
11
   <data>
<import type="com.leavesc.databinding_demo.model.User" />
<variable
name="userInfo"
type="User" />
</data>

<TextView
android:id="@+id/tv_userName"
···
android:text="@{userInfo.name}" />

第二步activity绑定xml

1
2
3
setContentView(R.layout.activity_databind);
ActivityDatabindBinding activityDatabindBinding =
DataBindingUtil.setContentView(this, R.layout.activity_databind);

第三步为xml赋值

1
2
User user = new User();
activityDatabindBinding.setUser(user);

第四步更新user类,观察xml是否改变

1
2
user.setUserName("jordan" + aLong + "号");
user.setPassWord("jordan-love-" + aLong);

观察界面是否变化

Recycleview入门#

第一步:编写Recycleview.Adapter

1.1 编写ViewHolder,增加ViewDataBinding相关接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MyViewHolder extends RecyclerView.ViewHolder {
private ViewDataBinding binding;

public MyViewHolder(ViewDataBinding binding) {
super(binding.getRoot());
this.binding = binding;
}

public ViewDataBinding getBinding() {
return binding;
}

public void setBinding(ViewDataBinding binding) {
this.binding = binding;
}
}

1.2 重写onCreateViewHolder,调用Databinding相关类,初始化ViewDatabinding,初始化ViewHolder,返回给调用者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
  @NonNull
@Override
public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
ViewDataBinding binding =
DataBindingUtil.inflate(
LayoutInflater.from(parent.getContext()),
R.layout.item_in_databind_rv, parent, false);
MyViewHolder holder = new MyViewHolder(binding);
return holder;
}
在这一步也要编写子项的xml布局,用databind写法


​```xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="user4"
type="com.shaunsheep.mvvm.dataBinding.UserObservable" />
</data>

<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<com.google.android.flexbox.FlexboxLayout
app:flexWrap="wrap"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{String.valueOf(user4.id)}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user4.userName}" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user4.passWord}" />
</com.google.android.flexbox.FlexboxLayout>
</RelativeLayout>
</layout>

1.3 重写onBindVIewHolder,将数据集,通过databind#setVariable塞给子项item

1
2
3
4
5
6
@Override
public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
holder.getBinding().setVariable(com.shaunsheep.mvvm.BR.user4,userObservableArrayList.get(position));
holder.getBinding().executePendingBindings();
}

1.4 完成俩其他必备的方法

1
2
3
4
5
6
7
8
@Override
public int getItemCount() {
return userObservableArrayList.size();
}

public void initData(ArrayList<UserObservable> userObservableArrayList){
this.userObservableArrayList = userObservableArrayList;
}

默认值#

1
android:text="@{userInfo.name,default=defaultValue}"

动态设置指定ID组件#

1
activityMain2Binding.tvUserName.setText("leavesC");

布局引用#

什么是布局引用?

xml的一个组件里,直接引用另外一个组件的属性,比如下面的例子,editextided_test_text;我们想做的是编辑该组件时实时刷新TextView,在数据绑定类提供的功能里,我们可以直接驼峰式写出组件id的名称,名称后跟上其可用的属性.

就像下面的例子ed_test_text的写法为edTestText,布局引用的写法为@{edTestText.text}

1
2
3
4
5
6
7
8
<EditText
android:id="@+id/ed_test_text"
android:layout_width="200dp"
android:layout_height="30dp" />
<TextView
android:layout_width="100dp"
android:layout_height="wrap_content"
android:text="@{edTestText.text}" />

BaseObservable更新指定字段#

方式1:先继承BaseObservable类,采用notifyPropertyChanged的方式,传入BR值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
build\generated\source\apt\debug\com\shaunsheep\mvvm\databinding

public class User extends BaseObservable {
/**
* 公有属性可以只加@bindable
*/
@Bindable
public long id;
/**
* 私有属性只能在public#get方法上加@bindable
*/
private String userName;
private String passWord;
@Bindable
public String getUserName() {
return userName;
}

public long getId() {
return id;
}

public void setId(long id) {
this.id = id;
// 更新全量字段
notifyChange();
}
public void setNameAndPassword(String userName,String passWord,long id){
this.id = id;
this.userName = userName;
this.passWord = passWord;
notifyChange();
}
public void setUserName(String userName) {
this.userName = userName;
// 更新指定字段
notifyPropertyChanged(com.shaunsheep.mvvm.BR.userName);
}
@Bindable
public String getPassWord() {
return passWord;
}

public void setPassWord(String passWord) {
this.passWord = passWord;
}


}

方式2:ObservableField更新

官方原生提供了对基本数据类型的封装,例如 ObservableBoolean、ObservableByte、ObservableChar、ObservableShort、ObservableInt、ObservableLong、ObservableFloat、ObservableDouble 以及 ObservableParcelable ,也可通过 ObservableField 泛型来申明其他类型

1
2
3
4
5
6
7
8
9
public class UserObservable {

ObservableField<String> userName;
ObservableFloat id;
public UserObservable(String userName,float id){
this.userName = new ObservableField<>(userName);
this.id = new ObservableFloat(id);
}
}

ObservableField更新指定数据#

定义:指可观察对象将其数据变化告知其他对象

用途:当其中一个可观察数据对象绑定到界面并且该数据对象的属性发生更改时,界面会自动更新

用法:使用ObservableField封装指定数据,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* 属性改变会立即刷新
*/
public class UserObservable {
ObservableField<String> passWord;
ObservableField<String> userName;
ObservableFloat id;
public UserObservable(String userName,String password,float id){
this.userName = new ObservableField<>(userName);
this.id = new ObservableFloat(id);
this.passWord = new ObservableField<>(password);
}

public ObservableField<String> getPassWord() {
return passWord;
}

public void setPassWord(ObservableField<String> passWord) {
this.passWord = passWord;
}

public ObservableField<String> getUserName() {
return userName;
}

public void setUserName(ObservableField<String> userName) {
this.userName = userName;
}

public ObservableFloat getId() {
return id;
}

public void setId(ObservableFloat id) {
this.id = id;
}
}

xml中绑定该数据,先在data标签中声明variable变量user2,后再视图组件中使用user2变量,将该变量绑定到xml中。

1
2
3
4
5
6
7
      <variable
name="user2"
type="com.shaunsheep.mvvm.dataBinding.UserObservable" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user2.passWord}" />

这样,当我们调用数据绑定类设置user2的值时,

1
2
UserObservable userObservable = new UserObservable(";", ";", 0L);   
activityDatabindBinding.setUser2(userObservable);

BaseObservable更新全量字段#

调用notifyChange

1
2
3
4
5
6
7
8
9
build\generated\source\apt\debug\com\shaunsheep\mvvm\databinding

private String passWord;
public void setId(long id) {
this.id = id;
// 更新全量字段
notifyChange();
}

data标签支持的标签#

支持的属性
import type
variable name、type、alis

转义字符#

属性名称 &lt;代替<
List<String> List&lt;String>
SparseArray SparseArray&lt;String>
Map<String,String> Map&lt;String, String>
int
String

可用表达式#

写法 解释
android:text=’@{“”}’ 空串-外侧单引号,内侧双引号
android:text=”@{``}” 空串-外侧爽引号,内侧反单引号
@{goods.details} 字符串赋值
@{String.valueOf(goods.price)} 类型转换
@{()->goodsHandler.changeGoodsName()} 点击事件
@{list[index],default=xx} 集合取值
@{map[key],default=yy} map取值
@{map.key} map取值
@{user.male ? View.VISIBLE : View.GONE}” 视图可见
@{set.contains(“xxx”)?”xxx”:key} 运算符示例
@{flag ? @dimen/paddingBig:@dimen/paddingSmall} 资源文件索引,只能通过表达式赋值,不能直接@drawwable/resoursename
@{user.name != null ? user.name : user.password} 空合并运算符,取第一个不为null的值

xml布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<data>
<import type="com.leavesc.databinding_demo.model.Goods" />
<import type="com.leavesc.databinding_demo.Main3Activity.GoodsHandler" />
<variable
name="goods"
type="Goods" />
<variable
name="goodsHandler"
type="GoodsHandler" />
</data>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="20dp"
tools:context=".Main3Activity">

<TextView
···
android:text="@{goods.name}" />

<TextView
···
android:text="@{goods.details}" />

<TextView
···
android:text="@{String.valueOf(goods.price)}" />

<Button
···
android:onClick="@{()->goodsHandler.changeGoodsName()}"
android:text="改变属性 name 和 price"
android:textAllCaps="false" />

<Button
···
android:onClick="@{()->goodsHandler.changeGoodsDetails()}"
android:text="改变属性 details 和 price"
android:textAllCaps="false" />

</LinearLayout>
</layout>

ui层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class Main3Activity extends AppCompatActivity {

private Goods goods;

private ActivityMain3Binding activityMain3Binding;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main3);
activityMain3Binding = DataBindingUtil.setContentView(this, R.layout.activity_main3);
goods = new Goods("code", "hi", 24);
activityMain3Binding.setGoods(goods);
activityMain3Binding.setGoodsHandler(new GoodsHandler());
}

public class GoodsHandler {

public void changeGoodsName() {
goods.setName("code" + new Random().nextInt(100));
goods.setPrice(new Random().nextInt(100));
}

public void changeGoodsDetails() {
goods.setDetails("hi" + new Random().nextInt(100));
goods.setPrice(new Random().nextInt(100));
}

}

}

事件处理#

根据官网介绍,事件处理总共有两种写法,一种是自定义方法引用,一种是监听器绑定

自定义方法引用#

共有3步

  1. 自定义类和方法,方法必须为public,入参必须带View
  2. 在布局文件中import导入该类
  3. buttononclick属性里传入类名::方法名,注意用两个冒号隔开
  4. 在Activity中创建类的实例,传入数据绑定类

定义一个类MyHandlers,里面定义点击事件的回调方法onClickFriend

1
2
3
4
5
6
public class ButtonClickHandler {
public void onClick(View view){
Log.e("ButtonClickHandler", "onClick: " );
}
}

xml中绑定类MyHandlers的回调方法

  • data标签中声明类型MyHandlers的变量handler
  • viewonClick属性绑定该类的属性android:onClick="@{handlers::onClickFriend}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="btnhandler"
type="com.shaunsheep.mvvm.dataBinding.method.ButtonClickHandler" />

</data>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{btnhandler::onClick}"
android:text="事件处理-方法引用写法"
android:textAllCaps="false" />
</layout>

1
activityDatabindBinding.setBtnhandler(new ButtonClickHandler());

自定义监听器#

分为3步

  1. 创建类和方法,方法入参任意写,比如User对象
  2. xml中导入该类,导入User类
  3. buttononclick属性传入@{()->类名.方法名(user)}
  4. Activity中为Databinding设置类的实例化对象

!>注意入参要和方法声明保持一致,这个例子为User

方法和类的声明

1
2
3
4
5
6
public class ButtonClickHandler {
public void onClick(View view){
Log.e("ButtonClickHandler", "onClick: " );
}
}

导入

1
2
3
<variable
name="clickpresenter"
type="com.shaunsheep.mvvm.dataBinding.method.ClickPresenter" />

传入属性

1
2
3
4
5
6
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{()->clickpresenter.onSaveUser(user)}"
android:text="事件处理-监听器绑定写法"
android:textAllCaps="false" />

设置类的实例化对象

1
activityDatabindBinding.setMyHandlers(new ClickPresenter());

双向绑定view#

@符号后多了一个符号=

写法
<EditText ··· android:text=”@={goods.name}” /> 双向同步

自定义特性的双向绑定#

问:有什么用?

答:实现双向数据绑定——数据源修改,视图变化;视图变化,数据源也发生变化;

需要使用@BindingAdapter@InverseBindingAdapter两个注解。

  • BindingAdapter用于数据绑定类调用该方法,为view设置成员变量的属性值、为view设置InverseBindingListener 子类实现
  • InverseBindingAdapter用于让数据绑定类获得该方法的返回值,达到getValue的效果

例如我们要为ScrollView 制作上拉到底部,显示加载更多布局,在编码前,我们进行如下设计

  • 我们在ScrollView子view里定义一个类型为boolean的成员变量footerRefreshing,用于控制加载更多布局的显示和隐藏
  • 我们在ScrollViewView里提供三个方法
  1. 方法1通过 @BindingAdapte声明数据绑定类如何设置view的属性值
1
2
3
4
5
6
7
8
9
10
11
12
13
@BindingAdapter(value = "footerRefreshing", requireAll = false)
public static void setFooterRefreshing(PhilView view, boolean refreshing) {
if (isRefreshing == refreshing) {
//防止死循环
Log.d(TAG, "footerRefreshing重复设置");
return;
} else {
Log.d(TAG, "setFooterRefreshing " + refreshing);
isRefreshing = refreshing;
}
}


  1. 通过@InverseBindingAdapter声明数据绑定类如何获取view的属性值
1
2
3
4
5
@InverseBindingAdapter(attribute = "footerRefreshing", event = "footerRefreshingAttrChanged")
public static boolean getFooterRefreshing(PhilView view) {
return isRefreshing;
}

  1. 通过@BindingAdapte声明监听器,当view的成员变量footerRefreshing发生变化的时候,会返回一个InverseBindingListener子类实现,该类有一个onChange方法,用于告知数据绑定类,成员变量发生变化
1
2
3
4
5
6
7
8
9
10
11
@BindingAdapter(value = {"footerRefreshingAttrChanged"}, requireAll = false)
public static void setFooterRefreshingAttrChanged(PhilView view, final InverseBindingListener inverseBindingListener) {
Log.d(TAG, "setFooterRefreshingAttrChanged");

if (inverseBindingListener == null) {
view.setRefreshingListener(null);
} else {
mFooterInverseBindingListener1 = inverseBindingListener;
view.setRefreshingListener(mOnRefreshingListener);
}
}
  1. 在合适的时机,如监听到滑动至ScrollView底部的事件,显示加载更多布局;延时指定时间后,隐藏加载更多布局
1
2
3
4
5
6
7
8
9
10
11
12
  public void startRefreshing() {
isRefreshing = true;
mFooterInverseBindingListener1.onChange();
mHeaderInverseBindingListener2.onChange();

}

public void stopRefreshing() {
isRefreshing = false;
mFooterInverseBindingListener1.onChange();
mHeaderInverseBindingListener2.onChange();
}

下拉刷新加载更多Demo源码地址

Demo时序图

BindingAdapter和InverseBindingAdapter用法

自定义特性的用法总结#

假设存在类型为A、属性名为a的变量

@BindingAdapter声明2个公有静态类方法,传入的value值都为A,方法入参不同,前者用于数据绑定类为View设置属性a、后者为用于数据绑定类为View设置属性监听

1
2
3
4
5
@BindingAdapter("a")
public static void setA(View view, A a)
@BindingAdapter("aAttrChanged")
public static void setListeners(
View view, final InverseBindingListener attrChange)

@InverseBindingAdapter声明1个静态公有类方法,传入View,用于数据绑定类获取View的成员属性值a

1
2
@InverseBindingAdapter("a")
public static Time getA(View view)

基础运算符#

DataBinding 支持在布局文件中使用以下运算符、表达式和关键字

  • 算术 + - / * %
  • 字符串合并 +
  • 逻辑 && ||
  • 二元 & | ^
  • 一元 + - ! ~
  • 移位 >> >>> <<
  • 比较 == > < >= <=
  • Instanceof
  • Grouping ()
  • character, String, numeric, null
  • Cast
  • 方法调用
  • Field 访问
  • Array 访问 []
  • 三元 ?:

目前不支持以下操作

  • this
  • super
  • new
  • 显示泛型调用

注解#

@BindingAdapter 事件监听、属性赋值、类型转换、埋点业务、防重按等切片编程
@Bindable 更新私有属性
@InverseBindingAdapter 参考这篇文章
@InverseBindingMethod
@InverseMethod

绑定适配器#

android Jetpack DataBinding - 注解篇(BindingAdapter) - summer_xx - 博客园 (cnblogs.com)

定义#

BindingAdapter :绑定适配器,是 Jetpack DataBinding 中用来扩展布局 xml 属性行为的注解,允许你针对布局 xml 中的一个或多个属性进行绑定行为扩展,这个属性可以是自定义属性,也可以是原生属性。这个扩展行为可以是简单的ViewModel属性与控件赋值绑定,也可以是关联某个控件属性的额外操作,例如在设置属性之前进行值域检查,或类型转换,或者统一处理一些事情。

关键词:扩展xml属性的行为

假设有2个变量,这个两个变量作用于2个view,根据这2个变量我们可以设置ImageView的资源文件,资源文件来自于网络、磁盘等

很明显我们会遇到困难:通过databind.setImagePath、databind.setImagePath2,无法实现这个目的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
           <variable
name="imgpath"
type="String" />
<variable
name="imgpath2"
type="String" />
...
<ImageView
android:id="@+id/iv_1"
android:layout_width="30dp"
android:layout_height="30dp"
android:scaleType="fitXY"
android:src="@{imgpath}" />

<ImageView
android:id="@+id/iv_2"
android:layout_width="30dp"
android:layout_height="30dp"
android:scaleType="fitXY"
android:src="@{imgpath2}" />

src是默认的控件属性,而src需要使用一个资源ID,来完成赋值,原生的ImageView并不支持网络地址来获取设置图片,此时就可以通过 BindingAdapter 来扩展行为,使其具备使用一个网络地址,并且从网络下载图片并显示的能力

这一段代码就实现了对android:src属性的扩展,有以下3个要点:

  1. 调用databind.setImagePathdatabind.setImagePath2

  2. 它会触发android:src="@{imgpath}"android:src="@{imgpath2}"

  3. 此时会寻找匹配src的注解方法,通过注解上的声明@BindingAdapter("android:src"),自动找到了静态函数setSrc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@BindingAdapter("android:src")
public static void setSrc(ImageView imageView, String imagePath) {
Log.e("绑定适配器", "setSrc: " + imagePath);
if (TextUtils.isEmpty(imagePath)) {
return;
}
// 伪代码
Glide.with(imageView.getContext())
.load(imagePath)
.error(R.drawable.ic_launcher_background)
.listener(new RequestListener<String, GlideDrawable>() {
@Override
public boolean onException(Exception e, String model, Target<GlideDrawable> target, boolean isFirstResource) {
Log.e("绑定适配器", "onException: "+e.toString() );
return false;
}

@Override
public boolean onResourceReady(GlideDrawable resource, String model, Target<GlideDrawable> target, boolean isFromMemoryCache, boolean isFirstResource) {
return false;
}
})
.into(imageView);
}

定义静态方法#

  • 绑定方法的位置:绑定适配的方法可以随意放在任何 类中,你可以自定义一个类,也可以放在Activity里,APT会在构建时扫描全局代码

  • 多属性写法:

1
2
3
4
5
6


value = {"galleryEffectWidthLeft",
"galleryEffectWidthRight",
"galleryEffectPageMargin",
"galleryEffectScale"}
  • requireAll

    用于校验数量一致性:注解声明的属性标签数量必须等于方法入参数量

    默认是true:表示函数入参必须包括所有注解声明的xml属性标签,缺一不可;

    如果设置false,表示不需要关乎所有的xml属性标签

1
2
3
4
5
6
7
8
9
public class ViewAttrAdapter {
// 需要注意的是 XML标签 关注了几个,参数列表就需要写几个对应的接受参数。且关注控件类必须在第一个参数。
@BindingAdapter({xml属性标签, ...},requireAll = {true|false})
public static void 函数名(关注的控件类 view, xml属性标签值 value, ...){
// 行为
}
// 可以包含多个函数
.....
}

当requireAll = true ,注解处声明4个属性,入参参数必须是4个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
    @BindingAdapter(value = {"galleryEffectWidthLeft", 
"galleryEffectWidthRight",
"galleryEffectPageMargin",
"galleryEffectScale"},
requireAll = true)
public static void setBannerGalleryEffect(Banner banner, int galleryWidthLeft, int galleryWidthRight, int galleryPageMargin, float galleryScale) {
banner.setBannerGalleryEffect(galleryWidthLeft,
galleryWidthRight,
galleryPageMargin,
galleryScale);
}

// xml必须设置4个值,才会生效进入注解方法
<com.youth.banner.Banner
android:layout_width="match_parent"
android:layout_height="175dp"
android:layout_marginTop="90dp"
app:adapter="@{recommendVipZoneViewModel.bannerAdapter}"
app:banner_radius="8dp"
app:galleryEffectWidthLeft="@{7}"
app:galleryEffectWidthRight="@{8}"
app:galleryEffectPageMargin="@{9}"
app:galleryEffectScale="@{0.85f}" />
// 错误,app:galleryEffectScale 属性未设置,不会进入注解方法
<com.youth.banner.Banner
android:layout_width="match_parent"
android:layout_height="175dp"
android:layout_marginTop="90dp"
app:adapter="@{recommendVipZoneViewModel.bannerAdapter}"
app:banner_radius="8dp"
app:galleryEffectWidthLeft="@{7}"
app:galleryEffectWidthRight="@{8}"
app:galleryEffectPageMargin="@{9}" />

设置多个属性#

useAdapter设置多个属性值;静态注解方法里value参数传入标签数组,数组写法为大括号包裹,逗号分割开,每个属性表情为双引号包裹

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
     public void useAdapter(View view) {
activityDatabindBinding.setImgpath(imagePath);
activityDatabindBinding.setImgpath2(imagePath2);
activityDatabindBinding.setPaddingLeft(3);
}


/**
* android:src的含义是关注所有android:src处的赋值,一有变动就进入此方法
* @param imageView
* @param imagePath
*/
@BindingAdapter(value = {"android:src","android:paddingLeft"},requireAll = false)
public static void setttSrc(ImageView imageView, String imagePath,Integer paddingLeft) {
Log.e("绑定适配器", "setSrc: " + imagePath);
Log.e("绑定适配器", "paddingLeft: " + paddingLeft);

if (TextUtils.isEmpty(imagePath)) {
return;
}
imageView.setPadding(paddingLeft,imageView.getPaddingTop(),imageView.getPaddingRight(),imageView.getPaddingBottom());
// 伪代码
Glide.with(imageView.getContext())
.load(imagePath)
.error(R.drawable.ic_launcher_background)
.listener(new RequestListener<String, GlideDrawable>() {
@Override
public boolean onException(Exception e, String model, Target<GlideDrawable> target, boolean isFirstResource) {
Log.e("绑定适配器", "onException: "+e.toString() );
return false;
}

@Override
public boolean onResourceReady(GlideDrawable resource, String model, Target<GlideDrawable> target, boolean isFromMemoryCache, boolean isFirstResource) {
return false;
}
})
.into(imageView);

}

布局文件参考如下

可以看到2个ImageView有两个属性

  • ​ android:paddingLeft=”@{paddingLeft}”
  • ​ android:src=”@{imgpath}”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
       <variable
name="paddingLeft"
type="Integer" />
<variable
name="imgpath"
type="String" />

<variable
name="imgpath2"
type="String" />
...

<ImageView
android:id="@+id/iv_1"
android:layout_width="30dp"
android:layout_height="30dp"
android:scaleType="fitXY"
android:paddingLeft="@{paddingLeft}"
android:src="@{imgpath}" />

<ImageView
android:id="@+id/iv_2"
android:layout_width="30dp"
android:layout_height="30dp"
android:paddingLeft="@{paddingLeft}"
android:scaleType="fitXY"
android:src="@{imgpath2}" />

判断新值和旧值#

静态方法为一个变量可以接收2个参数,第一个参数是指旧值,第二个参数是指新值

1
2
3
4
5
6
7
8
9
10
11
12
@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());
}
}

接收多个新值和旧值#

同时获取旧值和新值的方法应该先为所有属性声明所有旧值,然后再按顺序声明所有属性的新值

1
2
3
4
5
6
7
@BindingAdapter(value = {"android:src","android:paddingLeft"},requireAll = false)
public static void setttSrc(ImageView imageView,
String oldImagePath,
Integer oldPaddingLeft,
String newImagePath,
Integer newPaddingLeft) {
}

常见问题#

问题1:绑定适配器的匹配规则是什么?

解决:输入什么值,就跟什么值进行模糊匹配:如src可以与android:srcsrc匹配

1
2
3
4
// 这个限定使用时一定是android:src才能匹配
@BindingAdapter("android:src")
// 这个限定使用时 app:src 和 android:src 都可以匹配,但需要注意的是,android必须是在自定义属性声明的XML中有描述过的,如 attrs.xml 中
@BindingAdapter("src")

问题2:old values should be followed by new values

可能出现于2种场合

  1. 注解的value参数,设置了错误的属性标签数量
  2. 有多个属性标签,且出现了新值和老值

解决:

情况1:错误写法@BindingAdapter({"created_by,created_at"}

正确写法@BindingAdapter({"created_by","created_at"},双引号包裹单个属性标签,用逗号分割多个属性标签

情况2:存在多个新值老值得参数情况,顺序为:oldValueA,oldValueB,newValueA,newValueB,先按顺序写老值,后按顺序传新值;正确写法为接收多个新值和旧值

点击查看
-------------------本文结束 感谢您的阅读-------------------