1. 程式人生 > >Android DataBinding (五) 自定義 View 的雙向繫結

Android DataBinding (五) 自定義 View 的雙向繫結

前言

自定義 View 的時候如果用到非系統定義的屬性的時候,如果要實現雙向繫結,不是用了 @= 就行的,自定義 View 中還需要一些設定。

下面通過一個例子來說明自定義 View 的雙向繫結的實現。

例子要求:
1. 通過 RadioButton 來選擇愛好(愛好的選項是:吃飯 / 睡覺 / 打豆豆)
2. 畫面載入的時候顯示初始的愛好值(將 ViewModel 裡設好的值傳到 RadioButton 上)
3. RadioButton 選擇的時候把值傳到 ViewModel 中去
4. 可以將 RadioButton 的值清空,也就是說可以沒有愛好

首先自定義 RadioButton 和 RadioGroup

由於愛好是需要定義成 enum 型別的,而 RadioGroup 選擇 RadioButton 的時候是通過 id 來的,所以必須先把 enum 轉換成 id 才能夠實現繫結。但是我們可以通過自定義 RadioButton 和 RadioGroup 來讓他們支援 enum 繫結!

先來看自定義 RadioButton 的程式碼

public class DataBindingRadioButton extends AppCompatRadioButton {

    private Integer value;

    public DataBindingRadioButton
(Context context) { super(context); } public DataBindingRadioButton(Context context, AttributeSet attrs) { super(context, attrs); } public DataBindingRadioButton(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public
Integer getValue() { return value; } public void setValue(Integer value) { this.value = value; } @Override public void toggle() { if (isChecked()) { if (getParent() instanceof RadioGroup) { // 點選選中的 RadioButton,可以取消選擇 ((RadioGroup) getParent()).clearCheck(); } } else { setChecked(true); } } @BindingAdapter(value = {"value"}) public static void setValue(DataBindingRadioButton radioButton, Integer value) { radioButton.setValue(value); ViewParent parent = radioButton.getParent(); if (parent instanceof DataBindingRadioGroup) { Integer checkedValue = ((DataBindingRadioGroup) parent).getCheckedValue(); radioButton.setChecked(IntegerUtil.isSame(checkedValue, value)); } } }

我們給 DataBindingRadioButton 定義了一個屬性 value,value 的值就是 enum 對應的 Integer 值。

enum 的值是通過 DataBinding 繫結進來的,所以需要對應的 set 方法。

我們沒有直接用 setValue(Integer value),而是通過 @BindingAdapter
用了另外一個帶有引數 DataBindingRadioButton 的 set 方法。

原因是不僅需要把值傳進來,還需要讓 RadioGroup 知道選中的 RadioButton 是哪一個。RadioGroup 如果設定 OnCheckedChange 監聽的話,radioButton.setChecked 就會通知 RadioGroup 了。

RadioButton 預設是必須選擇一個,toggle() 部分程式碼是讓 RadioButton 支援什麼都不選。因為我們的要求是也可以沒有愛好。

程式碼中 IntegerUtil 是為了比較兩個 Integer 寫的一個 Util 類。問題來了,為什麼 value 的值是 Integer 型別的而不是 int 型別的?因為支援不選擇愛好,所以愛好的值可以為 null,所以需要定義成 Integer 型別的。

下面是自定義 RadioGroup 的程式碼

@InverseBindingMethods({
        @InverseBindingMethod(
                type = DataBindingRadioGroup.class,
                attribute = "checkedValue",
                event = "checkedValueAttrChanged",
                method = "getCheckedValue")
})
public class DataBindingRadioGroup extends RadioGroup {

    private Integer checkedValue;
    private OnValueChangedListener listener;

    public DataBindingRadioGroup(Context context) {
        super(context);
        init();
    }

    public DataBindingRadioGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public void init() {
        setOnCheckedChangeListener((group, checkedId) -> {
            if (checkedId > 0) {
                DataBindingRadioButton radioButton = (DataBindingRadioButton) findViewById(checkedId);
                setCheckedValue(radioButton.isChecked() ? radioButton.getValue() : null);
            } else {
                setCheckedValue(null);
            }
        });
    }

    public Integer getCheckedValue() {
        return checkedValue;
    }

    public void setCheckedValue(Integer checkedValue) {

        if (IntegerUtil.isSame(this.checkedValue, checkedValue)) {
            return;
        }

        this.checkedValue = checkedValue;

        if (this.checkedValue == null) {
            clearCheck();
        } else {
            DataBindingRadioButton customRadioButton = (DataBindingRadioButton) findViewById(getCheckedRadioButtonId());
            if (customRadioButton == null || !IntegerUtil.isSame(this.checkedValue, customRadioButton.getValue())) {

                for (int i = 0; i < getChildCount(); i++) {
                    View child = getChildAt(i);
                    if (child instanceof DataBindingRadioButton) {
                        Integer value = ((DataBindingRadioButton) child).getValue();
                        if (IntegerUtil.isSame(this.checkedValue, value)) {
                            ((DataBindingRadioButton) child).setChecked(true);
                        }
                    }
                }
            }
        }

        if (listener != null) {
            listener.onValueChanged();
        }
    }

    public void setListener(OnValueChangedListener listener) {
        this.listener = listener;
    }

    public interface OnValueChangedListener {
        void onValueChanged();
    }

    @BindingAdapter("checkedValueAttrChanged")
    public static void setValueChangedListener(DataBindingRadioGroup view, final InverseBindingListener bindingListener) {
        if (bindingListener == null) {
            view.setListener(null);
        } else {
            // 通知 ViewModel
            view.setListener(bindingListener::onChange);
        }
    }
}

要支援逆向繫結,首先要在類名上定義 @InverseBindingMethods。
attribute = “checkedValue” 是指定支援逆向繫結的屬性。
event = “checkedValueAttrChanged” 是指定 valueChanged 監聽事件。
method = “getCheckedValue” 是指定逆向繫結的時候的資料來源方法。

event 和 method 都不是必須的,如果不指定,預設會以以下規則自動生成
event = “xxxAttrChanged”
method = “getXxx”

method 的定義還可以直接在方法上面

@InverseBindingAdapter(attribute = "checkedValue", event = "checkedValueAttrChanged")
public Integer getCheckedValue() {
    return checkedValue;
}

@BindingAdapter(“checkedValueAttrChanged”) 是用來指定監聽方法的,重點在 InverseBindingListener,它的 onChange 方法是最後通知 ViewModel 值變更的地方(InverseBindingListener 的實現在生成的類裡面,以本例子的話,就是 ActivityMainBinding,下面貼上 InverseBindingListener 的實現)。

    private android.databinding.InverseBindingListener mboundView1checkedValueAttrChanged = new android.databinding.InverseBindingListener() {
        @Override
        public void onChange() {
            // Inverse of vm.hobby
            //         is vm.setHobby((java.lang.Integer) callbackArg_0)
            // 這裡就是 method = "getCheckedValue" 指定的方法
            java.lang.Integer callbackArg_0 = mboundView1.getCheckedValue();
            // localize variables for thread safety
            // vm != null
            boolean vmJavaLangObjectNull = false;
            // vm
            com.teletian.databindingradiobutton.viewmodel.ViewModel vm = mVm;
            // vm.hobby
            java.lang.Integer vmHobby = null;

            vmJavaLangObjectNull = (vm) != (null);
            if (vmJavaLangObjectNull) {
                // 這裡就是修改 ViewModel 的值
                vm.setHobby(((java.lang.Integer) (callbackArg_0)));
            }
        }
    };

setValueChangedListener 所做的事情就是將 onChange 方法做的事情設定到 OnValueChangedListener 裡面去。

也許你會問,為什麼要這麼麻煩,我直接定義一個 InverseBindingListener 的屬性直接賦值給它不就 OK 了!

是的,確實是這樣,上面的程式碼確實可以簡單的這樣做!但是如果 RadioGroup 真的需要設定 OnValueChangedListener,那麼就不能這樣了!程式碼需要改成下面這樣

    @BindingAdapter(value = {"onCheckedValueChanged", "checkedValueAttrChanged"}, requireAll = false)
    public static void setValueChangedListener(DataBindingRadioGroup view,
                                               final OnValueChangedListener valueChangedListener,
                                               final InverseBindingListener bindingListener) {
        if (bindingListener == null) {
            view.setListener(valueChangedListener);
        } else {
            view.setListener(() -> {
                if (valueChangedListener != null) {
                    valueChangedListener.onValueChanged();
                }
                // 通知 ViewModel
                bindingListener.onChange();
            });
        }
    }

setCheckedValue 方法裡面做的事情就是,控制 RadioButton 的 Check 狀態以及執行監聽的內容。
由於會呼叫 RadioButton 的 setChecked 方法,然後 init 方法裡面又設定了 setOnCheckedChangeListener,所以 setCheckedValue 方法會再次被呼叫,為了防止迴圈呼叫,以下程式碼是必不可少的

if (IntegerUtil.isSame(this.checkedValue, checkedValue)) {
    return;
}

RadioGroup 和 RadioButton 都自定義完了,下面來看看 Layout 檔案

         <com.teletian.databindingradiobutton.customview.DataBindingRadioGroup
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:checkedValue="@={vm.hobby}">

            <com.teletian.databindingradiobutton.customview.DataBindingRadioButton
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="吃飯"
                app:value="@{Hobby.EATING.value}" />

            <com.teletian.databindingradiobutton.customview.DataBindingRadioButton
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="睡覺"
                app:value="@{Hobby.SLEEPING.value}" />

            <com.teletian.databindingradiobutton.customview.DataBindingRadioButton
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="打豆豆"
                app:value="@{Hobby.ATTACKING_DOUDOU.value}" />

        </com.teletian.databindingradiobutton.customview.DataBindingRadioGroup>

首先 RadioButton 的值是通過 app:value=”@{Hobby.EATING.value}” 指定的,這樣就把 enum 的值 和 RadioButton 聯絡起來了。

然後在 RadioGroup 中設定 app:checkedValue=”@={vm.hobby}” 來設定雙向繫結。

原始碼