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 ?
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>
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.
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()
}
}
Since all answers are now deprecated, I'll give y'all a new option.
class DummyViewModel : ViewModel() {
var estadoNestedSV:Int?=null
}
override fun onStop() {
try {
super.onStop()
viewModel.estadoNestedSV = binding.nestedSV.scrollY
} catch (e: Exception) {
Log.i((activity as MainActivity).constantes.TAG_GENERAL, e.message!!)
}
}
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!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With