DataBindingAndroid Architecture ComponentsAndroid知识

如何使用Data Binding Library(二)

2017-06-11  本文已影响88人  lanceJin

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

上一篇下一篇

猜你喜欢

热点阅读