You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

问题分析与解决方案

代码中的错误点

  1. 滚动对象错误:实际负责滚动的是外层的ScrollView,不是内部的TextView。你之前一直获取textBook.scrollY,但TextView未启用自身滚动(注释掉了movementMethod),滚动状态由ScrollView维护,所以这个值始终为0。
  2. ScrollView未初始化:代码中把ScrollView的初始化代码注释掉了,导致无法获取其实例来操作滚动状态。
  3. 恢复时机错误:在onRestoreInstanceState中恢复滚动位置时,TextView的文本可能还未加载完成,此时滚动操作会失效。
  4. 保存时机局限:仅在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,提问作者Юрий Жежелев

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 09:55:56