From 5f83446a153cda443b782db6fd281b9a6d76ed4c Mon Sep 17 00:00:00 2001 From: fansan Date: Sat, 13 Jun 2026 16:15:17 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E4=BC=98=E5=8C=96=E9=98=85?= =?UTF-8?q?=E8=AF=BB=E7=95=8C=E9=9D=A2=E8=8F=9C=E5=8D=95=E5=88=87=E6=8D=A2?= =?UTF-8?q?page=E6=97=B6=E7=9A=84=E9=AB=98=E5=BA=A6=E8=BF=87=E6=B8=A1?= =?UTF-8?q?=E5=8A=A8=E7=94=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/ui/book/read/ReadBookMenuBar.kt | 4 +- .../ui/book/read/sheet/HeaderFooterPage.kt | 70 +++++++++-- .../app/ui/book/read/sheet/ReadStyleSheet.kt | 116 ++++++++++++++---- .../app/ui/book/read/sheet/SystemMenuPage.kt | 109 ++++++++++++---- 4 files changed, 237 insertions(+), 62 deletions(-) diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookMenuBar.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookMenuBar.kt index 3fe62f24b..7358e9759 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookMenuBar.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookMenuBar.kt @@ -613,6 +613,7 @@ private fun ReadBookMenuSurface( title = stringResource(R.string.read_config), maxHeight = maxHeight, bottomPadding = if (extendSurfaceToNavigationBar) navBarHeight else 0.dp, + animateSize = false, onBack = { onIntent(ReadBookIntent.ReadMenuBack) }, ) { ReadStyleContent( @@ -762,6 +763,7 @@ private fun ReadBookMenuRoutePage( maxHeight: Dp, scrollContent: Boolean = false, bottomPadding: Dp = 0.dp, + animateSize: Boolean = true, onBack: () -> Unit, content: @Composable () -> Unit, ) { @@ -769,7 +771,7 @@ private fun ReadBookMenuRoutePage( modifier = Modifier .fillMaxWidth() .heightIn(max = maxHeight) - .animateContentSize() + .let { if (animateSize) it.animateContentSize() else it } .padding(top = 16.dp, bottom = 16.dp + bottomPadding), ) { Row( diff --git a/app/src/main/java/io/legado/app/ui/book/read/sheet/HeaderFooterPage.kt b/app/src/main/java/io/legado/app/ui/book/read/sheet/HeaderFooterPage.kt index c2140982a..6ed38b145 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/sheet/HeaderFooterPage.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/sheet/HeaderFooterPage.kt @@ -1,5 +1,8 @@ package io.legado.app.ui.book.read.sheet +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -15,20 +18,26 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringArrayResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp import androidx.core.net.toUri import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.legado.app.R @@ -39,7 +48,6 @@ import io.legado.app.ui.book.read.ConfigUpdate import io.legado.app.ui.book.read.ReadBookIntent import io.legado.app.ui.widget.components.FontSelectSheet import io.legado.app.ui.widget.components.dialog.ColorPickerSheet -import org.koin.compose.koinInject import io.legado.app.ui.widget.components.settingItem.TinyClickableSettingItem import io.legado.app.ui.widget.components.settingItem.TinyColorSettingItem import io.legado.app.ui.widget.components.settingItem.TinyDropdownSettingItem @@ -48,6 +56,7 @@ import io.legado.app.ui.widget.components.settingItem.TinySwitchSettingItem import io.legado.app.ui.widget.components.tabRow.CardTabRow import io.legado.app.utils.getCompatColor import kotlinx.coroutines.launch +import org.koin.compose.koinInject // Color picker IDs private const val COLOR_HEADER = 7 @@ -70,6 +79,34 @@ internal fun HeaderFooterPage( val pagerState = rememberPagerState(pageCount = { 3 }) var selectedTab by remember { mutableIntStateOf(0) } + val pageHeights = remember { mutableStateMapOf() } + val density = LocalDensity.current + val animatedHeight by remember(pagerState, density) { + derivedStateOf { + val pageCount = pagerState.pageCount + val position = (pagerState.currentPage + pagerState.currentPageOffsetFraction) + .coerceIn(0f, (pageCount - 1).coerceAtLeast(0).toFloat()) + val floorPage = position.toInt().coerceIn(0, pageCount - 1) + val ceilPage = (floorPage + 1).coerceIn(0, pageCount - 1) + + val floorHeight = pageHeights[floorPage] ?: 0 + val ceilHeight = pageHeights[ceilPage] ?: floorHeight + + val fraction = position - floorPage + + val startHeight = if (floorHeight > 0) floorHeight else (pageHeights.values.firstOrNull() ?: 0) + val endHeight = if (ceilHeight > 0) ceilHeight else startHeight + + val interpolated = startHeight + (endHeight - startHeight) * fraction + + if (interpolated > 0) { + with(density) { interpolated.toDp() } + } else { + Dp.Unspecified + } + } + } + // Header state var headerMode by remember { mutableIntStateOf(ReadBookConfig.headerMode) } var headerLeft by remember { mutableIntStateOf(ReadBookConfig.tipHeaderLeft) } @@ -138,15 +175,31 @@ internal fun HeaderFooterPage( selectedTabIndex = selectedTab, onTabSelected = { index -> selectedTab = index - scope.launch { pagerState.animateScrollToPage(index) } + scope.launch { + pagerState.animateScrollToPage( + page = index, + animationSpec = tween(durationMillis = 400, easing = FastOutSlowInEasing) + ) + } }, modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp), ) HorizontalPager( state = pagerState, - modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.Top, + modifier = Modifier + .fillMaxWidth() + .clipToBounds() + .pagerHeight(animatedHeight), ) { page -> - when (page) { + Box( + modifier = Modifier + .fillMaxWidth() + .onSizeChanged { size -> + pageHeights[page] = size.height + } + ) { + when (page) { 0 -> { // Header tab Column( @@ -372,6 +425,7 @@ internal fun HeaderFooterPage( } } } +} // Color picker ColorPickerSheet( diff --git a/app/src/main/java/io/legado/app/ui/book/read/sheet/ReadStyleSheet.kt b/app/src/main/java/io/legado/app/ui/book/read/sheet/ReadStyleSheet.kt index 20435348f..b0e893e63 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/sheet/ReadStyleSheet.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/sheet/ReadStyleSheet.kt @@ -1,6 +1,8 @@ package io.legado.app.ui.book.read.sheet -import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -8,14 +10,22 @@ import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.layout.layout +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import io.legado.app.R import io.legado.app.ui.book.read.ConfigUpdate @@ -44,6 +54,34 @@ fun ReadStyleContent( val pagerState = rememberPagerState(pageCount = { 3 }) var currentPage by remember { mutableIntStateOf(0) } + val pageHeights = remember { mutableStateMapOf() } + val density = LocalDensity.current + val animatedHeight by remember(pagerState, density) { + derivedStateOf { + val pageCount = pagerState.pageCount + val position = (pagerState.currentPage + pagerState.currentPageOffsetFraction) + .coerceIn(0f, (pageCount - 1).coerceAtLeast(0).toFloat()) + val floorPage = position.toInt().coerceIn(0, pageCount - 1) + val ceilPage = (floorPage + 1).coerceIn(0, pageCount - 1) + + val floorHeight = pageHeights[floorPage] ?: 0 + val ceilHeight = pageHeights[ceilPage] ?: floorHeight + + val fraction = position - floorPage + + val startHeight = if (floorHeight > 0) floorHeight else (pageHeights.values.firstOrNull() ?: 0) + val endHeight = if (ceilHeight > 0) ceilHeight else startHeight + + val interpolated = startHeight + (endHeight - startHeight) * fraction + + if (interpolated > 0) { + with(density) { interpolated.toDp() } + } else { + Dp.Unspecified + } + } + } + LaunchedEffect(pagerState) { snapshotFlow { pagerState.currentPage }.collect { page -> currentPage = page @@ -57,35 +95,46 @@ fun ReadStyleContent( ) { HorizontalPager( state = pagerState, + verticalAlignment = Alignment.Top, modifier = Modifier .weight(1f, fill = false) - .animateContentSize(), + .clipToBounds() + .pagerHeight(animatedHeight), ) { page -> - when (page) { - 0 -> GlobalThemePage( - onToggleDayNight = onToggleDayNight, - onOpenBgTextConfig = onOpenBgTextConfig, - onOpenTextTitle = onOpenTextTitle, - onOpenPaddingConfig = onOpenPaddingConfig, - onShareLayoutChange = { shareLayout -> - onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.ShareLayout(shareLayout))) - }, - onStyleSelect = { index -> - onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.StyleSelect(index))) - }, - modifier = Modifier.padding(horizontal = 16.dp), - onIntent = onIntent, - styleConfig = styleConfig, - ) + Box( + modifier = Modifier + .fillMaxWidth() + .onSizeChanged { size -> + pageHeights[page] = size.height + } + ) { + when (page) { + 0 -> GlobalThemePage( + onToggleDayNight = onToggleDayNight, + onOpenBgTextConfig = onOpenBgTextConfig, + onOpenTextTitle = onOpenTextTitle, + onOpenPaddingConfig = onOpenPaddingConfig, + onShareLayoutChange = { shareLayout -> + onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.ShareLayout(shareLayout))) + }, + onStyleSelect = { index -> + onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.StyleSelect(index))) + }, + modifier = Modifier.padding(horizontal = 16.dp), + onIntent = onIntent, + styleConfig = styleConfig, + ) - 1 -> SystemMenuPage( - customIcons = readMenuCustomIcons, - bottomBarButtons = bottomBarButtons, - onIntent = onIntent, - ) - 2 -> HeaderFooterPage( - onIntent = onIntent, - ) + 1 -> SystemMenuPage( + customIcons = readMenuCustomIcons, + bottomBarButtons = bottomBarButtons, + onIntent = onIntent, + ) + + 2 -> HeaderFooterPage( + onIntent = onIntent, + ) + } } } @@ -100,7 +149,12 @@ fun ReadStyleContent( selectedTabIndex = currentPage, onTabSelected = { index -> if (index < 3) { - scope.launch { pagerState.animateScrollToPage(index) } + scope.launch { + pagerState.animateScrollToPage( + page = index, + animationSpec = tween(durationMillis = 400, easing = FastOutSlowInEasing) + ) + } } else { onOpenMoreConfig() } @@ -109,3 +163,11 @@ fun ReadStyleContent( ) } } + +internal fun Modifier.pagerHeight(height: Dp) = this.layout { measurable, constraints -> + val placeable = measurable.measure(constraints) + val layoutHeight = if (height != Dp.Unspecified) height.roundToPx() else placeable.height + layout(placeable.width, layoutHeight) { + placeable.placeRelative(0, 0) + } +} diff --git a/app/src/main/java/io/legado/app/ui/book/read/sheet/SystemMenuPage.kt b/app/src/main/java/io/legado/app/ui/book/read/sheet/SystemMenuPage.kt index e5cae7682..502f243b3 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/sheet/SystemMenuPage.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/sheet/SystemMenuPage.kt @@ -2,6 +2,8 @@ package io.legado.app.ui.book.read.sheet import android.content.Context import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -24,20 +26,28 @@ import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.SkipNext import androidx.compose.material.icons.filled.SkipPrevious import androidx.compose.material.icons.filled.Translate +import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringArrayResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.legado.app.R @@ -89,6 +99,34 @@ internal fun SystemMenuPage( val pagerState = rememberPagerState(pageCount = { 3 }) var selectedTab by remember { mutableIntStateOf(0) } + val pageHeights = remember { mutableStateMapOf() } + val density = LocalDensity.current + val animatedHeight by remember(pagerState, density) { + derivedStateOf { + val pageCount = pagerState.pageCount + val position = (pagerState.currentPage + pagerState.currentPageOffsetFraction) + .coerceIn(0f, (pageCount - 1).coerceAtLeast(0).toFloat()) + val floorPage = position.toInt().coerceIn(0, pageCount - 1) + val ceilPage = (floorPage + 1).coerceIn(0, pageCount - 1) + + val floorHeight = pageHeights[floorPage] ?: 0 + val ceilHeight = pageHeights[ceilPage] ?: floorHeight + + val fraction = position - floorPage + + val startHeight = if (floorHeight > 0) floorHeight else (pageHeights.values.firstOrNull() ?: 0) + val endHeight = if (ceilHeight > 0) ceilHeight else startHeight + + val interpolated = startHeight + (endHeight - startHeight) * fraction + + if (interpolated > 0) { + with(density) { interpolated.toDp() } + } else { + Dp.Unspecified + } + } + } + // Shared state for sheets var showColorPicker by remember { mutableStateOf(false) } var colorPickerId by remember { mutableIntStateOf(0) } @@ -112,39 +150,58 @@ internal fun SystemMenuPage( selectedTabIndex = selectedTab, onTabSelected = { index -> selectedTab = index - scope.launch { pagerState.animateScrollToPage(index) } + scope.launch { + pagerState.animateScrollToPage( + page = index, + animationSpec = tween(durationMillis = 400, easing = FastOutSlowInEasing) + ) + } }, modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp), ) HorizontalPager( state = pagerState, - modifier = Modifier.weight(1f, fill = false), + verticalAlignment = Alignment.Top, + modifier = Modifier + .weight(1f, fill = false) + .clipToBounds() + .pagerHeight(animatedHeight), ) { page -> - when (page) { - 0 -> GlobalMenuTab( - preferences = preferences, - readBarStyle = preferences.readBarStyle, - readMenuColorMode = preferences.readMenuColorMode, - onIntent = onIntent, - onShowColorPicker = { id, initial -> - colorPickerId = id - colorPickerInitial = initial - showColorPicker = true - }, - ) - 1 -> BottomBarTab( - preferences = preferences, - customIcons = customIcons, - onIntent = onIntent, - onShowIconSheet = { showIconSheet = true }, - onShowColorPicker = { id, initial -> - colorPickerId = id - colorPickerInitial = initial - showColorPicker = true - }, - ) - 2 -> TopBarTab(preferences = preferences, onIntent = onIntent) + Box( + modifier = Modifier + .fillMaxWidth() + .onSizeChanged { size -> + pageHeights[page] = size.height + } + ) { + when (page) { + 0 -> GlobalMenuTab( + preferences = preferences, + readBarStyle = preferences.readBarStyle, + readMenuColorMode = preferences.readMenuColorMode, + onIntent = onIntent, + onShowColorPicker = { id, initial -> + colorPickerId = id + colorPickerInitial = initial + showColorPicker = true + }, + ) + + 1 -> BottomBarTab( + preferences = preferences, + customIcons = customIcons, + onIntent = onIntent, + onShowIconSheet = { showIconSheet = true }, + onShowColorPicker = { id, initial -> + colorPickerId = id + colorPickerInitial = initial + showColorPicker = true + }, + ) + + 2 -> TopBarTab(preferences = preferences, onIntent = onIntent) + } } } }