Преглед на файлове

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

@fansangg @贡献者
Kudomaga преди 4 месеца
родител
ревизия
1beefc59a2

+ 3 - 1
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(

+ 35 - 6
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.platform.LocalDensity
+import androidx.compose.ui.res.stringArrayResource
 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.unit.dp
-import androidx.compose.ui.res.stringArrayResource
 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,9 @@ internal fun HeaderFooterPage(
     val pagerState = rememberPagerState(pageCount = { 3 })
     var selectedTab by remember { mutableIntStateOf(0) }
 
+    val pageHeights = remember { mutableStateMapOf<Int, Int>() }
+    val animatedHeight by rememberPagerAnimatedHeight(pagerState, pageHeights)
+
     // Header state
     var headerMode by remember { mutableIntStateOf(ReadBookConfig.headerMode) }
     var headerLeft by remember { mutableIntStateOf(ReadBookConfig.tipHeaderLeft) }
@@ -138,15 +150,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 +400,7 @@ internal fun HeaderFooterPage(
             }
         }
     }
+}
 
     // Color picker
     ColorPickerSheet(

+ 97 - 27
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,9 @@ fun ReadStyleContent(
     val pagerState = rememberPagerState(pageCount = { 3 })
     var currentPage by remember { mutableIntStateOf(0) }
 
+    val pageHeights = remember { mutableStateMapOf<Int, Int>() }
+    val animatedHeight by rememberPagerAnimatedHeight(pagerState, pageHeights)
+
     LaunchedEffect(pagerState) {
         snapshotFlow { pagerState.currentPage }.collect { page ->
             currentPage = page
@@ -57,35 +70,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,
+                    )
 
-                1 -> SystemMenuPage(
-                    customIcons = readMenuCustomIcons,
-                    bottomBarButtons = bottomBarButtons,
-                    onIntent = onIntent,
-                )
-                2 -> HeaderFooterPage(
-                    onIntent = onIntent,
-                )
+                    2 -> HeaderFooterPage(
+                        onIntent = onIntent,
+                    )
+                }
             }
         }
 
@@ -100,7 +124,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 +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
+            }
+        }
+    }
+}

+ 58 - 26
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,9 @@ internal fun SystemMenuPage(
     val pagerState = rememberPagerState(pageCount = { 3 })
     var selectedTab by remember { mutableIntStateOf(0) }
 
+    val pageHeights = remember { mutableStateMapOf<Int, Int>() }
+    val animatedHeight by rememberPagerAnimatedHeight(pagerState, pageHeights)
+
     // Shared state for sheets
     var showColorPicker by remember { mutableStateOf(false) }
     var colorPickerId by remember { mutableIntStateOf(0) }
@@ -112,39 +125,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)
+                }
             }
         }
     }