18

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,674
  • 14
  • 22

4 Answers4

82

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,674
  • 14
  • 22
2

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.

1

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,583
  • 1
  • 30
  • 40
  • 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
  • Saving myScrollViewerInstanceState to companion object is not a good idea. It is a singleton, which lives outside of the Activity instance. Using singleton is generally a bad approach. – Jakub Kostka Apr 27 '23 at 08:46
1

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.
  • 357
  • 4
  • 7
  • Hi, This is working but when I scroll to the end of the scroll view and go to another fragment and return to the fragment with the scroll view, the scroll position is jumping up. Is there a solution for this? Thanks. – Shafayat Mamun Oct 31 '21 at 09:48
  • 1
    Shafayat, sometimes the scrollY variable takes time to save its state...A better approach could be to save the recyclerview's layout manager entire state (which includes the scroll state), and then restore it back when needed. Just save inside a viewmodel the parcelable obtained from myRecyclerView.LinearLayoutManager.onSaveInstanceState() and restore it with myRecyclerView.LinearLayoutManager.onRestoreInstanceState(Parcelable state) – Ramiro G.M. Nov 01 '21 at 02:40