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

This commit is contained in:
fansan
2026-06-20 00:20:35 +08:00
committed by GitHub
parent ad8b8df13f
commit ce9ca94441
7 changed files with 115 additions and 55 deletions
@@ -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())) },
@@ -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 = {
@@ -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 },
@@ -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 ->
@@ -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)
@@ -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
}
}
@@ -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)