如何使用Data Binding Library(二)

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中的代码。

BindingViewHolder.png
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方法类似,只是注释不一样)。

Renamed Getter.png

Custom Getter.png

通过自定义属性Setter设置TextWatcher,内部调用InverseBindingListener的onChange()方法来更新Model。

Event Handler.png

Set Model.png

由于是双向绑定,当界面改变数据后,数据又会改变界面,界面再改变数据,形成死循环,所以在属性Setters时,加上内容是否真的变化的判断。


Dead loop.png

这些更新逻辑都是由系统和框架自动完成。若开发者想对数据的改变加入自己的操作,可以通过addOnPropertyChangedCallback()方法添加实现。

Add Callback.png

7.使用其它控件属性


前面都是通过改变<variable>的值,来引起界面的变化,并不涉及控件间的引用。下面两个来自慕课网的例子分别使用其它控件的Visibility和Checked属性。

Visibility.png

Checked.png

8.总结


以上就是Data Binding比较常见的高级用法。需注意的是,表达式应该简单明了,与界面交互相关,而不该包含业务等复杂逻辑。还有动画和测试的内容,不过资料较少或感觉功能单一便没有介绍。若对这些方面了解全面的朋友欢迎联系交流。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容