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?
Binding adapters are responsible for making the appropriate framework calls to set values. One example is setting a property value like calling the setText() method. Another example is setting an event listener like calling the setOnClickListener() method.
To create a custom binding adapter, you need to create an extension function of the view that will use the adapter. Then, you add the @BindingAdapter annotation. You have to indicate the name of the view attribute that will execute this adapter as a parameter in the annotation.
A binding class is generated for each layout file. By default, the name of the class is based on the name of the layout file, converting it to Pascal case and adding the Binding suffix to it. The above layout filename is activity_main. xml so the corresponding generated class is ActivityMainBinding .
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With