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

feat:字典弹窗内容支持滑动切换

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

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

@@ -54,6 +54,8 @@ import io.legado.app.ui.widget.components.FontFolderState
 import io.legado.app.ui.widget.components.FontSelectSheet
 import io.legado.app.ui.widget.components.FontSelectSheet
 import io.legado.app.ui.widget.components.SectionTitle
 import io.legado.app.ui.widget.components.SectionTitle
 import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
 import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
+import io.legado.app.ui.widget.components.pager.pagerHeight
+import io.legado.app.ui.widget.components.pager.rememberPagerAnimatedHeight
 import io.legado.app.ui.widget.components.pager.rememberPagerFlingPassThroughConnection
 import io.legado.app.ui.widget.components.pager.rememberPagerFlingPassThroughConnection
 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

+ 2 - 49
app/src/main/java/io/legado/app/ui/book/read/sheet/ReadStyleSheet.kt

@@ -11,7 +11,6 @@ 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.mutableStateMapOf
@@ -22,11 +21,8 @@ import androidx.compose.runtime.snapshotFlow
 import androidx.compose.ui.Alignment
 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.draw.clipToBounds
-import androidx.compose.ui.layout.layout
 import androidx.compose.ui.layout.onSizeChanged
 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.data.repository.ReadPreferences
 import io.legado.app.data.repository.ReadPreferences
@@ -34,6 +30,8 @@ import io.legado.app.ui.book.read.ConfigUpdate
 import io.legado.app.ui.book.read.ReadBookButtonConfigItem
 import io.legado.app.ui.book.read.ReadBookButtonConfigItem
 import io.legado.app.ui.book.read.ReadBookIntent
 import io.legado.app.ui.book.read.ReadBookIntent
 import io.legado.app.ui.book.read.ReadBookStyleConfig
 import io.legado.app.ui.book.read.ReadBookStyleConfig
+import io.legado.app.ui.widget.components.pager.pagerHeight
+import io.legado.app.ui.widget.components.pager.rememberPagerAnimatedHeight
 import io.legado.app.ui.widget.components.pager.rememberPagerFlingPassThroughConnection
 import io.legado.app.ui.widget.components.pager.rememberPagerFlingPassThroughConnection
 import io.legado.app.ui.widget.components.tabRow.CardTabRow
 import io.legado.app.ui.widget.components.tabRow.CardTabRow
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
@@ -147,48 +145,3 @@ 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().coerceIn(constraints.minHeight, constraints.maxHeight)
-    } 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 - 0
app/src/main/java/io/legado/app/ui/book/read/sheet/SystemMenuPage.kt

@@ -65,6 +65,8 @@ import io.legado.app.ui.book.read.ReadBookSheet
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.widget.components.SectionTitle
 import io.legado.app.ui.widget.components.SectionTitle
 import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
 import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
+import io.legado.app.ui.widget.components.pager.pagerHeight
+import io.legado.app.ui.widget.components.pager.rememberPagerAnimatedHeight
 import io.legado.app.ui.widget.components.pager.rememberPagerFlingPassThroughConnection
 import io.legado.app.ui.widget.components.pager.rememberPagerFlingPassThroughConnection
 import io.legado.app.ui.widget.components.settingItem.TinyClearColorModeSettingItem
 import io.legado.app.ui.widget.components.settingItem.TinyClearColorModeSettingItem
 import io.legado.app.ui.widget.components.settingItem.TinyClickableSettingItem
 import io.legado.app.ui.widget.components.settingItem.TinyClickableSettingItem

+ 2 - 0
app/src/main/java/io/legado/app/ui/book/read/sheet/TextTitleSheet.kt

@@ -56,6 +56,8 @@ import io.legado.app.ui.widget.components.SectionTitle
 import io.legado.app.ui.widget.components.alert.AppAlertDialog
 import io.legado.app.ui.widget.components.alert.AppAlertDialog
 import io.legado.app.ui.widget.components.card.NormalCard
 import io.legado.app.ui.widget.components.card.NormalCard
 import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
 import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
+import io.legado.app.ui.widget.components.pager.pagerHeight
+import io.legado.app.ui.widget.components.pager.rememberPagerAnimatedHeight
 import io.legado.app.ui.widget.components.pager.rememberPagerFlingPassThroughConnection
 import io.legado.app.ui.widget.components.pager.rememberPagerFlingPassThroughConnection
 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

+ 7 - 1
app/src/main/java/io/legado/app/ui/dict/DictContract.kt

@@ -7,9 +7,15 @@ import kotlinx.collections.immutable.persistentListOf
 @Stable
 @Stable
 data class DictUiState(
 data class DictUiState(
     val word: String = "",
     val word: String = "",
-    val isLoading: Boolean = false,
     val rules: ImmutableList<DictRuleUi> = persistentListOf(),
     val rules: ImmutableList<DictRuleUi> = persistentListOf(),
     val selectedIndex: Int = 0,
     val selectedIndex: Int = 0,
+    val pages: ImmutableList<DictPageUiState> = persistentListOf(),
+    val emptyReason: DictEmptyReason? = null,
+)
+
+@Stable
+data class DictPageUiState(
+    val isLoading: Boolean = false,
     val htmlContent: String = "",
     val htmlContent: String = "",
     val emptyReason: DictEmptyReason? = null,
     val emptyReason: DictEmptyReason? = null,
 )
 )

+ 132 - 17
app/src/main/java/io/legado/app/ui/dict/DictSheet.kt

@@ -1,5 +1,10 @@
 package io.legado.app.ui.dict
 package io.legado.app.ui.dict
 
 
+import androidx.compose.animation.core.FastOutSlowInEasing
+import androidx.compose.animation.core.spring
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.rememberPagerState
 import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.verticalScroll
 import androidx.compose.foundation.verticalScroll
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Box
@@ -10,10 +15,16 @@ import androidx.compose.foundation.layout.padding
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateMapOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.snapshotFlow
 import androidx.compose.ui.Alignment
 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.ContentScale
 import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.text.ExperimentalTextApi
 import androidx.compose.ui.text.ExperimentalTextApi
@@ -23,11 +34,16 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import coil.compose.AsyncImage
 import coil.compose.AsyncImage
 import io.legado.app.R
 import io.legado.app.R
 import io.legado.app.ui.widget.components.EmptyMessage
 import io.legado.app.ui.widget.components.EmptyMessage
-import io.legado.app.ui.widget.components.text.AppText
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
+import io.legado.app.ui.widget.components.pager.pagerHeight
+import io.legado.app.ui.widget.components.pager.rememberPagerAnimatedHeight
 import io.legado.app.ui.widget.components.progressIndicator.AppCircularProgressIndicator
 import io.legado.app.ui.widget.components.progressIndicator.AppCircularProgressIndicator
 import io.legado.app.ui.widget.components.tabRow.AppTabRow
 import io.legado.app.ui.widget.components.tabRow.AppTabRow
+import io.legado.app.ui.widget.components.text.AppText
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.drop
+import kotlinx.coroutines.launch
 import org.koin.androidx.compose.koinViewModel
 import org.koin.androidx.compose.koinViewModel
 
 
 @Composable
 @Composable
@@ -49,6 +65,8 @@ fun DictSheet(
         show = show,
         show = show,
         onDismissRequest = onDismissRequest,
         onDismissRequest = onDismissRequest,
         title = word,
         title = word,
+        //内部pager管理高度,避免二次动画
+        animateContentSize = false,
     ) {
     ) {
         DictSheetContent(
         DictSheetContent(
             state = state,
             state = state,
@@ -93,28 +111,125 @@ private fun DictContent(
     val emptyText = stringResource(R.string.empty)
     val emptyText = stringResource(R.string.empty)
     val searchEmptyText = stringResource(R.string.search_empty)
     val searchEmptyText = stringResource(R.string.search_empty)
     val tabTitles = remember(state.rules) { state.rules.map { it.name } }
     val tabTitles = remember(state.rules) { state.rules.map { it.name } }
-    val emptyMessage = when (state.emptyReason) {
-        DictEmptyReason.BlankWord -> blankWordText
-        DictEmptyReason.NoRules -> emptyText
-        DictEmptyReason.NoResult -> searchEmptyText
-        null -> null
+    val emptyMessageFor: (DictEmptyReason?) -> String? = { reason ->
+        when (reason) {
+            DictEmptyReason.BlankWord -> blankWordText
+            DictEmptyReason.NoRules -> emptyText
+            DictEmptyReason.NoResult -> searchEmptyText
+            null -> null
+        }
+    }
+    val selectedIndex = state.selectedIndex.coerceIn(0, state.rules.lastIndex.coerceAtLeast(0))
+
+    if (state.rules.size > 1) {
+        DictPagerContent(
+            state = state,
+            tabTitles = tabTitles,
+            selectedIndex = selectedIndex,
+            emptyMessageFor = emptyMessageFor,
+            onIntent = onIntent,
+            modifier = modifier,
+        )
+    } else {
+        val pageState = state.pages.getOrNull(selectedIndex)
+            ?: DictPageUiState(emptyReason = state.emptyReason)
+        DictPageContent(
+            state = pageState,
+            emptyMessage = emptyMessageFor(pageState.emptyReason),
+            modifier = modifier,
+        )
     }
     }
+}
+
+@Composable
+private fun DictPagerContent(
+    state: DictUiState,
+    tabTitles: List<String>,
+    selectedIndex: Int,
+    emptyMessageFor: (DictEmptyReason?) -> String?,
+    onIntent: (DictIntent) -> Unit,
+    modifier: Modifier = Modifier,
+) {
+    val scope = rememberCoroutineScope()
+    val currentOnIntent by rememberUpdatedState(onIntent)
+    val pagerState = rememberPagerState(initialPage = selectedIndex) { state.rules.size }
+    val pageHeights = remember(state.word, state.rules) { mutableStateMapOf<Int, Int>() }
+    val animatedHeight by rememberPagerAnimatedHeight(
+        pagerState = pagerState,
+        pageHeights = pageHeights,
+        fallbackHeight = 200.dp,
+        heightAnimationSpec = spring(),
+    )
 
 
     Column(
     Column(
-        modifier = modifier.verticalScroll(rememberScrollState()),
+        modifier = modifier,
     ) {
     ) {
-        if (state.rules.size > 1) {
-            AppTabRow(
-                tabTitles = tabTitles,
-                selectedTabIndex = state.selectedIndex.coerceIn(0, state.rules.lastIndex),
-                onTabSelected = { index ->
-                    onIntent(DictIntent.SelectRule(index))
-                },
-                modifier = Modifier.fillMaxWidth(),
-                isScrollable = true,
-            )
+        AppTabRow(
+            tabTitles = tabTitles,
+            selectedTabIndex = pagerState.currentPage.coerceIn(0, state.rules.lastIndex),
+            onTabSelected = { index ->
+                scope.launch {
+                    pagerState.animateScrollToPage(
+                        page = index,
+                        animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing),
+                    )
+                }
+            },
+            modifier = Modifier.fillMaxWidth(),
+            isScrollable = true,
+        )
+
+        HorizontalPager(
+            state = pagerState,
+            verticalAlignment = Alignment.Top,
+            overscrollEffect = null,
+            modifier = Modifier
+                .weight(1f, fill = false)
+                .clipToBounds()
+                .pagerHeight(animatedHeight),
+        ) { page ->
+            Box(
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .onSizeChanged { size ->
+                        pageHeights[page] = size.height
+                    }
+            ) {
+                val pageState = state.pages.getOrNull(page) ?: DictPageUiState()
+                DictPageContent(
+                    state = pageState,
+                    emptyMessage = emptyMessageFor(pageState.emptyReason),
+                    modifier = Modifier.fillMaxWidth(),
+                )
+            }
         }
         }
+    }
+
+    LaunchedEffect(selectedIndex, state.rules.size) {
+        if (pagerState.currentPage != selectedIndex) {
+            pagerState.scrollToPage(selectedIndex)
+        }
+    }
 
 
+    LaunchedEffect(pagerState) {
+        snapshotFlow { pagerState.settledPage }
+            .distinctUntilChanged()
+            .drop(1)
+            .collect { page ->
+                currentOnIntent(DictIntent.SelectRule(page))
+            }
+    }
+}
+
+@Composable
+private fun DictPageContent(
+    state: DictPageUiState,
+    emptyMessage: String?,
+    modifier: Modifier = Modifier,
+) {
+    Column(
+        modifier = modifier.verticalScroll(rememberScrollState()),
+    ) {
         when {
         when {
             state.isLoading -> {
             state.isLoading -> {
                 Box(
                 Box(

+ 65 - 18
app/src/main/java/io/legado/app/ui/dict/DictViewModel.kt

@@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope
 import io.legado.app.data.entities.DictRule
 import io.legado.app.data.entities.DictRule
 import io.legado.app.data.repository.DictRuleRepository
 import io.legado.app.data.repository.DictRuleRepository
 import kotlinx.collections.immutable.toImmutableList
 import kotlinx.collections.immutable.toImmutableList
+import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableSharedFlow
@@ -40,7 +41,6 @@ class DictViewModel(
         val query = word.trim()
         val query = word.trim()
         _uiState.value = DictUiState(
         _uiState.value = DictUiState(
             word = query,
             word = query,
-            isLoading = query.isNotBlank(),
             emptyReason = if (query.isBlank()) DictEmptyReason.BlankWord else null,
             emptyReason = if (query.isBlank()) DictEmptyReason.BlankWord else null,
         )
         )
         if (query.isBlank()) return
         if (query.isBlank()) return
@@ -52,7 +52,7 @@ class DictViewModel(
             _uiState.update { state ->
             _uiState.update { state ->
                 state.copy(
                 state.copy(
                     rules = dictRules.map { DictRuleUi(it.name) }.toImmutableList(),
                     rules = dictRules.map { DictRuleUi(it.name) }.toImmutableList(),
-                    isLoading = dictRules.isNotEmpty(),
+                    pages = dictRules.map { DictPageUiState() }.toImmutableList(),
                     emptyReason = if (dictRules.isEmpty()) DictEmptyReason.NoRules else null,
                     emptyReason = if (dictRules.isEmpty()) DictEmptyReason.NoRules else null,
                 )
                 )
             }
             }
@@ -64,7 +64,16 @@ class DictViewModel(
 
 
     private fun selectRule(index: Int) {
     private fun selectRule(index: Int) {
         val state = _uiState.value
         val state = _uiState.value
-        if (index !in dictRules.indices || state.selectedIndex == index && state.isLoading) return
+        if (index !in dictRules.indices || index !in state.pages.indices) return
+        val page = state.pages[index]
+        if (page.isLoading) {
+            _uiState.update { it.copy(selectedIndex = index) }
+            return
+        }
+        if (page.htmlContent.isNotBlank() || page.emptyReason == DictEmptyReason.NoResult) {
+            _uiState.update { it.copy(selectedIndex = index) }
+            return
+        }
         searchRule(index = index, word = state.word)
         searchRule(index = index, word = state.word)
     }
     }
 
 
@@ -74,32 +83,58 @@ class DictViewModel(
         _uiState.update {
         _uiState.update {
             it.copy(
             it.copy(
                 selectedIndex = index,
                 selectedIndex = index,
-                isLoading = true,
-                htmlContent = "",
                 emptyReason = null,
                 emptyReason = null,
-            )
+            ).updatePage(index) { page ->
+                page.copy(
+                    isLoading = true,
+                    htmlContent = "",
+                    emptyReason = null,
+                )
+            }
         }
         }
         dictJob = viewModelScope.launch {
         dictJob = viewModelScope.launch {
-            val result = runCatching {
+            val result = try {
                 withContext(Dispatchers.IO) {
                 withContext(Dispatchers.IO) {
                     rule.search(word)
                     rule.search(word)
                 }
                 }
-            }.getOrElse { error ->
+            } catch (error: CancellationException) {
+                _uiState.update { state ->
+                    if (state.word == word && dictRules.getOrNull(index) === rule) {
+                        state.updatePage(index) { page ->
+                            if (page.isLoading) {
+                                page.copy(isLoading = false)
+                            } else {
+                                page
+                            }
+                        }
+                    } else {
+                        state
+                    }
+                }
+                throw error
+            } catch (error: Throwable) {
                 error.localizedMessage ?: "ERROR"
                 error.localizedMessage ?: "ERROR"
             }
             }
             _uiState.update {
             _uiState.update {
+                if (it.word != word || dictRules.getOrNull(index) !== rule) {
+                    return@update it
+                }
                 if (result.isBlank()) {
                 if (result.isBlank()) {
-                    it.copy(
-                        isLoading = false,
-                        htmlContent = "",
-                        emptyReason = DictEmptyReason.NoResult,
-                    )
+                    it.updatePage(index) { page ->
+                        page.copy(
+                            isLoading = false,
+                            htmlContent = "",
+                            emptyReason = DictEmptyReason.NoResult,
+                        )
+                    }
                 } else {
                 } else {
-                    it.copy(
-                        isLoading = false,
-                        htmlContent = result,
-                        emptyReason = null,
-                    )
+                    it.updatePage(index) { page ->
+                        page.copy(
+                            isLoading = false,
+                            htmlContent = result,
+                            emptyReason = null,
+                        )
+                    }
                 }
                 }
             }
             }
         }
         }
@@ -110,3 +145,15 @@ class DictViewModel(
         super.onCleared()
         super.onCleared()
     }
     }
 }
 }
+
+private fun DictUiState.updatePage(
+    index: Int,
+    transform: (DictPageUiState) -> DictPageUiState,
+): DictUiState {
+    if (index !in pages.indices) return this
+    return copy(
+        pages = pages.mapIndexed { pageIndex, page ->
+            if (pageIndex == index) transform(page) else page
+        }.toImmutableList()
+    )
+}

+ 9 - 2
app/src/main/java/io/legado/app/ui/widget/components/modalBottomSheet/AppModalBottomSheet.kt

@@ -48,6 +48,7 @@ fun AppModalBottomSheet(
     title: String? = null,
     title: String? = null,
     startAction: @Composable (() -> Unit)? = null,
     startAction: @Composable (() -> Unit)? = null,
     endAction: @Composable (() -> Unit)? = null,
     endAction: @Composable (() -> Unit)? = null,
+    animateContentSize: Boolean = true,
     contentWindowInsets: @Composable () -> WindowInsets = { BottomSheetDefaults.modalWindowInsets },
     contentWindowInsets: @Composable () -> WindowInsets = { BottomSheetDefaults.modalWindowInsets },
     content: @Composable ColumnScope.() -> Unit
     content: @Composable ColumnScope.() -> Unit
 ) {
 ) {
@@ -92,7 +93,9 @@ fun AppModalBottomSheet(
                         modifier = Modifier
                         modifier = Modifier
                             .fillMaxWidth()
                             .fillMaxWidth()
                             .padding(bottom = 24.dp)
                             .padding(bottom = 24.dp)
-                            .animateContentSize(),
+                            .let { contentModifier ->
+                                if (animateContentSize) contentModifier.animateContentSize() else contentModifier
+                            },
                         content = content
                         content = content
                     )
                     )
                 }
                 }
@@ -125,7 +128,9 @@ fun AppModalBottomSheet(
                             .fillMaxWidth()
                             .fillMaxWidth()
                             .padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
                             .padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
                             .heightIn(max = maxHeight)
                             .heightIn(max = maxHeight)
-                            .animateContentSize()
+                            .let { contentModifier ->
+                                if (animateContentSize) contentModifier.animateContentSize() else contentModifier
+                            }
                             .then(modifier)
                             .then(modifier)
                     ) {
                     ) {
                         val hasHeader =
                         val hasHeader =
@@ -185,6 +190,7 @@ fun <T> AppModalBottomSheet(
     title: String? = null,
     title: String? = null,
     startAction: @Composable (() -> Unit)? = null,
     startAction: @Composable (() -> Unit)? = null,
     endAction: @Composable (() -> Unit)? = null,
     endAction: @Composable (() -> Unit)? = null,
+    animateContentSize: Boolean = true,
     contentWindowInsets: @Composable () -> WindowInsets = { BottomSheetDefaults.modalWindowInsets },
     contentWindowInsets: @Composable () -> WindowInsets = { BottomSheetDefaults.modalWindowInsets },
     content: @Composable ColumnScope.(T) -> Unit
     content: @Composable ColumnScope.(T) -> Unit
 ) {
 ) {
@@ -202,6 +208,7 @@ fun <T> AppModalBottomSheet(
         title = title,
         title = title,
         startAction = startAction,
         startAction = startAction,
         endAction = endAction,
         endAction = endAction,
+        animateContentSize = animateContentSize,
         contentWindowInsets = contentWindowInsets,
         contentWindowInsets = contentWindowInsets,
         content = {
         content = {
             if (currentData != null) {
             if (currentData != null) {

+ 77 - 0
app/src/main/java/io/legado/app/ui/widget/components/pager/PagerHeight.kt

@@ -0,0 +1,77 @@
+package io.legado.app.ui.widget.components.pager
+
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.foundation.pager.PagerState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.Dp
+
+fun Modifier.pagerHeight(height: Dp) = this.layout { measurable, constraints ->
+    val placeable = measurable.measure(constraints)
+    val layoutHeight = if (height != Dp.Unspecified) {
+        height.roundToPx().coerceIn(constraints.minHeight, constraints.maxHeight)
+    } else {
+        placeable.height
+    }
+    layout(placeable.width, layoutHeight) {
+        placeable.placeRelative(0, 0)
+    }
+}
+
+@Composable
+fun rememberPagerAnimatedHeight(
+    pagerState: PagerState,
+    pageHeights: Map<Int, Int>,
+    fallbackHeight: Dp = Dp.Unspecified,
+    heightAnimationSpec: FiniteAnimationSpec<Dp>? = null,
+): State<Dp> {
+    val density = LocalDensity.current
+    val fallbackHeightPx = with(density) {
+        fallbackHeight.takeIf { it != Dp.Unspecified }?.roundToPx() ?: 0
+    }
+    val targetHeight = remember(pagerState, pageHeights, density, fallbackHeightPx) {
+        derivedStateOf {
+            val pageCount = pagerState.pageCount
+            if (pageCount <= 0) {
+                return@derivedStateOf Dp.Unspecified
+            }
+            val position = (pagerState.currentPage + pagerState.currentPageOffsetFraction)
+                .coerceIn(0f, (pageCount - 1).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 knownHeight = pageHeights.values.firstOrNull { it > 0 } ?: fallbackHeightPx
+            val unknownPageHeight = fallbackHeightPx.takeIf { it > 0 } ?: knownHeight
+            val startHeight = if (floorHeight > 0) floorHeight else unknownPageHeight
+            val endHeight = if (ceilHeight > 0) ceilHeight else unknownPageHeight
+            val interpolated = startHeight + (endHeight - startHeight) * fraction
+
+            if (interpolated > 0) {
+                with(density) { interpolated.toDp() }
+            } else {
+                Dp.Unspecified
+            }
+        }
+    }
+    if (heightAnimationSpec == null) {
+        return targetHeight
+    }
+    return if (targetHeight.value == Dp.Unspecified) {
+        targetHeight
+    } else {
+        animateDpAsState(
+            targetValue = targetHeight.value,
+            animationSpec = heightAnimationSpec,
+        )
+    }
+}