9

My application revolves around a HomeActivity which contains 4 tabs at the bottom. Each of these tabs is a fragment, all of them are added (not replaced) from the start, and they are hidden/shown upon tapping the appropriate tab.

My problem is that whenever I change tab, the state of my scroll is lost. Each fragment which exhibits that issue uses a android.support.v4.widget.NestedScrollView (see below for an example).

Note: My fragments that use a RecyclerView or ListView keep their scroll state for some reason.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <include layout="@layout/include_appbar_title" />

    <android.support.v4.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <!-- Content -->

    </android.support.v4.widget.NestedScrollView>

</LinearLayout>

I read several posts regarding saving the instance state (this one, that one for example), and their solution either don't work in my scenario, or are not practical to implement given I have 4-12 different fragments I'd need to modify to make it work.

What is the best way to have a Nested Scroll View keep its scroll position on fragment changes ?

Julian Honma
  • 1,124
  • 11
  • 18

4 Answers4

47

One solution I found on inthecheesefactory is that fragments, by default, have their state saved (from the input in a EditText, to the scroll position), but ONLY if an ID is given to the xml element.

In my case, just adding an ID to my NestedScrollView fixed the problem:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <include layout="@layout/include_appbar_title" />

    <android.support.v4.widget.NestedScrollView
        android:id="@+id/NestedScrollView"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <!-- Content -->

    </android.support.v4.widget.NestedScrollView>

</LinearLayout>
Julian Honma
  • 1,124
  • 11
  • 18
0

You can manage the instance state (which includes the scroll state) by yourself by first making the corresponding methods public:

class SaveScrollNestedScrollViewer : NestedScrollView {
    constructor(context: Context) : super(context)

    constructor(context: Context, attributes: AttributeSet) : super(context, attributes)

    constructor(context: Context, attributes: AttributeSet, defStyleAttr: Int) : super(context, attributes, defStyleAttr)


    public override fun onSaveInstanceState(): Parcelable? {
        return super.onSaveInstanceState()
    }

    public override fun onRestoreInstanceState(state: Parcelable?) {
        super.onRestoreInstanceState(state)
    }
}

Then use it in your view with (YOUR_NAMESPACE is the namespace of the SaveScrollNestedScrollViewer class):

<YOUR_NAMESPACE.SaveScrollNestedScrollViewer
     android:id="@+id/my_scroll_viewer"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
</YOUR_NAMESPACE.SaveScrollNestedScrollViewer>

and then in the activity which displays it, save / recover the state as needed. For example, if you want to recover the scroll position after navigating away use the following:

class MyActivity : AppCompatActivity() {

    companion object {
        var myScrollViewerInstanceState: Parcelable? = null
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.my_activity)

        if (myScrollViewerInstanceState != null) {
            my_scroll_viewer.onRestoreInstanceState(myScrollViewerInstanceState)
        }
    }

    public override fun onPause() {
        super.onPause()
        myScrollViewerInstanceState = my_scroll_viewer.onSaveInstanceState()
    }
}
Florian Moser
  • 2,147
  • 1
  • 24
  • 30
  • Hi, I tried implementing this on a fragment but it didn't work, can you help? – Aldan Jul 28 '20 at 02:28
  • I do not know if this approach should work on fragments too or not. I would recommend looking for an example implementing this with fragments or consider asking a new question. – Florian Moser Jul 29 '20 at 09:30
0

Since all answers are now deprecated, I'll give y'all a new option.

  1. Create a variable to hold the nested scroll view on your view model:
class DummyViewModel : ViewModel() {
var estadoNestedSV:Int?=null
}
  1. Override onStop on your Fragment to save the state before the nested scroll view gets destroyed:
override fun onStop() {
        try {
            super.onStop()
            viewModel.estadoNestedSV = binding.nestedSV.scrollY
        } catch (e: Exception) {
            Log.i((activity as MainActivity).constantes.TAG_GENERAL, e.message!!)
        }
    }
  1. Restore the state after the view is created on your fragment by overriding onViewCreated:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        try {
           //Check first if data exists to know if this load is a first time or if the device was rotated.
           if(viewModel.data.value != null)
           binding.nestedSVPelisDetalles.scrollY = viewModel.estadoNestedSV!!
            } catch (e: Exception) {
            Log.i((activity as MainActivity).constantes.TAG_GENERAL, e.message!!)
            }
    }

Happy coding!

Ramiro G.M.
  • 134
  • 1
  • 6
0

Looking at the implementation of the NestedScrollView, we see that the scrollY property of the NestedScrollView is stored in its SavedState as its saved scroll position.

// Source: NestedScrollView.java

@Override
protected Parcelable onSaveInstanceState() {
    Parcelable superState = super.onSaveInstanceState();
    SavedState ss = new SavedState(superState);
    ss.scrollPosition = getScrollY();
    return ss;
}

Therefore I do agree with Ramiro G.M. in terms of the idea of retaining scroll position across configuration changes. I do not think a sub-class of NestedScrollView is necessary in this case.

If you are using a Fragment and MVVM, then I would save the scroll position of the NestedScrollView to my ViewModel in the fragments onViewDestroyed method. You can later observe the state via a LiveData object when the fragments view has been created.

override fun onViewCreated(...) {
    mViewModel.scrollState.observe(viewLifecycleOwner, { scrollState ->
         binding.myNestedScrollView.scrollY = scrollState
    })
}

override fun onDestroyView() {
    val scrollState = binding.myNestedScrollView.scrollY
    mViewModel.setScrollState(scrollState)
    super.onDestroyView()
}

This is just a simple example but the concept holds true.