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)。
例子要求:
- 通過 RadioButton 來選擇愛好(愛好的選項是:吃飯 / 睡覺 / 打豆豆)
- 畫面加載的時候顯示初始的愛好值(將 ViewModel 里設(shè)好的值傳到 RadioButton 上)
- RadioButton 選擇的時候把值傳到 ViewModel 中去
- 可以將 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