安卓 Data Binding 使用方法总结(妹妹篇)

时间:2022-05-10 23:19:46

前言

本文是《 安卓 Data Binding 使用方法总结(姐姐篇)》的姊妹篇。姐姐篇写于 Google I/O 2016 之前,当时还没有双向绑定、lambda 表达式这些特性。

本文参考视频 Advanced Data Binding - Google I/O 2016 、经实战后写成,主要涉及:

  • 双向绑定
  • lambda 表达式
  • 特殊变量
  • 动画
  • 自定义字体

一起来充电吧!

双向绑定

用法举例

很简单,在要使用双向绑定的地方,使用 “@={}” 即可。

<EditText android:text="@={user.firstName}" />

注意,这里的 firstName 必须是 ObservableField <T> 类型,至于原因,我们在探讨双向绑定的实现原理的时候会说明。

适用范围

双向绑定只适用于那些某个属性绑定监听事件的控件,如

  • TextView/EditView/Button (android:text, TextWatcher)
  • CheckBox (android:checked, OnCheckedChangeListener)
  • DatePicker(android:year, android:month, android:day, OnDateChangedListener)
  • TimePicker(android:hour, android:minute, OnTimeChangedListener)
  • RatingBar(android:rating, OnRatingBarChangeListener)

大部分控件都能满足双向绑定的需求,实在不行就自定义满足该要求的控件吧。

原理简析

双向绑定的实现原理的核心是 InverseBindingListener 这个接口:

package android.databinding;

public interface InverseBindingListener {
    void onChange();
}

我们再看这个例子:

<EditText android:text="@={user.firstName}" />

此时框架生成对应的 MainActivity2WayBinding.java,摘录其中的相关代码:

    private android.databinding.InverseBindingListener mboundView1androidTe = new android.databinding.InverseBindingListener() {
        @Override
        public void onChange() {
            // some code

            firstNameUser.set((java.lang.String) (callbackArg_0));

            // some code
        }
    };

    @Override
    protected void executeBindings() {
                if ((dirtyFlags & 0x7L) != 0) {                       
                android.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView1, firstNameUser);
        }
    }

简单分析一下上述代码。

框架自动生成了一个 android.databinding.InverseBindingListener,该 listener 的作用就是更新 firstName 的值,即 firstName.set()。所以 firstName 必须是 ObservableField<T> 类型。

然后该 listener 被绑定到 EditText 上。

firstName 值被更新时,会执行 executBindings(),调用 TextViewBindingAdapter.setText() 方法:

@BindingAdapter("android:text")
    public static void setText(TextView view, CharSequence text) {
        final CharSequence oldText = view.getText();
        if (text == oldText || (text == null && oldText.length() == 0)) {
            return;
        }
        if (text instanceof Spanned) {
            if (text.equals(oldText)) {
                return; // No change in the spans, so don't set anything.
            }
        } else if (!haveContentsChanged(text, oldText)) {
            return; // No content changes, so don't set anything.
        }
        view.setText(text);
    }

只有值发生改变时才会更新 EditText 的值,否则什么都不做,防止出现无限循环。

lambda 表达式

<Button android:onClick="@{(v) -> presenter.save(v, user)}" />

在这个 lambda 表达式中,OnClickListener.onClick(View v) 没有返回值,presenter.save(v, user) 有或没有返回值都能正常运行;但是如果事件的方法有返回值,如 OnLongClickListener.onLongClick(View v) 返回布尔值,则 android:onLongClick='@{(v) -> presenter.save(v, user)}' 中的方法 presenter.save(v, user) 必须返回对应类型的值,否则编译报错。

@{(v) -> presenter.save(v, user)} 中,-> 前面的参数要么为空,要么全部列出来并和对应的 listener 的回调方法的参数保持一致,参数可随意命名,比如 OnFocusChangeListener.onFocusChanged(View v, boolean hasFocus),可以写成 android:hasFocus='@{() -> presenter.refresh(fcs)}'android:hasFocus='@{(v, fcs) -> presenter.refresh(fcs)}'的形式。

除了 lambda 表达式,我们还可以使用方法引用(method reference)的形式: android:onClick='@{presenter.save}',这两种方式有什么区别呢?直接引用发布会上 keynote 中的截图:


安卓 Data Binding 使用方法总结(妹妹篇)



安卓 Data Binding 使用方法总结(妹妹篇)

在回调方法的参数方面也有所不同,在 lambda 表达式的参数可以是任意表达式,而方法引用的参数则必须要和 listener 的回调方法保持一致:

lambda

"... = @{()->presenter.save(user.friend)}"
"... = @{()->data.presenter.save(user.friend)}"
this.saveButton.setOnClickListener(this);

void onCick(View view) {
   Presenter presenter = this.presenter;
   Item item = this.item;
   if (presenter != null) {
       presenter.saveItem(item);
   }
}

方法引用

"...onClick=@{presenter::save}"
Presenter presenter = this.presenter;
if (presenter != null) {
    this.saveButton.setOnClickListener(new Listener(presenter));

} else {
    this.saveButton.setOnClickListener(null);
}

class Listener implements OnClickListener {
    void onClick(View view) {
        mPresenter.onClick(view);
    }
}

注意,方法引用中,回调方法返回值的类型(不管是 void,boolean,还是其他)都要一致,否则编译报错。如,...onClick='@{presenter::save}' 中 Presenter.save(View v) 的返回值类型要和 OnClickListener.onClick(View v) 一致。

方法引用不止能在 android:onClick= 属性中使用,在其他属性中也可以使用,而且可以使用表达式作为参数,见 特殊变量->Context 一节中的例子:

    <TextView
        android:id="@+id/context_demo"
        android:text="@{user.load(context, @id/context_demo)}" />


    public String load(Context context, int field) {
        return context.getResources().getString(R.string.app_name);
    }

特殊变量

带 id 的控件

可以在表达式中直接引用带 id 的 view,引用时采用驼峰命名法。
将一个控件的属性赋值给另一个属性,这样我们可以在 layout 中完成 UI 的展示逻辑,简洁而且可读性强,从而让开发者把精力集中在业务逻辑的开发。
如下面的代码,是根据 CheckBox 是否勾选而决定是否展示相应的控件。第一个 EditText 的表达式中引用了 CheckBox 的属性 checked,第二个 EditText 引用第一个 EditText 的 visibility 属性。

        <CheckBox
            android:id="@+id/checkbox"
            android:text="填写姓名" />

        <EditText
            android:id="@+id/first_name"
            android:text="@={user.firstName}"
            android:visibility="@{checkbox.checked ? View.VISIBLE : View.GONE}" />

        <EditText
            android:text="@{user.lastName}"
            android:visibility="@{firstName.visibility}" />

Context

现在我们可以脱离具体的 View 就能得到 Context,得到 Context 是根 view 的 Context。

注意如果有名为 context 的自定义变量存在,前者会被覆盖掉。

    <TextView
        android:id="@+id/context_demo"
        android:text="@{user.load(context, @id/context_demo)}" />


    public String load(Context context, int field) {
        return context.getResources().getString(R.string.app_name);
    }

动画

在 DataBinding 中,我们可以使用 Transition (适用 API >= 19,系统 >= 4.4)来实现某些动画效果。如理如下:

        binding.addOnRebindCallback(new OnRebindCallback() {
            @Override
            public boolean onPreBind(ViewDataBinding binding) {

                ViewGroup root = (ViewGroup) binding.getRoot();

                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                    TransitionManager.beginDelayedTransition(root);
                }

                return true;
            }
        });

两个 TextView 在隐藏和显示时会有延迟效果,如下图:


安卓 Data Binding 使用方法总结(妹妹篇)

但是这种方法对某些情况是失效的,如随着滚轮的滑动 TextView 的内容发生改变:

安卓 Data Binding 使用方法总结(妹妹篇)

更具普遍性的方法是在 @BindingAdapter 修饰的方法中进行设置:

@BindingAdapter("adText")
public static animateTextChanges(TextView textView, String oldText, String newText) {
    if (oldText == null || oldText.equals(newText)) {
       return;
    }

    animateTextChange(textView, oldText, newText);
}

自定义字体

我们可以通过如下方式来为 TextView 自定义字体:

<TextView app:font="@{`Source-Sans-Pro-Regular.ttf`}"/> 

public class AppAdapters {
  @BindingAdapter({"font"})
  public static void setFont(TextView textView, String fontName){
    AssetManager assetManager = textView.getContext().getAssets();
    String path = "fonts/" + fontName;
    Typeface typeface = sCache.get(path);
    if (typeface == null) {
      typeface = Typeface.createFromAsset(assetManager, path);
      sCache.put(path, typeface);
    }
    textView.setTypeface(typeface);
  }
}

更多信息请参考开源项目 fontbinding

最佳实践

通过一个登录页面的 layout 布局,来感受一下双向绑定、字符串引用等的使用方法:

<layout>
    <data>
        <variable name="model" type="iotalks.ForModel" />
        <import type="iotalks.Validator" />
    </data>

    <LinearLayout>
        <TextView android:text="@={model.name}"/>
        <Button  android:enabled="@{Validator.isValid(model)}" android:onClick="@{()->presenter.save(model)}" android:text="@{@string/welcome(model.name)}" />
    </LinearLayout>
</layout>

如果还不过瘾,请参考开源项目:Android Architecture Blueprints [beta],绝对让你茅塞顿、豁然开朗、醍醐灌顶、如梦初醒。

更多资料

也许本文不值得一看,但是下面这些资料则不然。