Kotlin安卓书籍类应用重启时如何保存文本滚动位置?
问题描述
我叫Yuriy,来自哈萨克斯坦,正在学习Kotlin编程语言。我开发了一款类似书籍的安卓应用,实现了从assets文件夹加载书籍文本并展示,以及文本垂直滚动功能,但目前无法在应用重启时保存文本的滚动位置,不清楚正确的实现方式,希望能获得帮助完成该功能,并指出我代码中的错误。
现有代码
MainActivity.kt
import android.annotation.SuppressLint import android.os.Bundle import android.widget.ScrollView import android.widget.TextView import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import java.io.BufferedReader import java.io.InputStreamReader @SuppressLint("StaticFieldLeak") private lateinit var textBook: TextView class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Включаем функцию enableEdgeToEdge, чтобы приложение занимало всю доступную область экрана enableEdgeToEdge() // Устанавливаем содержимое экрана setContentView(R.layout.activity_main) // Инициализируем `TextView` с помощью `findViewById` textBook = findViewById(R.id.textViewBook) // Инициализируем `ScrollView` с помощью `findViewById` //val scrollView = findViewById<ScrollView>(R.id.myScrollView) // Устанавливаем слушатель изменения размеров окна для получения информации о системных панелях ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> // Получаем информацию о системных панелях и добавляем соответствующие отступы к View val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) insets } //объявляем вне блока try, что позволяет ей быть доступной для использования в любом месте вашего кода. var text = "" // Открываем файл "book.txt" из ассетов и читаем его построчно // В этом коде мы обернули весь код, который работает с файлом, // в блок try-catch. Если при открытии файла или чтении его содержимого // возникнет исключение, мы его перехватим и выведем сообщение об ошибке // с помощью метода e.printStackTrace(). Это поможет нам понять, // что пошло не так, и исправить проблему. try { val inputStream = this.assets.open("book.txt") val reader = BufferedReader(InputStreamReader(inputStream)) reader.use { var line: String? while (reader.readLine().also { line = it } != null) { // Добавляем каждую строку к переменной text text += line + "\n" } } } catch (e: Exception) { // Обрабатываем исключение e.printStackTrace() } // Устанавливаем text_view.text = text, чтобы отобразить текст на экране textBook.text = text // Добавляем прокрутку вверх, низ к textBook // textBook.movementMethod = ScrollingMovementMethod() // val prefs = getSharedPreferences("MyPrefs", MODE_PRIVATE) // val editor = prefs.edit() //editor.putInt("scrollPosition", scrollView.scrollY) // editor.apply() //scrollView.scrollTo(0, prefs.getInt("scrollPosition", 0)) } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) val prefs = getSharedPreferences("MyPrefs", MODE_PRIVATE) val editor = prefs.edit() editor.putInt("scrollPosition", textBook.scrollY) editor.apply() } override fun onRestoreInstanceState(savedInstanceState: Bundle) { super.onRestoreInstanceState(savedInstanceState) val prefs = getSharedPreferences("MyPrefs", MODE_PRIVATE) textBook.scrollTo(0, prefs.getInt("scrollPosition", 0)) } }
activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/main" android:layout_width="fill_parent" android:layout_height="fill_parent" tools:context=".MainActivity"> <!-- Это корневой ConstraintLayout, который содержит все остальные элементы --> <ScrollView android:id="@+id/myScrollView" android:layout_width="fill_parent" android:layout_height="wrap_content" android:scrollbars="vertical" android:fillViewport="true" tools:ignore="MissingConstraints"> <!-- Это ScrollView, который позволяет прокручивать содержимое, когда оно слишком большое --> <TextView android:id="@+id/textViewBook" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/background_book" android:orientation="vertical" android:padding="15dp" android:textSize="16sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.0" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.0" /> </ScrollView> </androidx.constraintlayout.widget.ConstraintLayout>
我尝试过的三种实现方式
方式一:用Bundle保存
override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putInt("scrollPosition", textBook.scrollY) } override fun onRestoreInstanceState(savedInstanceState: Bundle) { super.onRestoreInstanceState(savedInstanceState) textBook.scrollTo(0, savedInstanceState.getInt("scrollPosition")) }
方式二:用SharedPreferences保存
override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) val prefs = getSharedPreferences("MyPrefs", MODE_PRIVATE) val editor = prefs.edit() editor.putInt("scrollPosition", textBook.scrollY) editor.apply() } override fun onRestoreInstanceState(savedInstanceState: Bundle) { super.onRestoreInstanceState(savedInstanceState) val prefs = getSharedPreferences("MyPrefs", MODE_PRIVATE) textBook.scrollTo(0, prefs.getInt("scrollPosition", 0)) }
方式三:直接操作ScrollView
val prefs = getSharedPreferences("MyPrefs", MODE_PRIVATE) val editor = prefs.edit() editor.putInt("scrollPosition", scrollView.scrollY) editor.apply() scrollView.scrollTo(0, prefs.getInt("scrollPosition", 0))
问题分析与解决方案
代码中的错误点
- 滚动对象错误:实际负责滚动的是外层的
ScrollView,不是内部的TextView。你之前一直获取textBook.scrollY,但TextView未启用自身滚动(注释掉了movementMethod),滚动状态由ScrollView维护,所以这个值始终为0。 - ScrollView未初始化:代码中把ScrollView的初始化代码注释掉了,导致无法获取其实例来操作滚动状态。
- 恢复时机错误:在
onRestoreInstanceState中恢复滚动位置时,TextView的文本可能还未加载完成,此时滚动操作会失效。 - 保存时机局限:仅在
onSaveInstanceState中保存,该方法仅在系统回收Activity时触发,用户主动退出应用时不会执行,导致滚动位置丢失。
正确实现代码
修改后的MainActivity.kt:
import android.os.Bundle import android.widget.ScrollView import android.widget.TextView import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import java.io.BufferedReader import java.io.InputStreamReader class MainActivity : AppCompatActivity() { private lateinit var textBook: TextView private lateinit var scrollView: ScrollView private val PREFS_NAME = "BookReaderPrefs" private val SCROLL_POS_KEY = "scroll_position" override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContentView(R.layout.activity_main) // 初始化控件 textBook = findViewById(R.id.textViewBook) scrollView = findViewById(R.id.myScrollView) ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) insets } // 加载书籍文本 loadBookText() // 恢复滚动位置 restoreScrollPosition() } private fun loadBookText() { var text = "" try { val inputStream = assets.open("book.txt") BufferedReader(InputStreamReader(inputStream)).use { reader -> var line: String? while (reader.readLine().also { line = it } != null) { text += line + "\n" } } } catch (e: Exception) { e.printStackTrace() } textBook.text = text } private fun restoreScrollPosition() { val prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE) val scrollPos = prefs.getInt(SCROLL_POS_KEY, 0) // 用post确保TextView渲染完成后再执行滚动 scrollView.post { scrollView.scrollTo(0, scrollPos) } } private fun saveScrollPosition() { val prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE) val editor = prefs.edit() editor.putInt(SCROLL_POS_KEY, scrollView.scrollY) editor.apply() } // 在Activity暂停时保存滚动位置,覆盖主动退出和系统回收场景 override fun onPause() { super.onPause() saveScrollPosition() } }
实现说明
- 初始化ScrollView:取消注释,正确获取ScrollView实例,所有滚动操作都针对它。
- 保存时机:在
onPause中保存滚动位置,无论用户主动退出还是系统回收Activity,都会执行该方法,确保位置不会丢失。 - 恢复时机:在文本加载完成后调用恢复方法,并用
scrollView.post确保UI渲染完成后再滚动,避免无效操作。 - 命名规范:使用更清晰的SharedPreferences命名,避免混淆。
内容的提问来源于stack exchange,提问作者Юрий Жежелев
相关产品推荐
相关产品推荐

