[新增] 悬浮底栏和液态玻璃底栏
This commit is contained in:
@@ -207,6 +207,8 @@ object PreferKey {
|
||||
const val themePref = "themePref"
|
||||
const val bookshelfSortOrder = "bookshelfSortOrder"
|
||||
const val showBottomView = "showBottomView"
|
||||
const val useFloatingBottomBar = "useFloatingBottomBar"
|
||||
const val useFloatingBottomBarLiquidGlass = "useFloatingBottomBarLiquidGlass"
|
||||
const val mangaScrollMode = "mangaScrollMode"
|
||||
const val webtoonSidePaddingDp = "webtoonSidePaddingDp"
|
||||
const val mangaVolumeKeyPage = "mangaVolumeKeyPage"
|
||||
@@ -267,6 +269,7 @@ object PreferKey {
|
||||
const val defaultSourceChangeAll = "defaultSourceChangeAll"
|
||||
|
||||
const val showTip = "showTip"
|
||||
const val showBookCount = "showBookCount"
|
||||
const val sliderVibrator = "sliderVibrator"
|
||||
const val selectVibrator = "selectVibrator"
|
||||
const val audioCacheCleanTime = "audioCacheCleanTime"
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
package io.legado.app.ui.animation
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.foundation.MutatorMutex
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.input.pointer.util.VelocityTracker
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import io.legado.app.ui.util.inspectDragGestures
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.android.awaitFrame
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.abs
|
||||
|
||||
class DampedDragAnimation(
|
||||
private val animationScope: CoroutineScope,
|
||||
val initialValue: Float,
|
||||
val valueRange: ClosedRange<Float>,
|
||||
val visibilityThreshold: Float,
|
||||
val initialScale: Float,
|
||||
val pressedScale: Float,
|
||||
val canDrag: (Offset) -> Boolean = { true },
|
||||
val onDragStarted: DampedDragAnimation.(position: Offset) -> Unit,
|
||||
val onDragStopped: DampedDragAnimation.() -> Unit,
|
||||
val onDrag: DampedDragAnimation.(size: IntSize, dragAmount: Offset) -> Unit,
|
||||
) {
|
||||
|
||||
private val valueAnimationSpec = spring(1f, 1000f, visibilityThreshold)
|
||||
private val velocityAnimationSpec = spring(0.5f, 300f, visibilityThreshold * 10f)
|
||||
private val pressProgressAnimationSpec = spring(1f, 1000f, 0.001f)
|
||||
private val scaleXAnimationSpec = spring(0.6f, 250f, 0.001f)
|
||||
private val scaleYAnimationSpec = spring(0.7f, 250f, 0.001f)
|
||||
|
||||
private val valueAnimation = Animatable(initialValue, visibilityThreshold)
|
||||
private val velocityAnimation = Animatable(0f, 5f)
|
||||
private val pressProgressAnimation = Animatable(0f, 0.001f)
|
||||
private val scaleXAnimation = Animatable(initialScale, 0.001f)
|
||||
private val scaleYAnimation = Animatable(initialScale, 0.001f)
|
||||
|
||||
private val mutatorMutex = MutatorMutex()
|
||||
private val velocityTracker = VelocityTracker()
|
||||
|
||||
val value: Float get() = valueAnimation.value
|
||||
val targetValue: Float get() = valueAnimation.targetValue
|
||||
val pressProgress: Float get() = pressProgressAnimation.value
|
||||
val scaleX: Float get() = scaleXAnimation.value
|
||||
val scaleY: Float get() = scaleYAnimation.value
|
||||
val velocity: Float get() = velocityAnimation.value
|
||||
|
||||
val modifier: Modifier = Modifier.pointerInput(Unit) {
|
||||
inspectDragGestures(
|
||||
onDragStart = { down ->
|
||||
onDragStarted(down.position)
|
||||
press()
|
||||
},
|
||||
onDragEnd = {
|
||||
onDragStopped()
|
||||
release()
|
||||
},
|
||||
onDragCancel = {
|
||||
onDragStopped()
|
||||
release()
|
||||
}
|
||||
) { change, dragAmount ->
|
||||
val position = change.position
|
||||
val previousPosition = change.previousPosition
|
||||
|
||||
val isInside = canDrag(position)
|
||||
val wasInside = canDrag(previousPosition)
|
||||
|
||||
if (isInside && wasInside) {
|
||||
onDrag(size, dragAmount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun press() {
|
||||
velocityTracker.resetTracking()
|
||||
animationScope.launch {
|
||||
launch { pressProgressAnimation.animateTo(1f, pressProgressAnimationSpec) }
|
||||
launch { scaleXAnimation.animateTo(pressedScale, scaleXAnimationSpec) }
|
||||
launch { scaleYAnimation.animateTo(pressedScale, scaleYAnimationSpec) }
|
||||
}
|
||||
}
|
||||
|
||||
fun release() {
|
||||
animationScope.launch {
|
||||
awaitFrame()
|
||||
if (value != targetValue) {
|
||||
val threshold = (valueRange.endInclusive - valueRange.start) * 0.025f
|
||||
snapshotFlow { valueAnimation.value }
|
||||
.filter { abs(it - valueAnimation.targetValue) < threshold }
|
||||
.first()
|
||||
}
|
||||
launch { pressProgressAnimation.animateTo(0f, pressProgressAnimationSpec) }
|
||||
launch { scaleXAnimation.animateTo(initialScale, scaleXAnimationSpec) }
|
||||
launch { scaleYAnimation.animateTo(initialScale, scaleYAnimationSpec) }
|
||||
}
|
||||
}
|
||||
|
||||
fun updateValue(value: Float) {
|
||||
val coercedTargetValue = value.coerceIn(valueRange)
|
||||
animationScope.launch {
|
||||
launch {
|
||||
valueAnimation.animateTo(coercedTargetValue, valueAnimationSpec) {
|
||||
updateVelocity()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun animateToValue(value: Float) {
|
||||
animationScope.launch {
|
||||
mutatorMutex.mutate {
|
||||
press()
|
||||
val coercedTargetValue = value.coerceIn(valueRange)
|
||||
launch { valueAnimation.animateTo(coercedTargetValue, valueAnimationSpec) }
|
||||
if (velocity != 0f) {
|
||||
launch { velocityAnimation.animateTo(0f, velocityAnimationSpec) }
|
||||
}
|
||||
release()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateVelocity() {
|
||||
velocityTracker.addPosition(
|
||||
System.currentTimeMillis(),
|
||||
Offset(value, 0f)
|
||||
)
|
||||
val targetVelocity =
|
||||
velocityTracker.calculateVelocity().x / (valueRange.endInclusive - valueRange.start)
|
||||
animationScope.launch {
|
||||
velocityAnimation.animateTo(targetVelocity, velocityAnimationSpec)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package io.legado.app.ui.animation
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.RuntimeShader
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.VectorConverter
|
||||
import androidx.compose.animation.core.VisibilityThreshold
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.BlendMode
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ShaderBrush
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.util.fastCoerceIn
|
||||
import io.legado.app.ui.util.inspectDragGestures
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.intellij.lang.annotations.Language
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
class InteractiveHighlight(
|
||||
val animationScope: CoroutineScope,
|
||||
val position: (size: Size, offset: Offset) -> Offset = { _, offset -> offset }
|
||||
) {
|
||||
|
||||
private val pressProgressAnimationSpec = spring(0.5f, 300f, 0.001f)
|
||||
private val positionAnimationSpec = spring(0.5f, 300f, Offset.VisibilityThreshold)
|
||||
|
||||
private val pressProgressAnimation = Animatable(0f, 0.001f)
|
||||
private val positionAnimation =
|
||||
Animatable(Offset.Zero, Offset.VectorConverter, Offset.VisibilityThreshold)
|
||||
|
||||
private var startPosition = Offset.Zero
|
||||
|
||||
@Language("AGSL")
|
||||
private val shader = RuntimeShader(
|
||||
"""
|
||||
uniform float2 size;
|
||||
layout(color) uniform half4 color;
|
||||
uniform float radius;
|
||||
uniform float2 position;
|
||||
|
||||
half4 main(float2 coord) {
|
||||
float dist = distance(coord, position);
|
||||
float intensity = smoothstep(radius, radius * 0.5, dist);
|
||||
return color * intensity;
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
val modifier: Modifier =
|
||||
Modifier.drawWithContent {
|
||||
val progress = pressProgressAnimation.value
|
||||
if (progress > 0f) {
|
||||
drawRect(
|
||||
Color.White.copy(0.06f * progress),
|
||||
blendMode = BlendMode.Plus
|
||||
)
|
||||
shader.apply {
|
||||
val currentPosition = position(size, positionAnimation.value)
|
||||
setFloatUniform("size", size.width, size.height)
|
||||
setColorUniform("color", Color.White.copy(0.12f * progress).toArgb())
|
||||
setFloatUniform("radius", size.minDimension * 1.2f)
|
||||
setFloatUniform(
|
||||
"position",
|
||||
currentPosition.x.fastCoerceIn(0f, size.width),
|
||||
currentPosition.y.fastCoerceIn(0f, size.height)
|
||||
)
|
||||
}
|
||||
drawRect(
|
||||
ShaderBrush(shader),
|
||||
blendMode = BlendMode.Plus
|
||||
)
|
||||
}
|
||||
|
||||
drawContent()
|
||||
}
|
||||
|
||||
val gestureModifier: Modifier =
|
||||
Modifier.pointerInput(animationScope) {
|
||||
inspectDragGestures(
|
||||
onDragStart = { down ->
|
||||
startPosition = down.position
|
||||
animationScope.launch {
|
||||
launch { pressProgressAnimation.animateTo(1f, pressProgressAnimationSpec) }
|
||||
launch { positionAnimation.snapTo(startPosition) }
|
||||
}
|
||||
},
|
||||
onDragEnd = {
|
||||
animationScope.launch {
|
||||
launch { pressProgressAnimation.animateTo(0f, pressProgressAnimationSpec) }
|
||||
launch { positionAnimation.animateTo(startPosition, positionAnimationSpec) }
|
||||
}
|
||||
},
|
||||
onDragCancel = {
|
||||
animationScope.launch {
|
||||
launch { pressProgressAnimation.animateTo(0f, pressProgressAnimationSpec) }
|
||||
launch { positionAnimation.animateTo(startPosition, positionAnimationSpec) }
|
||||
}
|
||||
}
|
||||
) { change, _ ->
|
||||
animationScope.launch { positionAnimation.snapTo(change.position) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,11 @@ object MainConfig {
|
||||
var showDiscovery by prefDelegate(PreferKey.showDiscovery, true)
|
||||
var showRSS by prefDelegate(PreferKey.showRss, true)
|
||||
var showBottomView by prefDelegate(PreferKey.showBottomView, true)
|
||||
var useFloatingBottomBar by prefDelegate(PreferKey.useFloatingBottomBar, false)
|
||||
var useFloatingBottomBarLiquidGlass by prefDelegate(
|
||||
PreferKey.useFloatingBottomBarLiquidGlass,
|
||||
false
|
||||
)
|
||||
var defaultHomePage by prefDelegate(PreferKey.defaultHomePage, "bookshelf")
|
||||
var tabletInterface by prefDelegate(PreferKey.tabletInterface, "auto")
|
||||
var labelVisibilityMode by prefDelegate(PreferKey.labelVisibilityMode, "auto")
|
||||
|
||||
@@ -72,6 +72,13 @@ object ThemeConfig {
|
||||
|
||||
var showBottomView by prefDelegate(PreferKey.showBottomView, true)
|
||||
|
||||
var useFloatingBottomBar by prefDelegate(PreferKey.useFloatingBottomBar, false)
|
||||
|
||||
var useFloatingBottomBarLiquidGlass by prefDelegate(
|
||||
PreferKey.useFloatingBottomBarLiquidGlass,
|
||||
false
|
||||
)
|
||||
|
||||
var tabletInterface by prefDelegate(PreferKey.tabletInterface, "auto")
|
||||
|
||||
var labelVisibilityMode by prefDelegate(PreferKey.labelVisibilityMode, "auto")
|
||||
|
||||
@@ -35,7 +35,6 @@ import androidx.compose.material.icons.filled.BrightnessMedium
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.DarkMode
|
||||
import androidx.compose.material.icons.filled.LightMode
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ButtonGroupDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
@@ -43,10 +42,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.ToggleButton
|
||||
import androidx.compose.material3.ToggleButtonDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -375,6 +371,20 @@ fun ThemeConfigScreen(
|
||||
checked = ThemeConfig.showBottomView,
|
||||
onCheckedChange = { ThemeConfig.showBottomView = it }
|
||||
)
|
||||
SwitchSettingItem(
|
||||
title = stringResource(R.string.floating_bottom_bar),
|
||||
description = stringResource(R.string.floating_bottom_bar_summary),
|
||||
checked = ThemeConfig.useFloatingBottomBar,
|
||||
onCheckedChange = { ThemeConfig.useFloatingBottomBar = it }
|
||||
)
|
||||
AnimatedVisibility(visible = ThemeConfig.useFloatingBottomBar) {
|
||||
SwitchSettingItem(
|
||||
title = stringResource(R.string.floating_bottom_bar_liquid_glass),
|
||||
description = stringResource(R.string.floating_bottom_bar_liquid_glass_summary),
|
||||
checked = ThemeConfig.useFloatingBottomBarLiquidGlass,
|
||||
onCheckedChange = { ThemeConfig.useFloatingBottomBarLiquidGlass = it }
|
||||
)
|
||||
}
|
||||
DropdownListSettingItem(
|
||||
title = stringResource(R.string.tabletInterface),
|
||||
selectedValue = ThemeConfig.tabletInterface,
|
||||
|
||||
@@ -1,68 +1,33 @@
|
||||
package io.legado.app.ui.main
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.LibraryBooks
|
||||
import androidx.compose.material.icons.automirrored.outlined.LibraryBooks
|
||||
import androidx.compose.material.icons.filled.Explore
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material.icons.filled.RssFeed
|
||||
import androidx.compose.material.icons.outlined.Explore
|
||||
import androidx.compose.material.icons.outlined.Person
|
||||
import androidx.compose.material.icons.outlined.RssFeed
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import io.legado.app.R
|
||||
import top.yukonga.miuix.kmp.icon.MiuixIcons
|
||||
import top.yukonga.miuix.kmp.icon.extended.Album
|
||||
import top.yukonga.miuix.kmp.icon.extended.ContactsBook
|
||||
import top.yukonga.miuix.kmp.icon.extended.Favorites
|
||||
import top.yukonga.miuix.kmp.icon.extended.Settings
|
||||
|
||||
sealed class MainDestination(
|
||||
val route: String,
|
||||
@StringRes val labelId: Int,
|
||||
val m3Icon: ImageVector,
|
||||
val m3SelectedIcon: ImageVector,
|
||||
val miuixIcon: ImageVector,
|
||||
val miuixSelectedIcon: ImageVector
|
||||
@StringRes val labelId: Int
|
||||
) {
|
||||
object Bookshelf : MainDestination(
|
||||
route = "bookshelf",
|
||||
labelId = R.string.bookshelf,
|
||||
m3Icon = Icons.AutoMirrored.Outlined.LibraryBooks,
|
||||
m3SelectedIcon = Icons.AutoMirrored.Filled.LibraryBooks,
|
||||
miuixIcon = MiuixIcons.Regular.ContactsBook, // 替换为实际的 MIUIX 线框图标
|
||||
miuixSelectedIcon = MiuixIcons.Heavy.ContactsBook // 替换为实际的 MIUIX 填充图标
|
||||
labelId = R.string.bookshelf
|
||||
)
|
||||
|
||||
object Explore : MainDestination(
|
||||
route = "explore",
|
||||
labelId = R.string.discovery,
|
||||
m3Icon = Icons.Outlined.Explore,
|
||||
m3SelectedIcon = Icons.Default.Explore,
|
||||
miuixIcon = MiuixIcons.Regular.Album,
|
||||
miuixSelectedIcon = MiuixIcons.Heavy.Album
|
||||
labelId = R.string.discovery
|
||||
)
|
||||
|
||||
object Rss : MainDestination(
|
||||
route = "rss",
|
||||
labelId = R.string.rss,
|
||||
m3Icon = Icons.Outlined.RssFeed,
|
||||
m3SelectedIcon = Icons.Default.RssFeed,
|
||||
miuixIcon = MiuixIcons.Regular.Favorites,
|
||||
miuixSelectedIcon = MiuixIcons.Heavy.Favorites
|
||||
labelId = R.string.rss
|
||||
)
|
||||
|
||||
object My : MainDestination(
|
||||
route = "my",
|
||||
labelId = R.string.my,
|
||||
m3Icon = Icons.Outlined.Person,
|
||||
m3SelectedIcon = Icons.Default.Person,
|
||||
miuixIcon = MiuixIcons.Regular.Settings,
|
||||
miuixSelectedIcon = MiuixIcons.Heavy.Settings
|
||||
labelId = R.string.my
|
||||
)
|
||||
|
||||
companion object {
|
||||
val mainDestinations = listOf(Bookshelf, Explore, Rss, My)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
package io.legado.app.ui.main
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
@@ -39,15 +46,18 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.kyant.backdrop.backdrops.layerBackdrop
|
||||
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||
import dev.chrisbanes.haze.HazeState
|
||||
import dev.chrisbanes.haze.hazeSource
|
||||
import io.legado.app.R
|
||||
import io.legado.app.ui.book.info.BookInfoActivity
|
||||
import io.legado.app.ui.book.search.SearchActivity
|
||||
@@ -57,11 +67,17 @@ import io.legado.app.ui.main.bookshelf.BookshelfViewModel
|
||||
import io.legado.app.ui.main.explore.ExploreScreen
|
||||
import io.legado.app.ui.main.my.MyScreen
|
||||
import io.legado.app.ui.main.rss.RssScreen
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.theme.regularHazeEffect
|
||||
import io.legado.app.ui.theme.responsiveHazeSource
|
||||
import io.legado.app.ui.widget.components.AppNavigationBar
|
||||
import io.legado.app.ui.widget.components.AppNavigationBarItem
|
||||
import io.legado.app.ui.widget.components.AppScaffold
|
||||
import io.legado.app.ui.widget.components.FloatingBottomBar
|
||||
import io.legado.app.ui.widget.components.FloatingBottomBarItem
|
||||
import io.legado.app.ui.widget.components.GlassDefaults
|
||||
import io.legado.app.ui.widget.components.icon.AppIcon
|
||||
import io.legado.app.ui.widget.components.icon.AppIcons
|
||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
|
||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
||||
import io.legado.app.ui.widget.components.text.AppText
|
||||
@@ -88,6 +104,11 @@ fun MainScreen(
|
||||
val bookshelfUiState by bookshelfViewModel.uiState.collectAsState()
|
||||
|
||||
val hazeState = remember { HazeState() }
|
||||
val floatingBarSurfaceColor = MaterialTheme.colorScheme.surface
|
||||
val floatingBarBackdrop = rememberLayerBackdrop {
|
||||
drawRect(floatingBarSurfaceColor)
|
||||
drawContent()
|
||||
}
|
||||
val destinations = remember(MainConfig.showDiscovery, MainConfig.showRSS) {
|
||||
MainDestination.mainDestinations.filter {
|
||||
when (it) {
|
||||
@@ -103,6 +124,15 @@ fun MainScreen(
|
||||
if (index != -1) index else 0
|
||||
}
|
||||
val pagerState = rememberPagerState(initialPage = initialPage) { destinations.size }
|
||||
val labelVisibilityMode = MainConfig.labelVisibilityMode
|
||||
val isUnlabeled = labelVisibilityMode == "unlabeled"
|
||||
val useFloatingBottomBar =
|
||||
!useRail && MainConfig.showBottomView && MainConfig.useFloatingBottomBar
|
||||
val useLiquidGlass = useFloatingBottomBar &&
|
||||
MainConfig.useFloatingBottomBarLiquidGlass &&
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
|
||||
val alwaysShowLabel = labelVisibilityMode == "labeled"
|
||||
val showLabel = !isUnlabeled
|
||||
|
||||
val navState = rememberWideNavigationRailState(
|
||||
initialValue = if (MainConfig.navExtended)
|
||||
@@ -230,51 +260,116 @@ fun MainScreen(
|
||||
}
|
||||
|
||||
AppScaffold(
|
||||
modifier = Modifier.weight(1f),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.responsiveHazeSource(hazeState),
|
||||
bottomBar = {
|
||||
if (!useRail && MainConfig.showBottomView) {
|
||||
val labelVisibilityMode = MainConfig.labelVisibilityMode
|
||||
val isUnlabeled = labelVisibilityMode == "unlabeled"
|
||||
|
||||
AppNavigationBar(
|
||||
modifier = Modifier
|
||||
.regularHazeEffect(state = hazeState)
|
||||
.height(if (isUnlabeled) 64.dp else 80.dp)
|
||||
) {
|
||||
val alwaysShowLabel = when (labelVisibilityMode) {
|
||||
"labeled" -> true
|
||||
else -> false
|
||||
}
|
||||
destinations.forEachIndexed { index, destination ->
|
||||
val selected = pagerState.targetPage == index
|
||||
AppNavigationBarItem(
|
||||
selected = selected,
|
||||
onClick = {
|
||||
coroutineScope.launch { pagerState.animateScrollToPage(index) }
|
||||
},
|
||||
labelString = stringResource(destination.labelId),
|
||||
miuixIcon = if (selected) destination.miuixSelectedIcon else destination.miuixIcon,
|
||||
m3Icon = {
|
||||
NavigationIcon(
|
||||
destination = destination,
|
||||
selected = selected,
|
||||
upBooksCount = uiState.upBooksCount
|
||||
if (useFloatingBottomBar) {
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
FloatingBottomBar(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null,
|
||||
onClick = {}
|
||||
)
|
||||
.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 12.dp + WindowInsets.navigationBars
|
||||
.asPaddingValues()
|
||||
.calculateBottomPadding()
|
||||
),
|
||||
selectedIndex = { pagerState.currentPage },
|
||||
onSelected = { index ->
|
||||
coroutineScope.launch {
|
||||
pagerState.animateScrollToPage(index)
|
||||
}
|
||||
},
|
||||
m3IndicatorColor = GlassDefaults.glassColor(
|
||||
noBlurColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
blurAlpha = GlassDefaults.ThickBlurAlpha
|
||||
),
|
||||
m3ShowLabel = !isUnlabeled,
|
||||
m3AlwaysShowLabel = alwaysShowLabel
|
||||
)
|
||||
backdrop = floatingBarBackdrop,
|
||||
tabsCount = destinations.size,
|
||||
isBlurEnabled = useLiquidGlass
|
||||
) {
|
||||
destinations.forEachIndexed { index, destination ->
|
||||
val selected = pagerState.currentPage == index
|
||||
FloatingBottomBarItem(
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
pagerState.animateScrollToPage(index)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.defaultMinSize(minWidth = 76.dp)
|
||||
) {
|
||||
NavigationIcon(
|
||||
destination = destination,
|
||||
selected = selected,
|
||||
upBooksCount = uiState.upBooksCount
|
||||
)
|
||||
if (showLabel && (alwaysShowLabel || selected)) {
|
||||
Text(
|
||||
text = stringResource(destination.labelId),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = if (selected) {
|
||||
LegadoTheme.colorScheme.primary
|
||||
} else {
|
||||
LegadoTheme.colorScheme.onSurface
|
||||
},
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
AppNavigationBar(
|
||||
modifier = Modifier
|
||||
.regularHazeEffect(state = hazeState)
|
||||
.height(if (isUnlabeled) 64.dp else 80.dp)
|
||||
) {
|
||||
destinations.forEachIndexed { index, destination ->
|
||||
val selected = pagerState.targetPage == index
|
||||
AppNavigationBarItem(
|
||||
selected = selected,
|
||||
onClick = {
|
||||
coroutineScope.launch { pagerState.animateScrollToPage(index) }
|
||||
},
|
||||
labelString = stringResource(destination.labelId),
|
||||
iconVector = AppIcons.mainDestination(destination, selected),
|
||||
m3Icon = {
|
||||
NavigationIcon(
|
||||
destination = destination,
|
||||
selected = selected,
|
||||
upBooksCount = uiState.upBooksCount
|
||||
)
|
||||
},
|
||||
m3IndicatorColor = GlassDefaults.glassColor(
|
||||
noBlurColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
blurAlpha = GlassDefaults.ThickBlurAlpha
|
||||
),
|
||||
m3ShowLabel = showLabel,
|
||||
m3AlwaysShowLabel = alwaysShowLabel
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
contentWindowInsets = WindowInsets(0)
|
||||
) { _ ->
|
||||
Box(modifier = Modifier.hazeSource(hazeState)) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (useFloatingBottomBar) {
|
||||
Modifier.layerBackdrop(floatingBarBackdrop)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
)
|
||||
) {
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
@@ -319,14 +414,14 @@ private fun NavigationIcon(
|
||||
upBooksCount: Int,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val icon = if (selected) destination.m3SelectedIcon else destination.m3Icon
|
||||
val icon = AppIcons.mainDestination(destination, selected)
|
||||
Box(modifier = modifier) {
|
||||
if (destination == MainDestination.Bookshelf && upBooksCount > 0) {
|
||||
BadgedBox(badge = { Badge { Text(upBooksCount.toString()) } }) {
|
||||
Icon(icon, contentDescription = null)
|
||||
AppIcon(icon, contentDescription = null)
|
||||
}
|
||||
} else {
|
||||
Icon(icon, contentDescription = null)
|
||||
AppIcon(icon, contentDescription = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package io.legado.app.ui.util
|
||||
|
||||
import androidx.compose.foundation.gestures.awaitEachGesture
|
||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.pointer.AwaitPointerEventScope
|
||||
import androidx.compose.ui.input.pointer.PointerEventPass
|
||||
import androidx.compose.ui.input.pointer.PointerId
|
||||
import androidx.compose.ui.input.pointer.PointerInputChange
|
||||
import androidx.compose.ui.input.pointer.PointerInputScope
|
||||
import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
|
||||
import androidx.compose.ui.input.pointer.positionChange
|
||||
import androidx.compose.ui.util.fastFirstOrNull
|
||||
|
||||
suspend fun PointerInputScope.inspectDragGestures(
|
||||
onDragStart: (down: PointerInputChange) -> Unit = {},
|
||||
onDragEnd: (change: PointerInputChange) -> Unit = {},
|
||||
onDragCancel: () -> Unit = {},
|
||||
onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
|
||||
) {
|
||||
awaitEachGesture {
|
||||
val initialDown = awaitFirstDown(false, PointerEventPass.Initial)
|
||||
val down = awaitFirstDown(false)
|
||||
|
||||
onDragStart(down)
|
||||
onDrag(initialDown, Offset.Zero)
|
||||
val upEvent = drag(
|
||||
pointerId = initialDown.id,
|
||||
onDrag = { onDrag(it, it.positionChange()) }
|
||||
)
|
||||
if (upEvent == null) {
|
||||
onDragCancel()
|
||||
} else {
|
||||
onDragEnd(upEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend inline fun AwaitPointerEventScope.drag(
|
||||
pointerId: PointerId,
|
||||
onDrag: (PointerInputChange) -> Unit
|
||||
): PointerInputChange? {
|
||||
val isPointerUp = currentEvent.changes.fastFirstOrNull { it.id == pointerId }?.pressed != true
|
||||
if (isPointerUp) {
|
||||
return null
|
||||
}
|
||||
var pointer = pointerId
|
||||
while (true) {
|
||||
val change = awaitDragOrUp(pointer) ?: return null
|
||||
if (change.isConsumed) {
|
||||
return null
|
||||
}
|
||||
if (change.changedToUpIgnoreConsumed()) {
|
||||
return change
|
||||
}
|
||||
onDrag(change)
|
||||
pointer = change.id
|
||||
}
|
||||
}
|
||||
|
||||
private suspend inline fun AwaitPointerEventScope.awaitDragOrUp(
|
||||
pointerId: PointerId
|
||||
): PointerInputChange? {
|
||||
var pointer = pointerId
|
||||
while (true) {
|
||||
val event = awaitPointerEvent()
|
||||
val dragEvent = event.changes.fastFirstOrNull { it.id == pointer } ?: return null
|
||||
if (dragEvent.changedToUpIgnoreConsumed()) {
|
||||
val otherDown = event.changes.fastFirstOrNull { it.pressed }
|
||||
if (otherDown == null) {
|
||||
return dragEvent
|
||||
} else {
|
||||
pointer = otherDown.id
|
||||
}
|
||||
} else {
|
||||
val hasDragged = dragEvent.previousPosition != dragEvent.position
|
||||
if (hasDragged) {
|
||||
return dragEvent
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,7 @@ fun RowScope.AppNavigationBarItem(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
labelString: String,
|
||||
miuixIcon: ImageVector,
|
||||
iconVector: ImageVector,
|
||||
m3Icon: @Composable () -> Unit,
|
||||
m3IndicatorColor: Color,
|
||||
m3ShowLabel: Boolean,
|
||||
@@ -73,7 +73,7 @@ fun RowScope.AppNavigationBarItem(
|
||||
MiuixNavigationBarItem(
|
||||
selected = selected,
|
||||
onClick = onClick,
|
||||
icon = miuixIcon,
|
||||
icon = iconVector,
|
||||
label = labelString,
|
||||
modifier = modifier
|
||||
)
|
||||
@@ -90,4 +90,4 @@ fun RowScope.AppNavigationBarItem(
|
||||
alwaysShowLabel = m3AlwaysShowLabel
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,419 @@
|
||||
// Portions of this file are derived from weishu/KernelSU
|
||||
// (https://github.com/tiann/KernelSU)
|
||||
// Copyright (C) KernelSU contributors
|
||||
// Licensed under GPL-3.0
|
||||
package io.legado.app.ui.widget.components
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.EaseOut
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastCoerceIn
|
||||
import androidx.compose.ui.util.fastRoundToInt
|
||||
import androidx.compose.ui.util.lerp
|
||||
import com.kyant.backdrop.Backdrop
|
||||
import com.kyant.backdrop.backdrops.layerBackdrop
|
||||
import com.kyant.backdrop.backdrops.rememberCombinedBackdrop
|
||||
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||
import com.kyant.backdrop.drawBackdrop
|
||||
import com.kyant.backdrop.effects.blur
|
||||
import com.kyant.backdrop.effects.lens
|
||||
import com.kyant.backdrop.effects.vibrancy
|
||||
import com.kyant.backdrop.highlight.Highlight
|
||||
import com.kyant.backdrop.shadow.InnerShadow
|
||||
import com.kyant.backdrop.shadow.Shadow
|
||||
import com.kyant.capsule.ContinuousCapsule
|
||||
import io.legado.app.ui.animation.DampedDragAnimation
|
||||
import io.legado.app.ui.animation.InteractiveHighlight
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.sign
|
||||
|
||||
val LocalFloatingBottomBarTabScale = staticCompositionLocalOf { { 1f } }
|
||||
|
||||
@Composable
|
||||
fun RowScope.FloatingBottomBarItem(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable ColumnScope.() -> Unit
|
||||
) {
|
||||
val scale = LocalFloatingBottomBarTabScale.current
|
||||
Column(
|
||||
modifier
|
||||
.clip(ContinuousCapsule)
|
||||
.clickable(
|
||||
interactionSource = null,
|
||||
indication = null,
|
||||
role = Role.Tab,
|
||||
onClick = onClick
|
||||
)
|
||||
.fillMaxHeight()
|
||||
.weight(1f)
|
||||
.graphicsLayer {
|
||||
val currentScale = scale()
|
||||
scaleX = currentScale
|
||||
scaleY = currentScale
|
||||
},
|
||||
verticalArrangement = Arrangement.spacedBy(1.dp, Alignment.CenterVertically),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FloatingBottomBar(
|
||||
modifier: Modifier = Modifier,
|
||||
selectedIndex: () -> Int,
|
||||
onSelected: (index: Int) -> Unit,
|
||||
backdrop: Backdrop,
|
||||
tabsCount: Int,
|
||||
isBlurEnabled: Boolean = true,
|
||||
content: @Composable RowScope.() -> Unit
|
||||
) {
|
||||
val isInLightTheme = !LegadoTheme.isDark
|
||||
val accentColor = LegadoTheme.colorScheme.primary
|
||||
val containerColor = if (isBlurEnabled) {
|
||||
LegadoTheme.colorScheme.surfaceContainer.copy(alpha = 0.4f)
|
||||
} else {
|
||||
LegadoTheme.colorScheme.surfaceContainer
|
||||
}
|
||||
|
||||
val tabsBackdrop = rememberLayerBackdrop()
|
||||
val density = LocalDensity.current
|
||||
val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
|
||||
val animationScope = rememberCoroutineScope()
|
||||
|
||||
var tabWidthPx by remember { mutableFloatStateOf(0f) }
|
||||
var totalWidthPx by remember { mutableFloatStateOf(0f) }
|
||||
|
||||
val offsetAnimation = remember { Animatable(0f) }
|
||||
val panelOffset by remember(density) {
|
||||
derivedStateOf {
|
||||
if (totalWidthPx == 0f) {
|
||||
0f
|
||||
} else {
|
||||
val fraction = (offsetAnimation.value / totalWidthPx).fastCoerceIn(-1f, 1f)
|
||||
with(density) {
|
||||
4f.dp.toPx() * fraction.sign * EaseOut.transform(abs(fraction))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var currentIndex by remember(selectedIndex) { mutableIntStateOf(selectedIndex()) }
|
||||
|
||||
class DampedDragAnimationHolder {
|
||||
var instance: DampedDragAnimation? = null
|
||||
}
|
||||
|
||||
val holder = remember { DampedDragAnimationHolder() }
|
||||
|
||||
val dampedDragAnimation = remember(animationScope, tabsCount, density, isLtr) {
|
||||
DampedDragAnimation(
|
||||
animationScope = animationScope,
|
||||
initialValue = selectedIndex().toFloat(),
|
||||
valueRange = 0f..(tabsCount - 1).toFloat(),
|
||||
visibilityThreshold = 0.001f,
|
||||
initialScale = 1f,
|
||||
pressedScale = 78f / 56f,
|
||||
canDrag = { offset ->
|
||||
val anim = holder.instance ?: return@DampedDragAnimation true
|
||||
if (tabWidthPx == 0f) return@DampedDragAnimation false
|
||||
|
||||
val currentValue = anim.value
|
||||
val indicatorX = currentValue * tabWidthPx
|
||||
val padding = with(density) { 4.dp.toPx() }
|
||||
val globalTouchX = if (isLtr) {
|
||||
val touchX = indicatorX + offset.x
|
||||
padding + touchX
|
||||
} else {
|
||||
totalWidthPx - padding - tabWidthPx - indicatorX + offset.x
|
||||
}
|
||||
globalTouchX in 0f..totalWidthPx
|
||||
},
|
||||
onDragStarted = {},
|
||||
onDragStopped = {
|
||||
val targetIndex = targetValue.fastRoundToInt().fastCoerceIn(0, tabsCount - 1)
|
||||
currentIndex = targetIndex
|
||||
animateToValue(targetIndex.toFloat())
|
||||
animationScope.launch {
|
||||
offsetAnimation.animateTo(0f, spring(1f, 300f, 0.5f))
|
||||
}
|
||||
},
|
||||
onDrag = { _, dragAmount ->
|
||||
if (tabWidthPx > 0f) {
|
||||
updateValue(
|
||||
(targetValue + dragAmount.x / tabWidthPx * if (isLtr) 1f else -1f)
|
||||
.fastCoerceIn(0f, (tabsCount - 1).toFloat())
|
||||
)
|
||||
animationScope.launch {
|
||||
offsetAnimation.snapTo(offsetAnimation.value + dragAmount.x)
|
||||
}
|
||||
}
|
||||
}
|
||||
).also { holder.instance = it }
|
||||
}
|
||||
|
||||
LaunchedEffect(selectedIndex) {
|
||||
snapshotFlow { selectedIndex() }.collectLatest { currentIndex = it }
|
||||
}
|
||||
LaunchedEffect(dampedDragAnimation) {
|
||||
snapshotFlow { currentIndex }.drop(1).collectLatest { index ->
|
||||
dampedDragAnimation.animateToValue(index.toFloat())
|
||||
onSelected(index)
|
||||
}
|
||||
}
|
||||
|
||||
val interactiveHighlight =
|
||||
if (isBlurEnabled && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
remember(animationScope, tabWidthPx) {
|
||||
InteractiveHighlight(
|
||||
animationScope = animationScope,
|
||||
position = { size, _ ->
|
||||
Offset(
|
||||
if (isLtr) {
|
||||
(dampedDragAnimation.value + 0.5f) * tabWidthPx + panelOffset
|
||||
} else {
|
||||
size.width - (dampedDragAnimation.value + 0.5f) * tabWidthPx + panelOffset
|
||||
},
|
||||
size.height / 2f
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier.width(IntrinsicSize.Min),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
Row(
|
||||
Modifier
|
||||
.onGloballyPositioned { coords ->
|
||||
totalWidthPx = coords.size.width.toFloat()
|
||||
val contentWidthPx = totalWidthPx - with(density) { 8.dp.toPx() }
|
||||
tabWidthPx = contentWidthPx / tabsCount
|
||||
}
|
||||
.graphicsLayer { translationX = panelOffset }
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null,
|
||||
onClick = {}
|
||||
)
|
||||
.drawBackdrop(
|
||||
backdrop = backdrop,
|
||||
shape = { ContinuousCapsule },
|
||||
effects = {
|
||||
if (isBlurEnabled) {
|
||||
vibrancy()
|
||||
blur(8f.dp.toPx())
|
||||
lens(24f.dp.toPx(), 24f.dp.toPx())
|
||||
}
|
||||
},
|
||||
highlight = {
|
||||
Highlight.Default.copy(alpha = if (isBlurEnabled) 1f else 0f)
|
||||
},
|
||||
shadow = {
|
||||
Shadow.Default.copy(
|
||||
color = Color.Black.copy(if (isInLightTheme) 0.1f else 0.2f)
|
||||
)
|
||||
},
|
||||
layerBlock = {
|
||||
if (isBlurEnabled) {
|
||||
val progress = dampedDragAnimation.pressProgress
|
||||
val scale = lerp(1f, 1f + 16f.dp.toPx() / size.width, progress)
|
||||
scaleX = scale
|
||||
scaleY = scale
|
||||
}
|
||||
},
|
||||
onDrawSurface = { drawRect(containerColor) }
|
||||
)
|
||||
.then(
|
||||
if (isBlurEnabled && interactiveHighlight != null) {
|
||||
interactiveHighlight.modifier
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
)
|
||||
.height(64.dp)
|
||||
.padding(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
content = content
|
||||
)
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalFloatingBottomBarTabScale provides {
|
||||
if (isBlurEnabled) {
|
||||
lerp(1f, 1.2f, dampedDragAnimation.pressProgress)
|
||||
} else {
|
||||
1f
|
||||
}
|
||||
}
|
||||
) {
|
||||
Row(
|
||||
Modifier
|
||||
.clearAndSetSemantics {}
|
||||
.alpha(0f)
|
||||
.layerBackdrop(tabsBackdrop)
|
||||
.graphicsLayer { translationX = panelOffset }
|
||||
.drawBackdrop(
|
||||
backdrop = backdrop,
|
||||
shape = { ContinuousCapsule },
|
||||
effects = {
|
||||
if (isBlurEnabled) {
|
||||
val progress = dampedDragAnimation.pressProgress
|
||||
vibrancy()
|
||||
blur(8f.dp.toPx())
|
||||
lens(24f.dp.toPx() * progress, 24f.dp.toPx() * progress)
|
||||
}
|
||||
},
|
||||
highlight = {
|
||||
Highlight.Default.copy(
|
||||
alpha = if (isBlurEnabled) {
|
||||
dampedDragAnimation.pressProgress
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
)
|
||||
},
|
||||
onDrawSurface = { drawRect(containerColor) }
|
||||
)
|
||||
.then(
|
||||
if (isBlurEnabled && interactiveHighlight != null) {
|
||||
interactiveHighlight.modifier
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
)
|
||||
.height(56.dp)
|
||||
.padding(horizontal = 4.dp)
|
||||
.graphicsLayer(colorFilter = ColorFilter.tint(accentColor)),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
if (tabWidthPx > 0f) {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(horizontal = 4.dp)
|
||||
.graphicsLayer {
|
||||
val contentWidth = totalWidthPx - with(density) { 8.dp.toPx() }
|
||||
val singleTabWidth = contentWidth / tabsCount
|
||||
val progressOffset = dampedDragAnimation.value * singleTabWidth
|
||||
|
||||
translationX = if (isLtr) {
|
||||
progressOffset + panelOffset
|
||||
} else {
|
||||
-progressOffset + panelOffset
|
||||
}
|
||||
}
|
||||
.then(
|
||||
if (isBlurEnabled && interactiveHighlight != null) {
|
||||
interactiveHighlight.gestureModifier
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
)
|
||||
.then(dampedDragAnimation.modifier)
|
||||
.drawBackdrop(
|
||||
backdrop = rememberCombinedBackdrop(backdrop, tabsBackdrop),
|
||||
shape = { ContinuousCapsule },
|
||||
effects = {
|
||||
if (isBlurEnabled) {
|
||||
val progress = dampedDragAnimation.pressProgress
|
||||
lens(10f.dp.toPx() * progress, 14f.dp.toPx() * progress, true)
|
||||
}
|
||||
},
|
||||
highlight = {
|
||||
Highlight.Default.copy(
|
||||
alpha = if (isBlurEnabled) {
|
||||
dampedDragAnimation.pressProgress
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
)
|
||||
},
|
||||
shadow = {
|
||||
Shadow(alpha = if (isBlurEnabled) dampedDragAnimation.pressProgress else 0f)
|
||||
},
|
||||
innerShadow = {
|
||||
InnerShadow(
|
||||
radius = 8f.dp * dampedDragAnimation.pressProgress,
|
||||
alpha = if (isBlurEnabled) dampedDragAnimation.pressProgress else 0f
|
||||
)
|
||||
},
|
||||
layerBlock = {
|
||||
if (isBlurEnabled) {
|
||||
scaleX = dampedDragAnimation.scaleX
|
||||
scaleY = dampedDragAnimation.scaleY
|
||||
val velocity = dampedDragAnimation.velocity / 10f
|
||||
scaleX /= 1f - (velocity * 0.75f).fastCoerceIn(-0.2f, 0.2f)
|
||||
scaleY *= 1f - (velocity * 0.25f).fastCoerceIn(-0.2f, 0.2f)
|
||||
}
|
||||
},
|
||||
onDrawSurface = {
|
||||
val progress =
|
||||
if (isBlurEnabled) dampedDragAnimation.pressProgress else 0f
|
||||
drawRect(
|
||||
color = if (isInLightTheme) {
|
||||
Color.Black.copy(0.1f)
|
||||
} else {
|
||||
Color.White.copy(0.1f)
|
||||
},
|
||||
alpha = 1f - progress
|
||||
)
|
||||
drawRect(Color.Black.copy(alpha = 0.03f * progress))
|
||||
}
|
||||
)
|
||||
.height(56.dp)
|
||||
.width(with(density) { ((totalWidthPx - 8.dp.toPx()) / tabsCount).toDp() })
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,22 +2,32 @@ package io.legado.app.ui.widget.components.icon
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.LibraryBooks
|
||||
import androidx.compose.material.icons.automirrored.outlined.LibraryBooks
|
||||
import androidx.compose.material.icons.filled.Clear
|
||||
import androidx.compose.material.icons.filled.Explore
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material.icons.filled.Replay
|
||||
import androidx.compose.material.icons.filled.RssFeed
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.outlined.Explore
|
||||
import androidx.compose.material.icons.outlined.Person
|
||||
import androidx.compose.material.icons.outlined.RssFeed
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import io.legado.app.ui.main.MainDestination
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.theme.ThemeResolver
|
||||
import top.yukonga.miuix.kmp.icon.MiuixIcons
|
||||
import top.yukonga.miuix.kmp.icon.basic.Search
|
||||
import top.yukonga.miuix.kmp.icon.extended.Album
|
||||
import top.yukonga.miuix.kmp.icon.extended.Back
|
||||
import top.yukonga.miuix.kmp.icon.extended.Close
|
||||
import top.yukonga.miuix.kmp.icon.extended.ContactsBook
|
||||
import top.yukonga.miuix.kmp.icon.extended.Favorites
|
||||
import top.yukonga.miuix.kmp.icon.extended.Refresh
|
||||
import top.yukonga.miuix.kmp.icon.extended.Settings
|
||||
|
||||
/**
|
||||
* 应用全局图标映射层
|
||||
*/
|
||||
object AppIcons {
|
||||
|
||||
private val isMiuix: Boolean
|
||||
@@ -26,29 +36,46 @@ object AppIcons {
|
||||
|
||||
val Search: ImageVector
|
||||
@Composable
|
||||
get() = if (isMiuix) {
|
||||
MiuixIcons.Basic.Search
|
||||
} else {
|
||||
Icons.Default.Search
|
||||
}
|
||||
get() = if (isMiuix) MiuixIcons.Basic.Search else Icons.Default.Search
|
||||
|
||||
val Close: ImageVector
|
||||
@Composable
|
||||
get() = if (isMiuix) {
|
||||
MiuixIcons.Close
|
||||
} else {
|
||||
Icons.Default.Clear
|
||||
}
|
||||
get() = if (isMiuix) MiuixIcons.Close else Icons.Default.Clear
|
||||
|
||||
val Back: ImageVector
|
||||
@Composable
|
||||
get() = if (isMiuix) {
|
||||
MiuixIcons.Back
|
||||
} else {
|
||||
Icons.AutoMirrored.Filled.ArrowBack
|
||||
}
|
||||
get() = if (isMiuix) MiuixIcons.Back else Icons.AutoMirrored.Filled.ArrowBack
|
||||
|
||||
val Replay: ImageVector
|
||||
@Composable
|
||||
get() = if (isMiuix) MiuixIcons.Refresh ?: Icons.Default.Replay else Icons.Default.Replay
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun mainDestination(destination: MainDestination, selected: Boolean): ImageVector {
|
||||
return when (destination) {
|
||||
MainDestination.Bookshelf -> if (isMiuix) {
|
||||
if (selected) MiuixIcons.Heavy.ContactsBook else MiuixIcons.Regular.ContactsBook
|
||||
} else {
|
||||
if (selected) Icons.AutoMirrored.Filled.LibraryBooks else Icons.AutoMirrored.Outlined.LibraryBooks
|
||||
}
|
||||
|
||||
MainDestination.Explore -> if (isMiuix) {
|
||||
if (selected) MiuixIcons.Heavy.Album else MiuixIcons.Regular.Album
|
||||
} else {
|
||||
if (selected) Icons.Default.Explore else Icons.Outlined.Explore
|
||||
}
|
||||
|
||||
MainDestination.Rss -> if (isMiuix) {
|
||||
if (selected) MiuixIcons.Heavy.Favorites else MiuixIcons.Regular.Favorites
|
||||
} else {
|
||||
if (selected) Icons.Default.RssFeed else Icons.Outlined.RssFeed
|
||||
}
|
||||
|
||||
MainDestination.My -> if (isMiuix) {
|
||||
if (selected) MiuixIcons.Heavy.Settings else MiuixIcons.Regular.Settings
|
||||
} else {
|
||||
if (selected) Icons.Default.Person else Icons.Outlined.Person
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user