1.前言
通过上一讲的介绍,可以走通Data Binding基本的流程,了解实现的逻辑。但是仅仅掌握这些是不够的,使用时会感觉缺乏灵活性,关键还是在界面的复用和属性的自定义上。Data Binding在layout中下足了功夫,也许这是ViewModel名字的由来吧。
2.高级标签
传统开发时,经常会使用一些具有优化布局和性能的标签,它们在Data Binding中是同样支持的。
2.1.include标签
当有相同布局时,通常会复用某些layout文件,但是展示的内容不一样时怎么办?以往做法,对相同布局可以自定义控件和对不同内容可以自定义属性,或者先include再findViewById()
等。
而Data Binding支持命名空间和变量名组合成属性,向<include>
中的布局传值,有点类似app:title="title"
这种自定义属性的样式。
<?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>
1.被传值的布局必须包含此变量;
2.<include>
不能作为<merge>
的直接子元素。
2.2.ViewStub标签
<ViewStub>
是个大小为0,且不可见的View。只能通过findViewById()
来找到,然后调用inflate()
或setVisibility(View.VISIBLE)
通知它载入设置的布局取代自己。
由于这一特性,<ViewStub>
在视图层次结构上是不存在的,Data Binding自动生成ViewStubProxy对象来帮助访问<ViewStub>
,以便初始化。同时ViewStubProxy中含有OnInflateListener监听器,当<ViewStub>
载入布局成功后,可以为新布局设置Binding。开发者可以自定义监听器,实现自己想要的操作。
binding.viewStub.getViewStub().inflate();
3.Binding进阶
关于Binding的使用还有一些其它事情需要注意。
3.1.动态Variables
通常开发不同类型Item时,都是在Adapter中先调用getItemViewType()
,给出区分逻辑,列出不同的种类;再是调用onCreateViewHolder()
,根据不同类型给出不同的View,封装成ViewHolder;最后调用onBindViewHolder()
,判断不同ViewHolder,给出不同展示和操作。
而Data Binding中第一步不变,第二步ViewHolder封装不同ViewDataBinding子类或者直接ViewDataBinding,第三步获取不同的ViewDataBinding子类做相应操作或者使用ViewDataBinding的 setVariable()
方法。
setVariable()
方法的好处是,若不同的逻辑都放在XML中,那么只要<variable>
名字相同,可以简化Adapter中的代码。
3.2.即时Binding
当<variable>
发生改变时,Binding将计划在下一帧之前刷新界面。有时需要立即执行,比如快速滚动RecyclerView,Item是可复用的,不立即执行会影响显示。
public void onBindViewHolder(BindingHolder holder, int position) {
final T item = mItems.get(position);
// 此处为通用的传值方法
holder.getBinding().setVariable(BR.item, item);
// 此处强制立即执行
holder.getBinding().executePendingBindings();
}
3.3.后台线程
Data Binding为解决线程同步问题,会本地化变量和属性。可以在线程中改变数据模型,集合除外。
4.属性Setters
当给布局文件中的控件属性赋值时,有些是系统命名空间的,有些是自定义命名空间的,那么Data Binding将会如何处理。
4.1.自动Setters
不考虑命名空间,只与属性名和赋值表达式的返回值有关,因为它们分别对应方法名和参数类型。方法名为set加上属性名的驼峰写法,例如setText()
;参数类型影响重载方法的选择,必要时在赋值的表达式中强制类型转换。
若控件中不含有某个属性,不会影响Data Binding的工作。开发者甚至可以为控件添加对应方法,使之完成自己的逻辑,比以前自定义属性简单多了。
4.2.重命名Setters
拿个Android框架已实现的说明一下。android:tint
属性对应的setter方法是setImageTintList()
,方法名是不一样的,这时自动setters策略失效了,需要通过@BindingMethods
和@BindingMethod
注解,在任意类前声明下引用,可同时声明多个。
@BindingMethods({
@BindingMethod(type = "android.widget.ImageView",
attribute = "android:tint",
method = "setImageTintList"),
})
4.3.自定义Setters
有些属性需要开发者自己实现逻辑,还是拿Android框架已实现的举例子。android:paddingLeft
属性没有对应的setter方法,只能通过借助setPadding(left, top, right, bottom)
方法实现。需创建个类,对其中实现逻辑的方法使用@BindingAdapter
注解标明。
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
甚至可以给控件一个异步加载图片的属性。当与系统默认的冲突时,开发人员定义的优先考虑。
可以创建多个参数的适配器,要求控件同时使用这两个属性时,才起作用。
@BindingAdapter({"bind:imageUrl", "bind:error"})
public static void loadImage(ImageView view, String url, Drawable error) {
Picasso.with(view.getContext()).load(url).error(error).into(view);
}
<ImageView
app:imageUrl="@{venue.imageUrl}"
app:error="@{@drawable/venueError}"/>
1.匹配过程中自定义的命名空间将被忽略;
2.可以为Android的命名空间写适配器。
在适配器中可同时获取旧值和新值,不过参数列表先排列所有旧值再是新值。
@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());
}
}
对于事件处理,适配器要求参数为含有一个抽象方法的接口或抽象类作为监听器。若监听器不只一个抽象方法,则需要拆分到多个独立的监听器中。若它们关联紧密,须同时设置,则应该增加多参数适配器,包含所有监听器。
@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.OnAttachStateChangeListener含有两个抽象方法
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewDetachedFromWindow {
void onViewDetachedFromWindow(View v);
}
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewAttachedToWindow {
void onViewAttachedToWindow(View v);
}
// 根据情况设置
@BindingAdapter("android:onViewAttachedToWindow")
public static void setListener(View view, OnViewAttachedToWindow attached) {
setListener(view, null, attached);
}
@BindingAdapter("android:onViewDetachedFromWindow")
public static void setListener(View view, OnViewDetachedFromWindow detached) {
setListener(view, detached, null);
}
@BindingAdapter({"android:onViewDetachedFromWindow", "android:onViewAttachedToWindow"})
public static void setListener(View view, final OnViewDetachedFromWindow detach,
final OnViewAttachedToWindow attach) {
if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1) {
final 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);
}
}
};
}
// ListenerUtil管理之前的监听器
final OnAttachStateChangeListener oldListener = ListenerUtil.trackListener(view,
newListener, R.id.onAttachStateChangeListener);
if (oldListener != null) {
view.removeOnAttachStateChangeListener(oldListener);
}
if (newListener != null) {
view.addOnAttachStateChangeListener(newListener);
}
}
}
5.转换器
通过表达式给XML属性设值,需根据属性名和值类型找到对应方法。若类型不符合或方法重载时,怎么办?
5.1.自动转换
当属性名和值类型明确有对应setter时,没问题。若方法唯一,值类型不对,会自动转化为所需参数类型。若存在方法重载,需开发人员在表达式中强转。
5.2.自定义转换
当有些类型系统无法自动转换时,需自己定义转换逻辑。比如,background属性需要Drawable对象,而表达式返回Integer类型地Color对象,可通过以下方法转化(不支持表达式可返回多种类型)。
<View
android:background="@{isError ? @color/red : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
return new ColorDrawable(color);
}
6.双向绑定
前面一直都在讲数据对界面的影响,似乎唯一能做到界面改变数据的就只有事件处理了。其实,理论中讲过Data Binding最大的优势就是双向绑定。当控件对某属性的改变具有监听事件时,即可使用。但是这块的知识在官网上没找到,是从慕课网上学习到的,感谢原创者的分享。
拿 <EditText>
的android:text
的属性来说。当需要将控件内修改的内容赋值给Model时,常规方法就是添加TextWatcher。
EditText editText = (EditText) findViewById(R.id.edittext);
editText.addTextChangedListener(watcher);
private TextWatcher watcher = new TextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
// TODO Auto-generated method stub
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
// TODO Auto-generated method stub
}
@Override
public void afterTextChanged(Editable s) {
// TODO Auto-generated method stub
}
};
而在Data Binding中只需要android:text="@={...}"
。建议设置的数据为实现Observble的对象,这样可以改变界面上其它引用此Model的控件。由于系统已经实现了这个功能,可以看看实现的流程。
首先通过属性Getters方法,获取对应值的改变,并指定调用的事件(与属性Setters方法类似,只是注释不一样)。
通过自定义属性Setter设置TextWatcher,内部调用InverseBindingListener的onChange()
方法来更新Model。
由于是双向绑定,当界面改变数据后,数据又会改变界面,界面再改变数据,形成死循环,所以在属性Setters时,加上内容是否真的变化的判断。
这些更新逻辑都是由系统和框架自动完成。若开发者想对数据的改变加入自己的操作,可以通过addOnPropertyChangedCallback()
方法添加实现。
7.使用其它控件属性
前面都是通过改变<variable>
的值,来引起界面的变化,并不涉及控件间的引用。下面两个来自慕课网的例子分别使用其它控件的Visibility和Checked属性。
8.总结
以上就是Data Binding比较常见的高级用法。需注意的是,表达式应该简单明了,与界面交互相关,而不该包含业务等复杂逻辑。还有动画和测试的内容,不过资料较少或感觉功能单一便没有介绍。若对这些方面了解全面的朋友欢迎联系交流。