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

Android DataBinding (一) 基本用法
Android DataBinding (二) 事件處理
Android DataBinding (三) Observable
Android DataBinding (四) 自定義屬性
Android DataBinding (五) 自定義 View 的雙向綁定 (本文)
Android DataBinding (六) EditText 綁定 TextChangedListener 和 FocusChangeListener

前言

自定義 View 的時候如果用到非系統(tǒng)定義的屬性的時候,如果要實(shí)現(xiàn)雙向綁定,不是用了 @= 就行的,自定義 View 中還需要一些設(shè)置。

下面通過一個例子來說明自定義 View 的雙向綁定的實(shí)現(xiàn)。

例子要求:

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

首先自定義 RadioButton 和 RadioGroup

由于愛好是需要定義成 enum 類型的,而 RadioGroup 選擇 RadioButton 的時候是通過 id 來的,所以必須先把 enum 轉(zhuǎn)換成 id 才能夠?qū)崿F(xiàn)綁定。但是我們可以通過自定義 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) {
                // 點(diǎn)擊選中的 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 對應(yīng)的 Integer 值。

enum 的值是通過 DataBinding 綁定進(jìn)來的,所以需要對應(yīng)的 set 方法。

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

原因是不僅需要把值傳進(jìn)來,還需要讓 RadioGroup 知道選中的 RadioButton 是哪一個。RadioGroup 如果設(shè)置 OnCheckedChange 監(jiān)聽的話,radioButton.setChecked 就會通知 RadioGroup 了。

RadioButton 默認(rèn)是必須選擇一個,toggle() 部分代碼是讓 RadioButton 支持什么都不選。因?yàn)槲覀兊囊笫且部梢詻]有愛好。

代碼中 IntegerUtil 是為了比較兩個 Integer 寫的一個 Util 類。問題來了,為什么 value 的值是 Integer 類型的而不是 int 類型的?因?yàn)橹С植贿x擇愛好,所以愛好的值可以為 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 監(jiān)聽事件。
method = "getCheckedValue" 是指定逆向綁定的時候的數(shù)據(jù)來源方法。

event 和 method 都不是必須的,如果不指定,默認(rèn)會以以下規(guī)則自動生成
event = "xxxAttrChanged"
method = "getXxx"

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

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

@BindingAdapter("checkedValueAttrChanged") 是用來指定監(jiān)聽方法的,重點(diǎn)在 InverseBindingListener,它的 onChange 方法是最后通知 ViewModel 值變更的地方(InverseBindingListener 的實(shí)現(xiàn)在生成的類里面,以本例子的話,就是 ActivityMainBinding,下面貼上 InverseBindingListener 的實(shí)現(xiàn))。

    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 方法做的事情設(shè)置到 OnValueChangedListener 里面去。

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

是的,確實(shí)是這樣,上面的代碼確實(shí)可以簡單的這樣做!但是如果 RadioGroup 真的需要設(shè)置 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 狀態(tài)以及執(zhí)行監(jiān)聽的內(nèi)容。
由于會調(diào)用 RadioButton 的 setChecked 方法,然后 init 方法里面又設(shè)置了 setOnCheckedChangeListener,所以 setCheckedValue 方法會再次被調(diào)用,為了防止循環(huán)調(diào)用,以下代碼是必不可少的

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 聯(lián)系起來了。

然后在 RadioGroup 中設(shè)置 app:checkedValue="@={vm.hobby}" 來設(shè)置雙向綁定。

源碼

https://github.com/teletian/AndroidSamples/tree/main/DataBindingRadioButton

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容