14

Is it possible to use data-binding for a SeekBar or ProgressBar in Android? I have this data element:

<data>
  <variable
     name="foo"
     type="com.example.Foo" />
</data>

If I refer to the variable foo.bar (which is an int) in a TextField, it works as expected:

<TextView
  android:text="@{String.valueOf(foo.bar)}"
  [...]

What I tried to do was this:

<SeekBar
  android:progress="@{foo.bar}"
  [...]

but it wasn't recognized. (As soon as I wrote an "@" between the quotes, it got red in the editor). Is there another way of data-binding it?

GoRoS
  • 5,183
  • 2
  • 43
  • 66
prograde
  • 2,620
  • 2
  • 23
  • 32
  • 1
    Hmm it should work. It might be an IDE highlight bug. Did you try compiling? – yigit Jul 12 '15 at 05:42
  • I learnt today, that I should always try compiling :) It works! Thanks, yigit! – prograde Jul 12 '15 at 23:35
  • But, hmm, it does call the foo.getBar() method to set itself in the right position. But it never seems to call the foo.setBar() method, so the value is never updated in my code. Does it still need a SeekBarChangeListener, or what? – prograde Jul 12 '15 at 23:53
  • 1
    We don't have two way data binding. It is tricky to get right and partially costly and we are trying to keep data binding as close as possible to the code you would write (+ optimizations). We may add two way binding after v1. – yigit Jul 13 '15 at 04:19
  • 1
    RC1 was just released and while you don't have two-way data binding, you can now easily bind to handler methods. android:onProgressChanged="@{handlers.progressChanged}" where your handlers class has a progressChanged method with the same parameters and return value of onProgressChanged. – George Mount Jul 20 '15 at 21:34

4 Answers4

18

To get the TextView to update when the SeekBar changes, bind to progress change through the android:onProgressChanged attribute. This expects a public method in the Model with the signature of SeekBarBindingAdapter.OnProgressChanged:

View

<TextView
    android:text="{@model.seekBarValue} />

<SeekBar
    android:onProgressChanged="@{model.onValueChanged}" />

Model

public class Model {
    public ObservableField<String> seekBarValue = new ObservableField<>("");

    public void onValueChanged(SeekBar seekBar, int progresValue, boolean fromUser) {
        seekBarValue.set(progresValue + "");
    }
}

In general I advice you to look at the source code for all the Adapter classes made for Data Binding. If you for instance navigate to the file SeekBarBindingAdapter.java in Android Studio (menu Navigate>File) you'll learn all the event methods you can bind to through the adapters there. This particular feature is available because Google have implemented the following adapter:

@BindingAdapter("android:onProgressChanged")
public static void setListener(SeekBar view, OnProgressChanged listener) {
    setListener(view, null, null, listener);
}
Nilzor
  • 18,082
  • 22
  • 100
  • 167
5

In case it helps someone, this question comes up under search a lot, this can now be done using two-way databinding. I use two bindable values below, I tend to prefer no display logic in the XML itself, but I suspect it can work with just one as well.

Create the bindable values in your ViewModel, one for the SeekBar (seekValue) and one for the TextView (seekDisplay)

int mSeekValue;
int mSeekDisplay;

@Bindable
public int getSeekValue() {
    return mSeekValue;
}

public void setSeekValue(int progress) {
    mSeekValue = progress;
    notifyPropertyChanged(BR.seekValue);
    setSeekDisplay(progress);
}

@Bindable
public String getSeekDisplay() {
    return Integer.toString(mSeekDisplay);
}

public void setSeekDisplay(int progress) {
    mSeekDisplay = progress;
    notifyPropertyChanged(BR.seekDisplay);
}

Now you can bind these to the Widgets.

<SeekBar
    android:id="@+id/my_seek"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:progress="@={viewModel.seekValue}" />
<TextView
    android:id="@+id/my_text"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@{viewModel.seekDisplay}" />

The main point to note is that you are using two-way databinding on the SeekBar using @={viewModel.seekValue}, which will call your setSeekValue(...) method.

One this is called, it will update your TextView by calling setSeekDisplay(...)

Kirk
  • 16,182
  • 20
  • 80
  • 112
  • +1 for the @={} notation. You could also get away with the single property seekValue and using @{Integer.toString(viewModel.seekValue)} on the TextView. – josmith42 Aug 22 '17 at 19:26
2

@George Mount is correct, you have to add a handler in your layout xml which is defined in your Model or Handler class (whatever you call it).

Look at my answer for this question for a full fledged example:

Two way databinding with Android Databinding Library

Here's the example from that answer:

Example:

public class AmanteEditModel extends BaseObservable {

    private String senhaConfirm;

    @Bindable
    public String getSenhaConfirm() {
        return senhaConfirm;
    }

    public void setSenhaConfirm(String senhaConfirm) {
        this.senhaConfirm = senhaConfirm;
        notifyPropertyChanged(BR.senhaConfirm);
    }

    // Textwatcher Reference: http://developer.android.com/reference/android/text/TextWatcher.html
    public TextWatcher getMyEditTextWatcher() {
        return new TextWatcher() {

            public void afterTextChanged(Editable s) {
            }

            public void beforeTextChanged(CharSequence s, int start,
                                          int count, int after) {
            }

            public void onTextChanged(CharSequence s, int start,
                                  int before, int count) {
                // Important! Use the property setter, otherwhise the model won't be informed about the change.
                setSenhaConfirm(s);
            }
        };
    }

}

In your layout xml change EditText to this:

<EditText
    android:id="@+id/amante_edit_senha_confirm"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="center_vertical"
    android:hint="Confirme a senha"
    android:inputType="textPassword"
    android:maxLines="1"
    android:text="@{model.senhaConfirm}"
    app:addTextChangeListener="@{model.myEditTextWatcher}"
    />

Watch for the namespace of addTextChangeListener. This method might not be available through the android: namespace, so I'm using app: here. You may also use bind: to make the binding more clear.

So don't miss to add

xmlns:app="http://schemas.android.com/apk/res-auto"

or

xmlns:bind="http://schemas.android.com/apk/res-auto"

to your XML namespaces.

This solution works for all input controls, custom included, given you provide the correct Listeners in your model.

TextWatcher Reference

Community
  • 1
  • 1
2

Well you may need to use two-way binding like below.

Add in binding utils class

private static final String ANDROID_PROGRESS = "android:progress";

@BindingAdapter(ANDROID_PROGRESS)
public static void setSeekbarProgress(SeekBar seekBar, int progress) {
    try {
        seekBar.setProgress(progress);
    } catch (Resources.NotFoundException nfe) {
        nfe.printStackTrace();
    }
}

@InverseBindingAdapter(attribute = ANDROID_PROGRESS)
public static int getSeekbarProgress(SeekBar seekBar) {
    try {
        return seekBar.getProgress();
    } catch (Resources.NotFoundException nfe) {
        nfe.printStackTrace();
        return 0;
    }
}

Add the observable in view model

var child1Age = ObservableField(1)

In the layout

 <SeekBar
            android:id="@+id/child1_age_sb"
            style="@style/HotelChildAgeSeekBarStyle"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/spacing_14"
            android:progress="@={vm.child1Age}"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/childs_age_label" />

        <TextView
            android:layout_marginTop="@dimen/spacing_6"
            android:id="@+id/child1_age_value"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            app:layout_constraintTop_toBottomOf="@+id/child1_age_sb"
            app:layout_constraintLeft_toLeftOf="@id/child1_age_sb"
            app:layout_constraintRight_toRightOf="@id/child1_age_sb"
            android:text="@{String.valueOf(vm.child1Age)}"
            android:textColor="@color/black_medium"
            android:textSize="@dimen/font_14"/>
Shahbaz Hashmi
  • 2,631
  • 2
  • 26
  • 49