保存NestedScrollView的滚动状态

19

我的应用程序围绕着一个HomeActivity,底部有4个选项卡。每个选项卡都是一个片段,它们从一开始就都被添加(而不是替换),并且在点击适当的选项卡时隐藏/显示。

我的问题是,每当我切换选项卡时,我的滚动状态就会丢失。出现此问题的每个片段都使用了android.support.v4.widget.NestedScrollView(请参见下面的示例)。

注意:对于使用RecyclerView或ListView的片段,它们保留其滚动状态,原因未知。

<?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>

我阅读了几篇关于保存实例状态的文章(例如这篇那篇),但它们的解决方案要么在我的情况下不起作用,要么不实用,因为我需要修改4-12个不同的片段才能使其工作。

如何让嵌套滚动视图保持其滚动位置,使其在片段更改时保持原样?

4个回答

85

我在inthecheesefactory上找到的一个解决方案是,默认情况下,片段的状态会被保存(从EditText中的输入到滚动位置),但仅当xml元素被赋予ID时才会保存。

在我的情况下,只需向我的NestedScrollView添加一个ID即可解决问题:

<?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>

16
你救了我的晚上 :) - ievgen
3
你好,这个功能能够与“导航组件”一起使用吗?因为我已经尝试过了,但是似乎并没有生效,界面总是会回到页面顶部。 - Aldan
2
它确实可以在 Navigation 组件2.4.0 版本中工作。 - Amr
Id必须是唯一的。刚遇到一个问题,这个解决方案没有起作用,因为我将其命名为“nested_scroll”,但是该名称被遮蔽了,所以它无法工作。 - Jakub Kostka
你是一个英雄! - ahmetnecdeto
显示剩余5条评论

2
查看NestedScrollView的实现,我们可以看到NestedScrollView的scrollY属性被存储在其SavedState中作为其保存的滚动位置。
// Source: NestedScrollView.java

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

因此,我同意Ramiro G.M.的想法,在配置更改时保留滚动位置。 我认为在这种情况下不需要NestedScrollView的子类。
如果您正在使用Fragment和MVVM,则可以在片段的onViewDestroyed方法中将NestedScrollView的滚动位置保存到ViewModel中。 当片段视图已创建时,您可以稍后通过LiveData对象观察状态。
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()
}

这只是一个简单的例子,但概念是正确的。

1

由于所有答案都已过时,我给大家提供一个新选项。

  1. 在您的视图模型中创建一个变量来保存嵌套滚动视图:
class DummyViewModel : ViewModel() {
var estadoNestedSV:Int?=null
}

2. 在您的Fragment中重写onStop以在嵌套滚动视图被销毁之前保存状态:
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. 通过覆盖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!!)
            }
    }

快乐编程!


嗨,这个工作正常,但是当我滚动到滚动视图的末尾并转到另一个片段,然后返回带有滚动视图的片段时,滚动位置会跳上去。有解决方法吗?谢谢。 - Shafayat Mamun
1
Shafayat,有时scrollY变量需要一些时间来保存其状态...更好的方法是保存recyclerview的布局管理器的整个状态(包括滚动状态),然后在需要时恢复它。只需在viewmodel中保存从myRecyclerView.LinearLayoutManager.onSaveInstanceState()获取的可包含对象,并使用myRecyclerView.LinearLayoutManager.onRestoreInstanceState(Parcelable state)进行恢复即可。 - Ramiro G.M.

1

您可以通过首先将相应的方法公开来自行管理实例状态(包括滚动状态):

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)
    }
}

然后在您的视图中使用它 (YOUR_NAMESPACESaveScrollNestedScrollViewer 类的命名空间):

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

然后在显示它的活动中,根据需要保存/恢复状态。例如,如果您想在导航离开后恢复滚动位置,请使用以下方法:

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()
    }
}

嗨,我尝试在一个片段上实现它,但是它没有起作用,你能帮忙吗? - Aldan
我不确定这种方法是否也适用于片段。我建议寻找一个使用片段实现此方法的示例,或者考虑提出一个新问题。 - Florian Moser
将myScrollViewerInstanceState保存到伴生对象并不是一个好主意。它是一个单例,存在于Activity实例之外。通常使用单例是一种不好的方法。 - Jakub Kostka

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接