当 LiveData 更新其内容时,向下滚动 RecyclerView 会跳回到顶部,它应该保持当前的 Y 偏移
Scrolled down RecyclerView jumps back to top when LiveData updates its content, it should keep its current Y offset
我有一个 RecyclerView 可以使用复选框 select 编辑语言列表。它使用 LiveData,因此当用户 selects 或 deselects 来自另一个客户端的语言时,更改会自动反映在所有这些语言中。
每次单击该复选框时,该语言的记录都会在房间数据库和服务器中 selected 和 deselected。用户登录的其他设备也会自动更新,所以我使用的是 LiveData。我与 RecyclerView 错误发生冲突,无论当前滚动位置如何,每次更新时都会将列表推回顶部。
因此,假设我有两部手机打开了语言屏幕,我将它们一直滚动到底部,然后 select 或 deselect 最后一种语言。两个设备上的 RecyclerView 都滚动回顶部,但我希望它保持静止。
我已经看到很多解决此问题的解决方法,但我对 Android 和 Kotlin 还比较陌生,建议的所有补丁都没有得到很好的解释,我无法实施它们。我需要做什么,应该在哪里做?
RecyclerView 在片段中:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/languages_container"
tools:context=".ui.members.languages.LanguagesFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/languages_recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:paddingBottom="15dp"
app:layoutManager="LinearLayoutManager"
app:layout_constraintTop_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:context="com.example.flashcardmallard.ui.members.languages.LanguagesFragment"
tools:listitem="@layout/fragment_languages_cards" />
</FrameLayout>
这是列表项:
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:orientation="horizontal">
<CheckBox
android:id="@+id/language_selection"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
片段(LANGUAGES_SCREEN_VIEWS 是伴随对象中的静态引用,用于我需要在其他 类 中使用的屏幕元素,例如 ViewModel):
class LanguagesFragment : Fragment()
{
override fun onCreateView ( inflater : LayoutInflater, container : ViewGroup?, savedInstanceState : Bundle? ) : View
{
LANGUAGES_SCREEN_VIEWS = FragmentLanguagesBinding.inflate ( inflater, container, false )
val languagesViewModel = ViewModelProvider ( this ).get ( LanguagesViewModel::class.java )
LANGUAGES_FRAGMENT = this
languagesViewModel.getLanguagesForUser().observe ( viewLifecycleOwner, { languages ->
LANGUAGES_SCREEN_VIEWS.languagesRecyclerView.adapter = LanguagesRecyclerViewAdapter ( languages ) } )
return LANGUAGES_SCREEN_VIEWS.root
}
}
这是适配器:
class LanguagesRecyclerViewAdapter ( private val languages: List < LanguagesEntity > ) : RecyclerView.Adapter < LanguagesRecyclerViewAdapter.ViewHolder > ()
{
override fun onCreateViewHolder ( viewGroup: ViewGroup, i: Int ): ViewHolder
{
return ViewHolder ( LayoutInflater.from ( viewGroup.context ).inflate ( R.layout.fragment_languages_cards, viewGroup, false ) )
}
override fun onBindViewHolder ( viewHolder: ViewHolder, index: Int )
{
val language = languages [ index ]
viewHolder.languageCard.text = language.name
viewHolder.languageCard.isChecked = language.spoken
}
override fun getItemCount() = languages.size
inner class ViewHolder ( itemView: View ) : RecyclerView.ViewHolder ( itemView )
{
val languageCard: CheckBox = itemView.findViewById ( R.id.language_selection )
init
{
languageCard.setOnClickListener {
LANGUAGE_RECYCLER_VIEW_STATE = LANGUAGES_SCREEN_VIEWS.languagesRecyclerView.layoutManager!!.onSaveInstanceState()!!
GlobalScope.launch {
// Save to local database
LanguageRepository ().updateLanguage ( languageCard )
// Delete on server, pop up if not possible
var dataSync = DataSyncBean ( languages = arrayListOf ( LanguageBean ( addedOrDeleted = if (
languageCard.isChecked ) ADDED else DELETED, name = languageCard.text.toString() ) ) )
try
{
dataSync = Retrofit.Builder().baseUrl ( ConstantsUtil.BASE_URL ).addConverterFactory ( GsonConverterFactory.create () ).client (
OkHttpClient.Builder().cookieJar ( CookieUtil () ).build() ).build().create (
RestfulCalls::class.java ).updateUserLanguageMapping ( dataSync )
}
catch ( e : Exception )
{
dataSync.syncOutcome = ConstantsUtil.SERVER_UNREACHABLE
withContext ( Dispatchers.Main ) {
val alertDialog : AlertDialog.Builder = AlertDialog.Builder ( LANGUAGES_FRAGMENT.context )
alertDialog.setTitle ( R.string.no_server_connection )
alertDialog.setMessage ( R.string.changes_local_only )
alertDialog.setPositiveButton ( R.string.ok ) { _, _ -> }
val alert: AlertDialog = alertDialog.create()
alert.setCanceledOnTouchOutside ( true )
alert.show()
}
}
}
}
}
}
}
我发现的一些解决方案提到使用 AsyncData,据我所知已弃用,因此我不会考虑这些。我在 DAO 中使用 Flow < List < LanguagesEntity > >
。
添加布局的解决方案无效:
RecyclerView notifyDataSetChanged scrolls to top position
val linearLayoutManager = LinearLayoutManager ( context, RecyclerView.VERTICAL, false )
LANGUAGES_SCREEN_VIEWS.languagesRecyclerView.setLayoutManager(linearLayoutManager)
另一个解决方案说这个错误取决于布局的高度是“包裹内容”并建议固定高度,但是尽管高度数字高得愚蠢,但 RecyclerView 根本没有滚动。
包括建议的解决方法。
此页面提供了一些解决方案:
Refreshing data in RecyclerView and keeping its scroll position
一个人
// Save state
private Parcelable recyclerViewState;
recyclerViewState = recyclerView.getLayoutManager().onSaveInstanceState();
// Restore state
recyclerView.getLayoutManager().onRestoreInstanceState(recyclerViewState);
但我完全不知道将这些行放在我的文件中的什么位置。
我想我应该截取数据更新前的那一刻(它不能点击,因为数据库可以通过从其他设备同步更新)和回收器视图正在更新的那一刻。我不明白这是哪里发生的。
Teo 在这个更具体的问题中给出了解决方案:
这是他的解决方案:
// STEP 1 - 创建一个通知变量的函数
internal fun setLanguage(lang: List<LanguagesEntity>) {
languages = lang
notifyDataSetChanged()
}
// 第 2 步 - 在一切之前设置 recyclerview
val languages = mutableListOf < LanguagesEntity > ()
var languagesAdapter = LanguagesRecyclerViewAdapter(languages)
LANGUAGES_SCREEN_VIEWS.languagesRecyclerView.adapter = languagesAdapter
// 第 3 步 - 为适配器设置新值
languagesViewModel.getLanguagesForUser().observe( requireActivity(), { languages ->
languagesAdapter.setLanguage(language)
} )
我有一个 RecyclerView 可以使用复选框 select 编辑语言列表。它使用 LiveData,因此当用户 selects 或 deselects 来自另一个客户端的语言时,更改会自动反映在所有这些语言中。
每次单击该复选框时,该语言的记录都会在房间数据库和服务器中 selected 和 deselected。用户登录的其他设备也会自动更新,所以我使用的是 LiveData。我与 RecyclerView 错误发生冲突,无论当前滚动位置如何,每次更新时都会将列表推回顶部。
因此,假设我有两部手机打开了语言屏幕,我将它们一直滚动到底部,然后 select 或 deselect 最后一种语言。两个设备上的 RecyclerView 都滚动回顶部,但我希望它保持静止。
我已经看到很多解决此问题的解决方法,但我对 Android 和 Kotlin 还比较陌生,建议的所有补丁都没有得到很好的解释,我无法实施它们。我需要做什么,应该在哪里做?
RecyclerView 在片段中:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/languages_container"
tools:context=".ui.members.languages.LanguagesFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/languages_recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:paddingBottom="15dp"
app:layoutManager="LinearLayoutManager"
app:layout_constraintTop_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:context="com.example.flashcardmallard.ui.members.languages.LanguagesFragment"
tools:listitem="@layout/fragment_languages_cards" />
</FrameLayout>
这是列表项:
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:orientation="horizontal">
<CheckBox
android:id="@+id/language_selection"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
片段(LANGUAGES_SCREEN_VIEWS 是伴随对象中的静态引用,用于我需要在其他 类 中使用的屏幕元素,例如 ViewModel):
class LanguagesFragment : Fragment()
{
override fun onCreateView ( inflater : LayoutInflater, container : ViewGroup?, savedInstanceState : Bundle? ) : View
{
LANGUAGES_SCREEN_VIEWS = FragmentLanguagesBinding.inflate ( inflater, container, false )
val languagesViewModel = ViewModelProvider ( this ).get ( LanguagesViewModel::class.java )
LANGUAGES_FRAGMENT = this
languagesViewModel.getLanguagesForUser().observe ( viewLifecycleOwner, { languages ->
LANGUAGES_SCREEN_VIEWS.languagesRecyclerView.adapter = LanguagesRecyclerViewAdapter ( languages ) } )
return LANGUAGES_SCREEN_VIEWS.root
}
}
这是适配器:
class LanguagesRecyclerViewAdapter ( private val languages: List < LanguagesEntity > ) : RecyclerView.Adapter < LanguagesRecyclerViewAdapter.ViewHolder > ()
{
override fun onCreateViewHolder ( viewGroup: ViewGroup, i: Int ): ViewHolder
{
return ViewHolder ( LayoutInflater.from ( viewGroup.context ).inflate ( R.layout.fragment_languages_cards, viewGroup, false ) )
}
override fun onBindViewHolder ( viewHolder: ViewHolder, index: Int )
{
val language = languages [ index ]
viewHolder.languageCard.text = language.name
viewHolder.languageCard.isChecked = language.spoken
}
override fun getItemCount() = languages.size
inner class ViewHolder ( itemView: View ) : RecyclerView.ViewHolder ( itemView )
{
val languageCard: CheckBox = itemView.findViewById ( R.id.language_selection )
init
{
languageCard.setOnClickListener {
LANGUAGE_RECYCLER_VIEW_STATE = LANGUAGES_SCREEN_VIEWS.languagesRecyclerView.layoutManager!!.onSaveInstanceState()!!
GlobalScope.launch {
// Save to local database
LanguageRepository ().updateLanguage ( languageCard )
// Delete on server, pop up if not possible
var dataSync = DataSyncBean ( languages = arrayListOf ( LanguageBean ( addedOrDeleted = if (
languageCard.isChecked ) ADDED else DELETED, name = languageCard.text.toString() ) ) )
try
{
dataSync = Retrofit.Builder().baseUrl ( ConstantsUtil.BASE_URL ).addConverterFactory ( GsonConverterFactory.create () ).client (
OkHttpClient.Builder().cookieJar ( CookieUtil () ).build() ).build().create (
RestfulCalls::class.java ).updateUserLanguageMapping ( dataSync )
}
catch ( e : Exception )
{
dataSync.syncOutcome = ConstantsUtil.SERVER_UNREACHABLE
withContext ( Dispatchers.Main ) {
val alertDialog : AlertDialog.Builder = AlertDialog.Builder ( LANGUAGES_FRAGMENT.context )
alertDialog.setTitle ( R.string.no_server_connection )
alertDialog.setMessage ( R.string.changes_local_only )
alertDialog.setPositiveButton ( R.string.ok ) { _, _ -> }
val alert: AlertDialog = alertDialog.create()
alert.setCanceledOnTouchOutside ( true )
alert.show()
}
}
}
}
}
}
}
我发现的一些解决方案提到使用 AsyncData,据我所知已弃用,因此我不会考虑这些。我在 DAO 中使用 Flow < List < LanguagesEntity > >
。
添加布局的解决方案无效:
RecyclerView notifyDataSetChanged scrolls to top position
val linearLayoutManager = LinearLayoutManager ( context, RecyclerView.VERTICAL, false )
LANGUAGES_SCREEN_VIEWS.languagesRecyclerView.setLayoutManager(linearLayoutManager)
另一个解决方案说这个错误取决于布局的高度是“包裹内容”并建议固定高度,但是尽管高度数字高得愚蠢,但 RecyclerView 根本没有滚动。
包括建议的解决方法。
此页面提供了一些解决方案:
Refreshing data in RecyclerView and keeping its scroll position
一个人
// Save state
private Parcelable recyclerViewState;
recyclerViewState = recyclerView.getLayoutManager().onSaveInstanceState();
// Restore state
recyclerView.getLayoutManager().onRestoreInstanceState(recyclerViewState);
但我完全不知道将这些行放在我的文件中的什么位置。
我想我应该截取数据更新前的那一刻(它不能点击,因为数据库可以通过从其他设备同步更新)和回收器视图正在更新的那一刻。我不明白这是哪里发生的。
Teo 在这个更具体的问题中给出了解决方案:
这是他的解决方案:
// STEP 1 - 创建一个通知变量的函数
internal fun setLanguage(lang: List<LanguagesEntity>) {
languages = lang
notifyDataSetChanged()
}
// 第 2 步 - 在一切之前设置 recyclerview
val languages = mutableListOf < LanguagesEntity > ()
var languagesAdapter = LanguagesRecyclerViewAdapter(languages)
LANGUAGES_SCREEN_VIEWS.languagesRecyclerView.adapter = languagesAdapter
// 第 3 步 - 为适配器设置新值
languagesViewModel.getLanguagesForUser().observe( requireActivity(), { languages ->
languagesAdapter.setLanguage(language)
} )