Android

Android Data Binding——高级

2017-04-23  本文已影响139人  FlyDragonInSky

上一篇文章Android Data Binding——进阶
介绍了Data Binding的语法等进阶功能。这一篇我们来介绍一下Data Binding的数据对象。

文中的例子可前往DataBindingDemo查看。

任何POJO对象都可以用在data binding中,但是对象改变时候,要如何通知UI更新呢?这是使用Data Binding最奥妙的地方。Ps:我们这边只是介绍如何使用,没有涉及到实现原理。

有三种不同的数据变化通知机制:observable objects, observable fields, and observable collections.

这些observable对象绑定到UI上,当对象的属性更改时就会自动通知UI更新。

Observale Objects

一个继承Observable接口的类,data binding会设置一个listener用于监听绑定的对象的属性变化。

public interface Observable {

    /**
     * Adds a callback to listen for changes to the Observable.
     * @param callback The callback to start listening.
     */
    void addOnPropertyChangedCallback(OnPropertyChangedCallback callback);

    /**
     * Removes a callback from those listening for changes.
     * @param callback The callback that should stop listening.
     */
    void removeOnPropertyChangedCallback(OnPropertyChangedCallback callback);

    /**
     * The callback that is called by Observable when an observable property has changed.
     */
    abstract class OnPropertyChangedCallback {

        /**
         * Called by an Observable whenever an observable property changes.
         * @param sender The Observable that is changing.
         * @param propertyId The BR identifier of the property that has changed. The getter
         *                   for this property should be annotated with {@link Bindable}.
         */
        public abstract void onPropertyChanged(Observable sender, int propertyId);
    }
}

Observable接口有注册/删除监听的方法,但是数据变化时是否通知取决于开发者。为了简化开发,data binding提供了一个BaseObservable的基类,帮我们实现了监听的注册和删除。这个类也实现了通知数据变化的方法,在getter中使用Bindable注解,在setter中调用notifyPropertyChanged通知数据变更。

public class ObservableUser extends BaseObservable {
    public String firstName;
    public String lastName;

    public ObservableUser(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    @Bindable
    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
        notifyPropertyChanged(BR.firstName);
    }

    @Bindable
    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
        notifyPropertyChanged(BR.lastName);
    }
}

Bindable注解会在编译时在BR中生成一个entry,当数据变化时调用notifyPropertyChanged通知这个entry数据发生了变化。

ObservableFields

创建Observable类还是比较麻烦的,data binding为我们提供了一个便捷的ObservableField类以及它的派生类:
ObservableBoolean, ObservableByte, ObservableChar, ObservableShort, ObservableInt, ObservableLong, ObservableFloat, ObservableDouble, and ObservableParcelable.

ObservableFields是包含了一个单一属性的observable objects,可以通过声明一个public final field来使用它:

public class ObservableFieldUser extends BaseObservable {
    public final ObservableField<String> firstName = new ObservableField<>();
    public final ObservableField<String> lastName = new ObservableField<>();

    public ObservableFieldUser(String firstName, String lastName) {
        this.firstName.set(firstName);
        this.lastName.set(lastName);
    }
}

然后可以用set/get来存取数据:

user.firstName.set("bai");
String s = user.firstName.get();

Observable Collections

有些应用希望使用更加灵活的结构来管理数据,Observable集合类允许使用key来访问这些数据对象。

ObservableMap<String, String> userMap = new ObservableArrayMap<>();
userMap.put("firstName", "bai");
userMap.put("lastName", "li");
binding.setUserMap(userMap);

然后在布局文件中用String keys获取map中的数据:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <import type="android.databinding.ObservableMap" />
        <variable
            name="userMap"
            type="ObservableMap<String,String>" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{`obserable map : ` + userMap[`firstName`] + ` ` + userMap[`lastName`]}" />
    </LinearLayout>
</layout>
ObservableList<User> useList = new ObservableArrayList<>();
useList.add(new User("bai", "li"));
binding.setUserList(useList);

然后在布局文件中使用下标获取list中的数据:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <import type="com.dragonjiang.databindingdemo.model.User" />
        <import type="android.databinding.ObservableList" />
        <variable
            name="userList"
            type="ObservableList<User>" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{`obserable list : ` + userList[0].toString()}" />
    </LinearLayout>
</layout>

生成绑定

自动生成的Binding类都继承了ViewDataBinding类,它们是连接layout的variables和Views的桥梁。

Creating

binding在View inflate之后创建。inflate方法会将Veiw绑定到binding上,对于不同的Veiw有不同的创建方法:

MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater);
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater, viewGroup, false);

如果布局使用不同的机制inflate,可以单独绑定:

MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);

有时候绑定不能提前确定,例如ListView的Item layout,这时候可以使用DataBindingUtil类:

ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId,
    parent, attachToParent);
ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId);

有ID的View

我们在之前的例子里面都没有给View声明一个id,因为用不到。但是如果有些情况下,我要调用到布局里面的特定的View,还是需要一个id。data binding提供了一个比findViewById更快的机制:

<TextView
    android:id="@+id/tv_name"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{user.firstName + ` ` + user.lastName}" />

data binding会在binding类中自动生成对应的属性:

public final TextView tvName;

可以直接使用:

binding.tvName.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Toast.makeText(LayoutDetailsActivity.this, binding.tvName.getText(), Toast.LENGTH_SHORT).show();
    }
});

ViewStubs

ViewStub不同于正常的View,它一开始是不可见的,在需要时才加载出特定的布局。所以data binding提供了一个ViewStubProxy类来代替ViewStub,开发者可以通过这个类来操作ViewStub

ViewStub需要在inflate时候创建一个binding,故需要设置监听ViewStub.OnInflateLister

public class ViewStubActivity extends AppCompatActivity {

    private ActivityViewStubBinding mBinding;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding = DataBindingUtil.setContentView(this, R.layout.activity_view_stub);
        mBinding.viewStub.setOnInflateListener(new ViewStub.OnInflateListener() {
            @Override
            public void onInflate(ViewStub stub, View inflated) {
                IncludeBinding binding = DataBindingUtil.bind(inflated);
                binding.setUser(new User("bai", "li"));
            }
        });
    }

    public void onClick(View view) {
        if (!mBinding.viewStub.isInflated()) {
            mBinding.viewStub.getViewStub().inflate();
        }
    }
}
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data></data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="onClick"
            android:text="inflate view_stub" />

        <ViewStub
            android:id="@+id/view_stub"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout="@layout/include" />
    </LinearLayout>
</layout>

高级绑定

有些情况下,例如RecyclerView.Adapter中我们无法事先知道binding类。需要在onBindViewHodler(VH, int)中给binding赋值。
在这种情况下,RecyclerView布局内都设置了一个item变量,可以通过getBinding方法返回一个ViewDataBinding类:

public void onBindViewHolder(VH holder, int position) {
    holder.binding.setModel(mDataList.get(position));
    holder.binding.executePendingBindings();
}

注意到上面executePendingBindings()表示立即绑定。如果没有指定立即执行,在数据变化时,binding会在下一帧开始前触发。

属性设置

当绑定的数据变化时,自动生成的binding类会寻找对应属性的setter方法。data binding框架设置了几种自定义赋值的机制。

自动Setter

对于一个属性,data binding 尝试找到对应的setter方法,例如我们自定义了一个UserView类,实现一个setUser方法:

public class UserView extends AppCompatTextView {
    public UserView(Context context) {
        super(context);
    }

    public UserView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public UserView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public void setUser(User user) {
        this.setText(user.toString());
    }
}

在布局文件中使用:

<com.dragonjiang.databindingdemo.ui.UserView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:user="@{user}" />

data binding自动为我们找到了setUser(User user)的方法。

重命名Setter

有的属性的名称与它的setter不匹配,对于这类属性,可以使用注解BindingMethods将属性与setter关联起来。例如下面这个例子将andorid:tintsetImageTintList关联起来:

@BindingMethods({
       @BindingMethod(type = "android.widget.ImageView",
                      attribute = "android:tint",
                      method = "setImageTintList"),
})

自定义Setter(Binding Adapter)

有些属性需要自定义属性设置逻辑,例如没有android:paddingLeft属性对应的setter方法。但是有setPadding(left, top, right, bottom)。一个用BindingAdapter注解的静态方法允许开发者自定义setter:

@android.databinding.BindingAdapter("android:paddingLeft")
    public static void setPaddingLeft(View view, int padding) {
        view.setPadding(padding,
                view.getPaddingTop(),
                view.getPaddingRight(),
                view.getPaddingBottom());
    }

BindingAdapter的方法还可以获取旧的值。只需将旧的值放前面,新的值放后面:

@android.databinding.BindingAdapter("android:paddingLeft")
    public static void setPaddingLeft(View view, int oldPadding, int padding) {
        if (oldPadding != padding) {
            view.setPadding(padding,
                    view.getPaddingTop(),
                    view.getPaddingRight(),
                    view.getPaddingBottom());
        }
    }

BindingAdapter很强大,尤其对自定义属性。比如可以用来异步加载图片:

@android.databinding.BindingAdapter({"imageUrl", "error"})
    public static void loadImage(ImageView view, String url, Drawable error) {
        Glide.with(view.getContext()).load(url).error(error).into(view);
    }
<ImageView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:error="@{@drawable/ic_launcher}"
    app:imageUrl="@{user.avatar}" />

imageUrlerror属性被使用时,就会匹配调用BindindAdapter的loadImage方法。

转换器

对象转换

如果binding表达式返回一个对象,data binding会寻找对应的setter(自动setter、重命名setter、自定义setter),然后将返回的对象强制转换成setter需要的类型。
这是一个使用ObservableMap的例子:

<TextView
   android:text='@{userMap["lastName"]}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

userMap返回一个对象,这个对象会被自动转换为setText(CharSequence)需要的类型。如果类型转换有问题,开发者需要受到进行类型转换。

自定义转换

有时候需要对一些特定的类型直接做转换,例如设置背景:

<com.dragonjiang.databindingdemo.ui.UserView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@{user.isAdult ? @color/colorAccent : @color/colorPrimary}"
    app:user="@{user}" />

这里background需要Drawable类型,而color是int类型,此时需要一个BindingConversation将int转为ColorDrawable:

@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
   return new ColorDrawable(color);
}

注意:转换只能在setter时生效,所以不允许混合类型

<View
   <!--这是不允许的-->
   android:background="@{isError ? @drawable/error : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

参考资料

上一篇 下一篇

猜你喜欢

热点阅读