Android DataBinding (五) 自定义 View 的双向绑定

Android DataBinding (一) 基本用法
Android DataBinding (二) 事件处理
Android DataBinding (三) Observable
Android DataBinding (四) 自定义属性
Android DataBinding (五) 自定义 View 的双向绑定 (本文)
Android DataBinding (六) EditText 绑定 TextChangedListener 和 FocusChangeListener

前言

自定义 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}" 来设置双向绑定。

源码

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容