11

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?

StayCool
  • 421
  • 1
  • 9
  • 23

2 Answers2

11

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.

Valeriy Katkov
  • 33,616
  • 20
  • 100
  • 123
  • Seems legit, will try your solution and give a feedback. But, why do you setSliderListeners? Do setSliderValue and getSLiderValue is enough for 2-way-databinding? – StayCool Jun 01 '20 at 08:15
  • 1
    @StayCool Ok, hope it helps you! According to the [two-way databinding documentation](https://developer.android.com/topic/libraries/data-binding/two-way#two-way-custom-attrs) a *listeners* adapter should be setup to let the databinding system know that the value is changed. If you provide only a *getter* and a *setter* it would be able to get and set the value but won't be able to determine when the value is changed. – Valeriy Katkov Jun 01 '20 at 08:25
  • I see. Here is my mistake was. You said you had problems with "android:value" attr, and I think that's why my modified SeekBarAdapter failed to work, because I used "android:valueAttrChanged". What is wrong with that value? Why another basic attrs (text, checkedButton, etc) works perfectly? – StayCool Jun 01 '20 at 08:34
  • 1
    @StayCool Properties line `text` work because they have [build-in two-way databinding attributes](https://developer.android.com/topic/libraries/data-binding/two-way). But there are no configured attributes for the material Slider. According to [this](https://github.com/material-components/material-components-android/issues/926) GitHub issue android material components don't proved preconfigured adapters. What is about `android:value`, I guess it conflicts somehow with some of the existing definitions, but don't know the exact reason. – Valeriy Katkov Jun 01 '20 at 08:48
  • I just had no knowledge about why my custom adapter with same attr name (value) could not work. Not it's clear for me that it's some kind of conflict, but still I can't realize why, because "built-in" adapters guys from androidx.databinding write doesn't really differ from which I or you did. – StayCool Jun 01 '20 at 08:56
  • 2
    @StayCool Please look at the updated answer. Finally I figured out how to use the `value` attribute, it allows us to get rid of a custom *setter* adapter. I've also updated the sample repo. If you still have questions, feel free to ask. – Valeriy Katkov Jun 01 '20 at 10:31
  • 2
    Thank you for your time and so detailed answer! But I still have two questions, which are actually above the topic question, so I don't suppose that you will answer : 1. How androix.databinding adapters are using "android" namespace for their attrs names, and why we can't? They are kind of override the value attribute from android namespace? [link](https://android.googlesource.com/platform/frameworks/data-binding/+/master/extensions/baseAdapters/src/main/java/android/databinding/adapters/SeekBarBindingAdapter.java) 2. Is new listener is setup everytime slider fires valueAttrChanged? – StayCool Jun 01 '20 at 12:00
  • 2
    @StayCool Glad to help you. `setSliderListeners()` is called only once per a view while its initialization, you can check it by adding a log to the function. A binding adapter called every time the bound value is changed, but in case of two-way databinding `valueAttrChanged` is initialized only once. What about `android` namespace, its reserved for built-in android attributes, the app specific attributes should use 'app' namespace. I cannot say how this rule is bypassed by androidx libraries, but androidx it's part of android sdk, so I think they know some secrets :) – Valeriy Katkov Jun 01 '20 at 12:44
  • @StayCool Finally I ended up using 'android' namespace, it seems ok for an 'android' attribute. I just tried it again and it works fine. I don't know the exact reason why it didn't work when I tried it before. I updated the answer. You can also look at my other [answer](https://stackoverflow.com/a/62130750/4858777) describing how `InverseBindingAdapter` and `BindingAdapter` work together, maybe it will be interesting to you too. – Valeriy Katkov Jun 02 '20 at 14:47
  • Hi @ValeriyKatkov This help so much, thanks a lot – Tonnie Jun 02 '21 at 04:51
  • @Valerity `private val _count = MutableLiveData() val count: LiveData get() = _count` I need to expose the slider value as LiveData in my XML but the app is crashing. Any leads around this? – Tonnie Jun 02 '21 at 04:53
0

Update Android Java

Data binding

<variable
        name="device"
        type=".....Device" />

Trong file binding

@InverseBindingAdapter(attribute = "android:value")
public Float getSlider(Slider slider) {
    return slider.getValue();
}

@BindingAdapter("app:valuesAttrChanged")
public void setSliderListeners(Slider slider, InverseBindingListener attrChange) {
    slider.addOnChangeListener(new Slider.OnChangeListener() {
        @Override
        public void onValueChange(@NonNull Slider slider, float value, boolean fromUser) {
            attrChange.onChange();
        }
    });
}

Trong file xml thêm dòng

android:value="@{device.data}"

data is value change

Trang Đỗ
  • 160
  • 1
  • 9