androidkotlindata-bindingsliderandroid-binding-adapter

How to create Binding Adapter for material.Slider view?


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?


Solution

  • 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.