My goal is to 2-way databind material.Slider view to MutableLiveData from my viewmodel:
<com.google.android.material.slider.Slider
...
android:value="@={viewmodel.fps}"
...
/>
Of course, it's not working because there is no databinding adapter for Slider in androidx.databinding library
[databinding] Cannot find a getter for <com.google.android.material.slider.Slider android:value> that accepts parameter type <java.lang.Integer>. If a binding adapter provides the getter, check that the adapter is annotated correctly and that the parameter type matches.
But, they have one for SeekBar: /androidx/databinding/adapters/SeekBarBindingAdapter.java
As I understand, 2-way databinding should work only with "progress" attribute, and 1-way databinding requires two attributes: "onChanged" and "progress"
I made a try to adapt SeekBarBindingAdapter for Slider:
@InverseBindingMethods({
@InverseBindingMethod(type = Slider.class, attribute = "android:value"),
})
public class SliderBindingAdapter {
@BindingAdapter("android:value")
public static void setValue(Slider view, int value) {
if (value != view.getValue()) {
view.setValue(value);
}
}
@BindingAdapter(value = {"android:valueAttrChanged", "android:onValueChange"}, requireAll = false)
public static void setOnSliderChangeListener(Slider view, final Slider.OnChangeListener valChanged, final InverseBindingListener attrChanged) {
if (valChanged == null)
view.addOnChangeListener(null);
else
view.addOnChangeListener((slider, value, fromUser) -> {
if (valChanged != null)
valChanged.onValueChange(slider, value, fromUser);
});
if (attrChanged != null) {
attrChanged.onChange();
}
}
@Override
public void onValueChange(@NonNull Slider slider, float value, boolean fromUser) {
}
It's not building:
Could not find event android:valueAttrChanged on View type Slider
but why it looks for valueAttrChanged if I only use
android:value="@={viewmodel.fps}"
?
How do I find the right attribute to add to BindingAdapter, if I don't see valueAttrChanged in Slider class?
Let's look at SeekBarBindingAdapter's setOnSeekBarChangeListener()
method. It adds four different attributes: {"android:onStartTrackingTouch", "android:onStopTrackingTouch", "android:onProgressChanged", "android:progressAttrChanged"}
but only the last one is used by two-way databinding.
But why there are four attributes? If you look at SeekBar class, it has setOnSeekBarChangeListener()
method which allows you to set and remove a listener. The problem is that SeekBar can only have one listener, and that listener provides different callbacks: onProgressChanged
, onStartTrackingTouch
and onStopTrackingTouch
.
SeekBarBindingAdapter
registers its own listener which means that no one can register another listener without removing the existing one. It's why SeekBarBindingAdapter
provides onStartTrackingTouch
, onStopTrackingTouch
and onProgressChanged
attributes, so you can listen to these events without registering your own OnSeekBarChangeListener
.
Actually the Slider
adapter can be much simpler than SeekBarBindingAdapter
, because the Slider allows you to add and remove listeners using addOnChangeListener()
and removeOnChangeListener()
. So a two-way databinding adapter can register its own listener and anyone else can register other listeners without removing previous ones.
It allows us to define a pretty concise adapter. I created a kotlin example, hope you can translate it to java:
@InverseBindingAdapter(attribute = "android:value")
fun getSliderValue(slider: Slider) = slider.value
@BindingAdapter("android:valueAttrChanged")
fun setSliderListeners(slider: Slider, attrChange: InverseBindingListener) {
slider.addOnChangeListener { _, _, _ ->
attrChange.onChange()
}
}
And the layout:
...
<com.google.android.material.slider.Slider
...
android:value="@={model.count}" />
...
You can find the full sources here.