Challenge - Trouble binding to SeekBar binding adapter listener method

I was able to complete this challenge, but not using data binding and it’s driving me nuts. In my layout I am using the onProgressChanged attribute inside the seekbar element but when I bind it to a method in my viewmodel or elsewhere I get a “Cannot Resolve Type” error. I’m not sure what I am missing, or how to properly implement the SeekBarBindingAdapter I guess.

    <layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
    <variable
        name="viewModel"
        type="com.bignerdranch.android.beatbox.SoundViewModel"/>

</data>
<FrameLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent">

    </android.support.v7.widget.RecyclerView>
    <SeekBar
        android:id="@+id/bottom_speed_bar"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:layout_gravity="bottom"
        style="@style/Widget.AppCompat.SeekBar"
        android:max="100"
        android:onProgressChanged="@{(seekbar, progress, fromUser)-> viewModel.onSeekBarMove}"
        />
</FrameLayout>
</layout>

Here Is my method that I can’t get to bind, using the same parameters as the onProgressChanged method inside the seekbar listener.

...
 public void onSeekBarMove(SeekBar seekBar, int progress, boolean fromUser){
//Set playback rate
    return;
}

And here is where the attributes are specified in the SeekBarBindingAdapter.java class

...
 @BindingAdapter(value = {"android:onStartTrackingTouch", "android:onStopTrackingTouch",
        "android:onProgressChanged", "android:progressAttrChanged"}
...

EDIT: I am now getting this error after changing the layout to:

    android:onProgressChanged="@{(seekbar, progress, fromuser) -> viewModel.onSeekBarMove(seekbar, progress, fromuser)}"

Debugging data binding has kind of been a nightmare. I got it to compile and run earlier by using he viewModel:onSeekBarMove syntax but the listener wasn’t working.

C:\Users\pstev\Documents\BeatBox\app\build\generated\source\apt\debug\com\bignerdranch\android\beatbox\databinding\FragmentBeatBoxBinding.java:131: error: cannot find symbol
        callbackArg_0.onSeekBarMove(callbackArg_1, callbackArg_2, com.bignerdranch.android.beatbox.SoundViewModel);
                                                                                          ^
symbol:   class beatbox
location: package com.bignerdranch.android
Note: C:\Users\pstev\Documents\BeatBox\app\src\main\java\com\bignerdranch\android\beatbox\BeatBox.java uses          or overrides a deprecated API.
Note: Recompile with -Xlint:deprecation for details.
1 error

Has anyone been able to complete this challenge? Or does anyone have any suggestions binding the seek progress bar?

I’m working on this too. One thing that might help you is that SoundViewModel has individual instances for each sound. So if you’re using the SoundViewModel viewModel for the seek bar, I think you’ll run into problems since there’s only one seek bar and many SoundViewModels floating around.

I am working on this as well. The issue for me, is that I did take a look at the adapter source for seekbar using Navigate->File and saw this:

The problem is that when I am in the layout file and start typing, the only prompt that Android Studio gives me is for the “Progress” item:

The ‘onProgressChanged’ value is not listed, and if I type it in anyway, studio gives me a warning that the attribute is unknown:

Yet I found this example which seems to indicate that this should work:

I wonder if there is a different namespace that has to be used or something?

Update: I went ahead and just tried it - at first I got all kinds of errors because I wasn’t matching the expected parameters correctly in the layout XML. However this works:

        android:onProgressChanged="@{viewModel.onProgressChanged}"

As long as I have the method in my viewModel class correctly written to accept the same parameters that are defined in the SeekBarBindingAdapter file:

public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
    Log.d("BeatBox", "Got progress change of: " + progress);
}

So I took a much simpler route to solve this challenge. I didn’t see any need to create a separate view model class the way we did for the SoundViewModel class for the Button. Why? Because as I understand it, the primary reason for separating these classes out is when we need UI widget manipulation (i.e. View manipulation) that we want to separate out from the controller. In the case of the Button, we wanted to be able to dynamically update the text on the Button as well as manage the Sound associated with the button. But in this case:

a) We aren’t having to do a notify in the case of reacting to the seekbar being changed, the only update is internal to BeatBox itself (updating the playback rate)
b) BeatBox is where the play is done, and it controls the rate at which the sound is played for all buttons, so we don’t need an intermediary file for this - we can just use BeatBox itself.
c) I think if the challenge had been to put a different rate on a per button basis, then a different approach would have been warranted.

So here is my layout file:

<layout
xmlns:android="http://schemas.android.com/apk/res/android">
<data>
    <variable
        name="viewModel"
        type="com.reddragon.beatbox.BeatBox" />
</data>
<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <!-- Recyclerview grabs all the screen real estate itself, so define seekbar first even
         though it is on the bottom -->
    <SeekBar
        android:id="@+id/seekBar"
        style="@style/Widget.AppCompat.SeekBar.Discrete"
        android:layout_alignParentBottom="true"
        android:max="10"
        android:paddingTop="5dp"
        android:paddingBottom="5dp"
        android:progress="5"
        android:onProgressChanged="@{viewModel.onProgressChanged}"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_alignParentTop="true"
        android:layout_above="@id/seekBar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" >
    </android.support.v7.widget.RecyclerView>

</RelativeLayout>

I simply added one method to BeatBox.java:

public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
    if ( fromUser ) {
        // 0-based counting for progress
        mSoundRate = (float)(1.0f +( (float)( progress - 5 )/10));
        Log.d("BeatBox", "Got progress change of: " + progress + ", changed rate to: " + mSoundRate);
    } else {
        Log.d("BeatBox", "Got progress change of: " + progress + " but not from user");
    }

}

And I updated BeatBoxFragment:

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
                         Bundle savedInstanceState) {
    FragmentBeatBoxBinding binding = DataBindingUtil.inflate(inflater, R.layout.fragment_beat_box, container, false);

    binding.recyclerView.setLayoutManager(new GridLayoutManager(getActivity(), 3));
    binding.recyclerView.setAdapter(new SoundAdapter(mBeatBox.getSounds()));
    binding.setViewModel(mBeatBox);

    return binding.getRoot();
}

Thoughts?

As an aside - I had a hard time with various layouts getting the SeekBar to show. I finally settled on the above approach after Googling a bit, but LinearLayout was not working for no matter use of layoutweight, etc. I see some have used FrameLayout - I thought this was now getting a bad rap?

Obvious, but for completion sake note that I also updated the play method to utilize an instance variable that gets updated from onProgressChanged:

public void play(Sound sound) {
    Integer soundId = sound.getSoundId();
    if (soundId == null) {
        return;
    }
    mSoundPool.play(soundId, 1.0f, 1.0f, 1, 0, mSoundRate);
}

I don’t think Beatbox should listen the progress changed of SeekBar. BeatBox is just responsible for play, so it should’t know the playback rate is controlled by SeekBar. I think onProgressChanged should be place in BeatBoxFragment, and then pass it to BeatBox. I hope you can understand what i say with my poor English.

SeekBar should be placed in BeatBoxFragment. BeatBox should not know how it’s playback speed is changed

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:android="http://schemas.android.com/apk/res/android" >

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/linearLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recycler_view"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:paddingVertical="20dp"
            app:layout_constraintBottom_toTopOf="@+id/seek_bar_playback_speed"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="1.0"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/playback_speed_text_view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="32dp"
            android:textColor="@android:color/black"
            android:textSize="14sp"
            app:layout_constraintBottom_toTopOf="@+id/seek_bar_playback_speed"
            app:layout_constraintStart_toStartOf="@+id/recycler_view"
            app:layout_constraintTop_toBottomOf="@+id/recycler_view"
            tools:text="@string/playback_speed" />

        <SeekBar
            android:id="@+id/seek_bar_playback_speed"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:layout_marginTop="16dp"
            android:layout_marginEnd="16dp"
            android:layout_marginBottom="16dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/recycler_view" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>