Просмотр исходного кода

perf: 优化字体选择器性能 (#1497)

fansan 4 месяцев назад
Родитель
Сommit
ce9ca94441

+ 12 - 6
app/src/main/java/io/legado/app/ui/book/read/ReadBookScreen.kt

@@ -12,7 +12,6 @@ import androidx.compose.ui.res.stringResource
 import androidx.core.net.toUri
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import io.legado.app.R
-import io.legado.app.data.repository.ReadPreferences
 import io.legado.app.data.repository.ReadSettingsRepository
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.ui.book.read.sheet.BgTextConfigSheet
@@ -37,6 +36,7 @@ import io.legado.app.ui.book.read.sheet.SpeakEngineConfigSheet
 import io.legado.app.ui.book.read.sheet.TitleBarIconSheet
 import io.legado.app.ui.book.read.sheet.ToolButtonConfigSheet
 import io.legado.app.ui.book.read.sheet.UnderlineConfigSheet
+import io.legado.app.ui.widget.components.FontFolderState
 import io.legado.app.ui.widget.components.FontSelectSheet
 import io.legado.app.ui.widget.components.alert.AppAlertDialog
 import io.legado.app.ui.widget.components.changeSource.ChangeSourceSheet
@@ -172,15 +172,21 @@ fun ReadBookScreen(
     )
     val fontSelectReadSettings: ReadSettingsRepository = org.koin.compose.koinInject()
     val fontSelectPreferences by fontSelectReadSettings.preferences.collectAsStateWithLifecycle(
-        initialValue = ReadPreferences()
+        initialValue = null
     )
-    val fontSelectFolderUri = fontSelectPreferences.fontFolder
-        .takeIf { it.isNotEmpty() }?.toUri()
+    val fontSelectFolderState = remember(fontSelectPreferences) {
+        val pref = fontSelectPreferences
+        if (pref == null) {
+            FontFolderState.Loading
+        } else {
+            FontFolderState.Loaded(pref.fontFolder.takeIf { it.isNotEmpty() }?.toUri())
+        }
+    }
     val fontSelectSystemTypefaces = stringArrayResource(R.array.system_typefaces)
     FontSelectSheet(
         show = state.activeSheet is ReadBookSheet.FontSelect,
         title = stringResource(R.string.select_font),
-        fontFolderUri = fontSelectFolderUri,
+        folderState = fontSelectFolderState,
         selectedFontPath = ReadBookConfig.textFont,
         onDismissRequest = dismissSheet,
         onSelectFont = { onIntent(ReadBookIntent.SelectFont(it.uri.toString())) },
@@ -191,7 +197,7 @@ fun ReadBookScreen(
     FontSelectSheet(
         show = state.activeSheet is ReadBookSheet.TitleFontSelect,
         title = stringResource(R.string.read_config_title_settings),
-        fontFolderUri = fontSelectFolderUri,
+        folderState = fontSelectFolderState,
         selectedFontPath = ReadBookConfig.titleFont,
         onDismissRequest = dismissSheet,
         onSelectFont = { onIntent(ReadBookIntent.SelectTitleFont(it.uri.toString())) },

+ 11 - 4
app/src/main/java/io/legado/app/ui/book/read/sheet/HeaderFooterPage.kt

@@ -49,12 +49,12 @@ import androidx.compose.ui.unit.dp
 import androidx.core.net.toUri
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import io.legado.app.R
-import io.legado.app.data.repository.ReadPreferences
 import io.legado.app.data.repository.ReadSettingsRepository
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.ui.book.read.ConfigUpdate
 import io.legado.app.ui.book.read.ReadBookIntent
 import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.widget.components.FontFolderState
 import io.legado.app.ui.widget.components.FontSelectSheet
 import io.legado.app.ui.widget.components.SectionTitle
 import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
@@ -568,15 +568,22 @@ internal fun HeaderFooterPage(
     // Font selector for header/footer
     val readSettingsRepository: ReadSettingsRepository = koinInject()
     val preferences by readSettingsRepository.preferences.collectAsStateWithLifecycle(
-        initialValue = ReadPreferences()
+        initialValue = null
     )
-    val fontFolderUri = preferences.fontFolder.takeIf { it.isNotEmpty() }?.toUri()
+    val fontFolderState = remember(preferences) {
+        val pref = preferences
+        if (pref == null) {
+            FontFolderState.Loading
+        } else {
+            FontFolderState.Loaded(pref.fontFolder.takeIf { it.isNotEmpty() }?.toUri())
+        }
+    }
     val systemTypefaces = stringArrayResource(R.array.system_typefaces)
 
     FontSelectSheet(
         show = showFontSelect,
         title = stringResource(R.string.select_font),
-        fontFolderUri = fontFolderUri,
+        folderState = fontFolderState,
         selectedFontPath = ReadBookConfig.headerFont,
         onDismissRequest = { showFontSelect = false },
         onSelectFont = {

+ 11 - 4
app/src/main/java/io/legado/app/ui/book/read/sheet/HighlightRuleEditSheet.kt

@@ -43,13 +43,13 @@ import androidx.core.net.toUri
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import io.legado.app.R
 import io.legado.app.data.entities.HighlightRule
-import io.legado.app.data.repository.ReadPreferences
 import io.legado.app.data.repository.ReadSettingsRepository
 import io.legado.app.data.repository.configNames
 import io.legado.app.data.repository.toJsonArray
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.widget.components.AppTextField
+import io.legado.app.ui.widget.components.FontFolderState
 import io.legado.app.ui.widget.components.FontSelectSheet
 import io.legado.app.ui.widget.components.SectionTitle
 import io.legado.app.ui.widget.components.card.NormalCard
@@ -515,9 +515,16 @@ fun HighlightRuleEditSheet(
     val readSettingsRepository: ReadSettingsRepository = org.koin.compose.koinInject()
     val fontSelectScope = rememberCoroutineScope()
     val fontSelectPreferences by readSettingsRepository.preferences.collectAsStateWithLifecycle(
-        initialValue = ReadPreferences()
+        initialValue = null
     )
-    val fontFolderUri = fontSelectPreferences.fontFolder.takeIf { it.isNotEmpty() }?.toUri()
+    val fontFolderState = remember(fontSelectPreferences) {
+        val pref = fontSelectPreferences
+        if (pref == null) {
+            FontFolderState.Loading
+        } else {
+            FontFolderState.Loaded(pref.fontFolder.takeIf { it.isNotEmpty() }?.toUri())
+        }
+    }
     val systemTypefaces = stringArrayResource(R.array.system_typefaces)
     val fontFolderLauncher = rememberLauncherForActivityResult(
         ActivityResultContracts.OpenDocumentTree()
@@ -534,7 +541,7 @@ fun HighlightRuleEditSheet(
     FontSelectSheet(
         show = showFontSelect,
         title = stringResource(R.string.select_font),
-        fontFolderUri = fontFolderUri,
+        folderState = fontFolderState,
         selectedFontPath = fontPath,
         onDismissRequest = { showFontSelect = false },
         onSelectFont = { fontPath = it.uri.toString(); showFontSelect = false },

+ 11 - 10
app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt

@@ -74,7 +74,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import io.legado.app.R
 import io.legado.app.base.AppContextWrapper
 import io.legado.app.constant.EventBus
-import io.legado.app.constant.PreferKey
 import io.legado.app.help.LauncherIconHelp
 import io.legado.app.help.config.ThemeConfigStore
 import io.legado.app.ui.config.labConfig.LabConfig
@@ -83,6 +82,7 @@ import io.legado.app.ui.theme.ThemeEngine
 import io.legado.app.ui.theme.ThemeResolver
 import io.legado.app.ui.theme.adaptiveContentPadding
 import io.legado.app.ui.widget.components.AppScaffold
+import io.legado.app.ui.widget.components.FontFolderState
 import io.legado.app.ui.widget.components.FontSelectSheet
 import io.legado.app.ui.widget.components.SplicedColumnGroup
 import io.legado.app.ui.widget.components.alert.AppAlertDialog
@@ -98,9 +98,7 @@ import io.legado.app.ui.widget.components.text.AppText
 import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
 import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
 import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
-import io.legado.app.utils.getPrefString
 import io.legado.app.utils.postEvent
-import io.legado.app.utils.putPrefString
 import io.legado.app.utils.restart
 import io.legado.app.utils.takePersistablePermissionSafely
 import io.legado.app.utils.toastOnUi
@@ -128,19 +126,22 @@ fun ThemeConfigScreen(
     var showFontSheet by remember { mutableStateOf(false) }
     val showThemeRefactorTip by viewModel.showThemeRefactorTip.collectAsStateWithLifecycle()
 
-    var fontFolderUri by remember {
-        mutableStateOf(
-            context.getPrefString(PreferKey.fontFolder)?.toUri()
-        )
+    val fontFolder by viewModel.fontFolder.collectAsStateWithLifecycle()
+    val fontFolderState = remember(fontFolder) {
+        val folder = fontFolder
+        if (folder == null) {
+            FontFolderState.Loading
+        } else {
+            FontFolderState.Loaded(folder.takeIf { it.isNotEmpty() }?.toUri())
+        }
     }
 
     val fontFolderLauncher = rememberLauncherForActivityResult(
         ActivityResultContracts.OpenDocumentTree()
     ) { uri ->
         if (uri != null) {
-            fontFolderUri = uri
             uri.takePersistablePermissionSafely(context, Intent.FLAG_GRANT_READ_URI_PERMISSION)
-            context.putPrefString(PreferKey.fontFolder, uri.toString())
+            viewModel.setFontFolder(uri.toString())
         }
     }
 
@@ -869,7 +870,7 @@ fun ThemeConfigScreen(
     FontSelectSheet(
         show = showFontSheet,
         title = stringResource(R.string.font_setting),
-        fontFolderUri = fontFolderUri,
+        folderState = fontFolderState,
         selectedFontPath = ThemeConfig.appFontPath,
         onDismissRequest = { showFontSheet = false },
         onSelectFont = { doc ->

+ 16 - 1
app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigViewModel.kt

@@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope
 import io.legado.app.constant.PreferKey
 import io.legado.app.data.local.preferences.LocalPreferencesKeys
 import io.legado.app.data.local.preferences.LocalPreferencesRepository
+import io.legado.app.data.repository.ReadSettingsRepository
 import io.legado.app.utils.FileDoc
 import io.legado.app.utils.FileUtils
 import io.legado.app.utils.MD5Utils
@@ -13,6 +14,7 @@ import io.legado.app.utils.externalFiles
 import io.legado.app.utils.inputStream
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
@@ -21,9 +23,22 @@ import java.io.File
 import java.io.FileOutputStream
 
 class ThemeConfigViewModel(
-    private val localPreferencesRepository: LocalPreferencesRepository
+    private val localPreferencesRepository: LocalPreferencesRepository,
+    private val readSettingsRepository: ReadSettingsRepository
 ) : ViewModel() {
 
+    val fontFolder = readSettingsRepository.preferences
+        .map { it.fontFolder as String? }
+        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
+
+    fun setFontFolder(path: String) {
+        viewModelScope.launch {
+            runCatching {
+                readSettingsRepository.setFontFolder(path)
+            }
+        }
+    }
+
     val showThemeRefactorTip = localPreferencesRepository
         .getPreference(LocalPreferencesKeys.SHOW_THEME_REFACTOR_TIP, true)
         .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), true)

+ 52 - 27
app/src/main/java/io/legado/app/ui/widget/components/FontSelectGrid.kt

@@ -1,6 +1,7 @@
 package io.legado.app.ui.widget.components
 
 import android.graphics.Typeface
+import android.net.Uri
 import androidx.compose.foundation.background
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Arrangement
@@ -25,8 +26,10 @@ import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.State
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.produceState
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
@@ -41,35 +44,49 @@ import io.legado.app.R
 import io.legado.app.help.loadFontFiles
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.utils.FileDoc
+import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.withContext
 
+private val fontGridHeight = 360.dp
+
+
+sealed interface FontFolderState {
+    data object Loading : FontFolderState
+    data class Loaded(val uri: Uri?) : FontFolderState
+}
+
 /**
  * Shared font selection grid with search support.
  *
- * @param fontFolderUri URI of the font folder to load from
+ * @param folderState 字体文件夹加载状态,见 [FontFolderState]
  * @param selectedFontName currently selected font name (for check mark), null to hide
  * @param onSelectFont called when a font file is selected
  * @param emptyText text to show when no fonts found
  */
 @Composable
 fun FontSelectGrid(
-    fontFolderUri: android.net.Uri?,
+    folderState: FontFolderState,
     selectedFontName: String?,
     onSelectFont: (FileDoc) -> Unit,
     emptyText: String? = null,
 ) {
     val context = LocalContext.current
     var fontItems by remember { mutableStateOf<List<FileDoc>>(emptyList()) }
-    var isLoading by remember { mutableStateOf(true) }
+    var filesLoading by remember { mutableStateOf(false) }
     var searchQuery by remember { mutableStateOf("") }
 
-    LaunchedEffect(fontFolderUri) {
-        isLoading = true
-        fontItems = withContext(Dispatchers.IO) {
-            loadFontFiles(context, fontFolderUri)
+    LaunchedEffect(folderState) {
+        if (folderState is FontFolderState.Loaded) {
+            filesLoading = true
+            try {
+                fontItems = withContext(Dispatchers.IO) {
+                    loadFontFiles(context, folderState.uri)
+                }
+            } finally {
+                filesLoading = false
+            }
         }
-        isLoading = false
     }
 
     val filteredItems = remember(fontItems, searchQuery) {
@@ -77,6 +94,8 @@ fun FontSelectGrid(
         else fontItems.filter { it.name.contains(searchQuery, ignoreCase = true) }
     }
 
+    val showLoading = folderState is FontFolderState.Loading || filesLoading
+
     Column(
         modifier = Modifier
             .fillMaxWidth()
@@ -92,11 +111,11 @@ fun FontSelectGrid(
         Spacer(Modifier.height(4.dp))
 
         // Font grid
-        if (isLoading) {
+        if (showLoading) {
             Box(
                 modifier = Modifier
                     .fillMaxWidth()
-                    .height(200.dp),
+                    .height(fontGridHeight),
                 contentAlignment = Alignment.Center,
             ) {
                 CircularProgressIndicator()
@@ -105,7 +124,7 @@ fun FontSelectGrid(
             Box(
                 modifier = Modifier
                     .fillMaxWidth()
-                    .height(100.dp),
+                    .height(fontGridHeight),
                 contentAlignment = Alignment.Center,
             ) {
                 Text(
@@ -120,7 +139,7 @@ fun FontSelectGrid(
                 contentPadding = PaddingValues(vertical = 4.dp),
                 horizontalArrangement = Arrangement.spacedBy(8.dp),
                 verticalArrangement = Arrangement.spacedBy(8.dp),
-                modifier = Modifier.height(360.dp),
+                modifier = Modifier.height(fontGridHeight),
             ) {
                 items(filteredItems, key = { it.name }) { item ->
                     FontItem(
@@ -140,21 +159,7 @@ private fun FontItem(
     isSelected: Boolean,
     onClick: () -> Unit,
 ) {
-    val context = LocalContext.current
-    val fontFamily = remember(item.uri) {
-        runCatching {
-            val uri = item.uri
-            val typeface: Typeface? = if (uri.scheme == "content") {
-                context.contentResolver.openFileDescriptor(uri, "r")?.use {
-                    Typeface.Builder(it.fileDescriptor).build()
-                }
-            } else {
-                uri.path?.let { Typeface.createFromFile(it) }
-            }
-            typeface?.let { FontFamily(it) }
-        }.getOrNull()
-    }
-
+    val fontFamily by rememberItemFontFamily(item.uri)
     Box(
         modifier = Modifier
             .fillMaxWidth()
@@ -186,3 +191,23 @@ private fun FontItem(
         }
     }
 }
+
+@Composable
+private fun rememberItemFontFamily(uri: Uri): State<FontFamily?> {
+    val context = LocalContext.current
+    return produceState<FontFamily?>(initialValue = null, uri) {
+        val parsed = withContext(Dispatchers.IO) {
+            runCatching {
+                val typeface: Typeface? = if (uri.scheme == "content") {
+                    context.contentResolver.openFileDescriptor(uri, "r")?.use {
+                        Typeface.Builder(it.fileDescriptor).build()
+                    }
+                } else {
+                    uri.path?.let { Typeface.createFromFile(it) }
+                }
+                typeface?.let { FontFamily(it) }
+            }.onFailure { if (it is CancellationException) throw it }.getOrNull()
+        }
+        if (parsed != null) value = parsed
+    }
+}

+ 2 - 3
app/src/main/java/io/legado/app/ui/widget/components/FontSelectSheet.kt

@@ -1,6 +1,5 @@
 package io.legado.app.ui.widget.components
 
-import android.net.Uri
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.FolderOpen
 import androidx.compose.material.icons.filled.TextFields
@@ -24,7 +23,7 @@ import io.legado.app.utils.FileDoc
 fun FontSelectSheet(
     show: Boolean = true,
     title: String,
-    fontFolderUri: Uri?,
+    folderState: FontFolderState,
     selectedFontPath: String?,
     onDismissRequest: () -> Unit,
     onSelectFont: (FileDoc) -> Unit,
@@ -83,7 +82,7 @@ fun FontSelectSheet(
         },
     ) {
         FontSelectGrid(
-            fontFolderUri = fontFolderUri,
+            folderState = folderState,
             selectedFontName = selectedFontName,
             onSelectFont = { doc ->
                 onSelectFont(doc)