Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 250
StayCool Avatar asked May 26 '20 16:05

StayCool


People also ask

What are binding adapters?

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.

How do you make a BindingAdapter?

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.

How do you create a binding class?

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 .


1 Answers

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.

like image 93
Valeriy Katkov Avatar answered Oct 10 '22 10:10

Valeriy Katkov