如何使用Data Binding Library(二)

作者: lanceJin | 来源:发表于2017-06-11 23:43 被阅读88次

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

    相关文章

      网友评论

      • 卜俊文:有列表的例子吗
        lanceJin:嗯,好的,我到时写好传到Github上。:blush:

      本文标题:如何使用Data Binding Library(二)

      本文链接:https://www.haomeiwen.com/subject/soebfxtx.html