refactor:优化阅读界面菜单切换page时的高度过渡动画

@fansangg @贡献者
This commit is contained in:
Kudomaga
2026-06-14 00:25:05 +08:00
committed by GitHub
4 changed files with 195 additions and 62 deletions
@@ -613,6 +613,7 @@ private fun ReadBookMenuSurface(
title = stringResource(R.string.read_config), title = stringResource(R.string.read_config),
maxHeight = maxHeight, maxHeight = maxHeight,
bottomPadding = if (extendSurfaceToNavigationBar) navBarHeight else 0.dp, bottomPadding = if (extendSurfaceToNavigationBar) navBarHeight else 0.dp,
animateSize = false,
onBack = { onIntent(ReadBookIntent.ReadMenuBack) }, onBack = { onIntent(ReadBookIntent.ReadMenuBack) },
) { ) {
ReadStyleContent( ReadStyleContent(
@@ -762,6 +763,7 @@ private fun ReadBookMenuRoutePage(
maxHeight: Dp, maxHeight: Dp,
scrollContent: Boolean = false, scrollContent: Boolean = false,
bottomPadding: Dp = 0.dp, bottomPadding: Dp = 0.dp,
animateSize: Boolean = true,
onBack: () -> Unit, onBack: () -> Unit,
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
@@ -769,7 +771,7 @@ private fun ReadBookMenuRoutePage(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.heightIn(max = maxHeight) .heightIn(max = maxHeight)
.animateContentSize() .let { if (animateSize) it.animateContentSize() else it }
.padding(top = 16.dp, bottom = 16.dp + bottomPadding), .padding(top = 16.dp, bottom = 16.dp + bottomPadding),
) { ) {
Row( Row(
@@ -1,5 +1,8 @@
package io.legado.app.ui.book.read.sheet 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.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@@ -15,20 +18,26 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.res.stringArrayResource 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.core.net.toUri
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import io.legado.app.R 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.book.read.ReadBookIntent
import io.legado.app.ui.widget.components.FontSelectSheet import io.legado.app.ui.widget.components.FontSelectSheet
import io.legado.app.ui.widget.components.dialog.ColorPickerSheet 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.TinyClickableSettingItem
import io.legado.app.ui.widget.components.settingItem.TinyColorSettingItem import io.legado.app.ui.widget.components.settingItem.TinyColorSettingItem
import io.legado.app.ui.widget.components.settingItem.TinyDropdownSettingItem 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.ui.widget.components.tabRow.CardTabRow
import io.legado.app.utils.getCompatColor import io.legado.app.utils.getCompatColor
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.compose.koinInject
// Color picker IDs // Color picker IDs
private const val COLOR_HEADER = 7 private const val COLOR_HEADER = 7
@@ -70,6 +79,9 @@ internal fun HeaderFooterPage(
val pagerState = rememberPagerState(pageCount = { 3 }) val pagerState = rememberPagerState(pageCount = { 3 })
var selectedTab by remember { mutableIntStateOf(0) } var selectedTab by remember { mutableIntStateOf(0) }
val pageHeights = remember { mutableStateMapOf<Int, Int>() }
val animatedHeight by rememberPagerAnimatedHeight(pagerState, pageHeights)
// Header state // Header state
var headerMode by remember { mutableIntStateOf(ReadBookConfig.headerMode) } var headerMode by remember { mutableIntStateOf(ReadBookConfig.headerMode) }
var headerLeft by remember { mutableIntStateOf(ReadBookConfig.tipHeaderLeft) } var headerLeft by remember { mutableIntStateOf(ReadBookConfig.tipHeaderLeft) }
@@ -138,15 +150,31 @@ internal fun HeaderFooterPage(
selectedTabIndex = selectedTab, selectedTabIndex = selectedTab,
onTabSelected = { index -> onTabSelected = { index ->
selectedTab = 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), modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp),
) )
HorizontalPager( HorizontalPager(
state = pagerState, state = pagerState,
modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.Top,
modifier = Modifier
.fillMaxWidth()
.clipToBounds()
.pagerHeight(animatedHeight),
) { page -> ) { page ->
when (page) { Box(
modifier = Modifier
.fillMaxWidth()
.onSizeChanged { size ->
pageHeights[page] = size.height
}
) {
when (page) {
0 -> { 0 -> {
// Header tab // Header tab
Column( Column(
@@ -372,6 +400,7 @@ internal fun HeaderFooterPage(
} }
} }
} }
}
// Color picker // Color picker
ColorPickerSheet( ColorPickerSheet(
@@ -1,6 +1,8 @@
package io.legado.app.ui.book.read.sheet 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.Column
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding 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.foundation.pager.rememberPagerState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.legado.app.R import io.legado.app.R
import io.legado.app.ui.book.read.ConfigUpdate import io.legado.app.ui.book.read.ConfigUpdate
@@ -44,6 +54,9 @@ fun ReadStyleContent(
val pagerState = rememberPagerState(pageCount = { 3 }) val pagerState = rememberPagerState(pageCount = { 3 })
var currentPage by remember { mutableIntStateOf(0) } var currentPage by remember { mutableIntStateOf(0) }
val pageHeights = remember { mutableStateMapOf<Int, Int>() }
val animatedHeight by rememberPagerAnimatedHeight(pagerState, pageHeights)
LaunchedEffect(pagerState) { LaunchedEffect(pagerState) {
snapshotFlow { pagerState.currentPage }.collect { page -> snapshotFlow { pagerState.currentPage }.collect { page ->
currentPage = page currentPage = page
@@ -57,35 +70,46 @@ fun ReadStyleContent(
) { ) {
HorizontalPager( HorizontalPager(
state = pagerState, state = pagerState,
verticalAlignment = Alignment.Top,
modifier = Modifier modifier = Modifier
.weight(1f, fill = false) .weight(1f, fill = false)
.animateContentSize(), .clipToBounds()
.pagerHeight(animatedHeight),
) { page -> ) { page ->
when (page) { Box(
0 -> GlobalThemePage( modifier = Modifier
onToggleDayNight = onToggleDayNight, .fillMaxWidth()
onOpenBgTextConfig = onOpenBgTextConfig, .onSizeChanged { size ->
onOpenTextTitle = onOpenTextTitle, pageHeights[page] = size.height
onOpenPaddingConfig = onOpenPaddingConfig, }
onShareLayoutChange = { shareLayout -> ) {
onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.ShareLayout(shareLayout))) when (page) {
}, 0 -> GlobalThemePage(
onStyleSelect = { index -> onToggleDayNight = onToggleDayNight,
onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.StyleSelect(index))) onOpenBgTextConfig = onOpenBgTextConfig,
}, onOpenTextTitle = onOpenTextTitle,
modifier = Modifier.padding(horizontal = 16.dp), onOpenPaddingConfig = onOpenPaddingConfig,
onIntent = onIntent, onShareLayoutChange = { shareLayout ->
styleConfig = styleConfig, 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( 1 -> SystemMenuPage(
customIcons = readMenuCustomIcons, customIcons = readMenuCustomIcons,
bottomBarButtons = bottomBarButtons, bottomBarButtons = bottomBarButtons,
onIntent = onIntent, onIntent = onIntent,
) )
2 -> HeaderFooterPage(
onIntent = onIntent, 2 -> HeaderFooterPage(
) onIntent = onIntent,
)
}
} }
} }
@@ -100,7 +124,12 @@ fun ReadStyleContent(
selectedTabIndex = currentPage, selectedTabIndex = currentPage,
onTabSelected = { index -> onTabSelected = { index ->
if (index < 3) { if (index < 3) {
scope.launch { pagerState.animateScrollToPage(index) } scope.launch {
pagerState.animateScrollToPage(
page = index,
animationSpec = tween(durationMillis = 400, easing = FastOutSlowInEasing)
)
}
} else { } else {
onOpenMoreConfig() onOpenMoreConfig()
} }
@@ -109,3 +138,44 @@ 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)
}
}
@Composable
internal fun rememberPagerAnimatedHeight(
pagerState: androidx.compose.foundation.pager.PagerState,
pageHeights: Map<Int, Int>
): androidx.compose.runtime.State<Dp> {
val density = LocalDensity.current
return 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
}
}
}
}
@@ -2,6 +2,8 @@ package io.legado.app.ui.book.read.sheet
import android.content.Context import android.content.Context
import androidx.compose.animation.AnimatedVisibility 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.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth 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.SkipNext
import androidx.compose.material.icons.filled.SkipPrevious import androidx.compose.material.icons.filled.SkipPrevious
import androidx.compose.material.icons.filled.Translate import androidx.compose.material.icons.filled.Translate
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.vector.ImageVector 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.platform.LocalContext
import androidx.compose.ui.res.stringArrayResource import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import io.legado.app.R import io.legado.app.R
@@ -89,6 +99,9 @@ internal fun SystemMenuPage(
val pagerState = rememberPagerState(pageCount = { 3 }) val pagerState = rememberPagerState(pageCount = { 3 })
var selectedTab by remember { mutableIntStateOf(0) } var selectedTab by remember { mutableIntStateOf(0) }
val pageHeights = remember { mutableStateMapOf<Int, Int>() }
val animatedHeight by rememberPagerAnimatedHeight(pagerState, pageHeights)
// Shared state for sheets // Shared state for sheets
var showColorPicker by remember { mutableStateOf(false) } var showColorPicker by remember { mutableStateOf(false) }
var colorPickerId by remember { mutableIntStateOf(0) } var colorPickerId by remember { mutableIntStateOf(0) }
@@ -112,39 +125,58 @@ internal fun SystemMenuPage(
selectedTabIndex = selectedTab, selectedTabIndex = selectedTab,
onTabSelected = { index -> onTabSelected = { index ->
selectedTab = 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), modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp),
) )
HorizontalPager( HorizontalPager(
state = pagerState, state = pagerState,
modifier = Modifier.weight(1f, fill = false), verticalAlignment = Alignment.Top,
modifier = Modifier
.weight(1f, fill = false)
.clipToBounds()
.pagerHeight(animatedHeight),
) { page -> ) { page ->
when (page) { Box(
0 -> GlobalMenuTab( modifier = Modifier
preferences = preferences, .fillMaxWidth()
readBarStyle = preferences.readBarStyle, .onSizeChanged { size ->
readMenuColorMode = preferences.readMenuColorMode, pageHeights[page] = size.height
onIntent = onIntent, }
onShowColorPicker = { id, initial -> ) {
colorPickerId = id when (page) {
colorPickerInitial = initial 0 -> GlobalMenuTab(
showColorPicker = true preferences = preferences,
}, readBarStyle = preferences.readBarStyle,
) readMenuColorMode = preferences.readMenuColorMode,
1 -> BottomBarTab( onIntent = onIntent,
preferences = preferences, onShowColorPicker = { id, initial ->
customIcons = customIcons, colorPickerId = id
onIntent = onIntent, colorPickerInitial = initial
onShowIconSheet = { showIconSheet = true }, showColorPicker = true
onShowColorPicker = { id, initial -> },
colorPickerId = id )
colorPickerInitial = initial
showColorPicker = true 1 -> BottomBarTab(
}, preferences = preferences,
) customIcons = customIcons,
2 -> TopBarTab(preferences = preferences, onIntent = onIntent) onIntent = onIntent,
onShowIconSheet = { showIconSheet = true },
onShowColorPicker = { id, initial ->
colorPickerId = id
colorPickerInitial = initial
showColorPicker = true
},
)
2 -> TopBarTab(preferences = preferences, onIntent = onIntent)
}
} }
} }
} }