小优化

This commit is contained in:
beyond committed 2026-09-07 16:34:29 +08:00
1 parent 391797d377
commit dd1c62f0f0
7 files changed
+296 -104

No files matched your search

@@ -316,7 +316,7 @@ fun MainApp() {
},
) {
Box(
Modifier.fillMaxWidth().padding(bottom = 12.dp),
Modifier.fillMaxWidth(),
contentAlignment = Alignment.BottomCenter,
) {
LiquidAppBottomBar(
@@ -1,8 +1,10 @@
package com.example.myapplication.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
@@ -11,6 +13,8 @@ import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@@ -30,66 +34,89 @@ fun LiquidAppBottomBar(
onSelect: (Int) -> Unit,
backdrop: Backdrop?,
modifier: Modifier = Modifier,
contentTint: Color = Color.Transparent,
) {
val active = LumaColors.accent
val inactive = LumaColors.textSecondary
// iOS 26: subtle content-aware color overlay for glass tinting
val tintOverlay = if (contentTint != Color.Transparent) {
Brush.verticalGradient(
listOf(
Color.Transparent,
contentTint.copy(alpha = 0.05f),
)
)
} else null
Row(
modifier = modifier
.navigationBarsPadding()
.padding(start = LumaSpacing.lg, end = LumaSpacing.lg, bottom = LumaSpacing.sm),
verticalAlignment = Alignment.CenterVertically,
) {
Box(modifier = Modifier.weight(1f)) {
LiquidBottomTabs(
selectedTabIndex = { selectedTab.coerceIn(0, 3) },
onTabSelected = onSelect,
backdrop = backdrop,
tabsCount = 4,
showSelection = { selectedTab < 4 },
) {
for (index in mainTabs.indices) {
val tab = mainTabs[index]
LiquidBottomTab(onClick = { onSelect(index) }) {
LumaAnimatedIcon(
icon = tab.icon,
selected = selectedTab == index,
contentDescription = stringResource(tab.labelRes),
tint = if (selectedTab == index) active else inactive,
modifier = Modifier.size(26.dp),
)
Spacer(Modifier.height(2.dp))
Text(
text = stringResource(tab.labelRes),
color = if (selectedTab == index) active else inactive,
fontSize = 10.sp,
fontWeight = if (selectedTab == index) FontWeight.SemiBold else FontWeight.Medium,
maxLines = 1,
)
Box(modifier = modifier) {
Row(
modifier = Modifier
.navigationBarsPadding()
.padding(start = LumaSpacing.lg, end = LumaSpacing.lg, bottom = LumaSpacing.sm),
verticalAlignment = Alignment.CenterVertically,
) {
Box(modifier = Modifier.weight(1f)) {
LiquidBottomTabs(
selectedTabIndex = { selectedTab.coerceIn(0, 3) },
onTabSelected = onSelect,
backdrop = backdrop,
tabsCount = 4,
showSelection = { selectedTab < 4 },
) {
for (index in mainTabs.indices) {
val tab = mainTabs[index]
LiquidBottomTab(onClick = { onSelect(index) }) {
LumaAnimatedIcon(
icon = tab.icon,
selected = selectedTab == index,
contentDescription = stringResource(tab.labelRes),
tint = if (selectedTab == index) active else inactive,
modifier = Modifier.size(26.dp),
)
Spacer(Modifier.height(2.dp))
Text(
text = stringResource(tab.labelRes),
color = if (selectedTab == index) active else inactive,
fontSize = 10.sp,
fontWeight = if (selectedTab == index) FontWeight.SemiBold else FontWeight.Medium,
maxLines = 1,
)
}
}
}
}
Spacer(Modifier.width(LumaSpacing.sm))
LiquidBottomTabs(
selectedTabIndex = { 0 },
onTabSelected = { onSelect(4) },
backdrop = backdrop,
tabsCount = 1,
showSelection = { selectedTab == 4 },
modifier = Modifier.size(64.dp),
) {
LiquidBottomTab(onClick = { onSelect(4) }) {
LumaAnimatedIcon(
icon = LumaIcon.Search,
selected = selectedTab == 4,
contentDescription = stringResource(com.example.myapplication.R.string.search),
tint = if (selectedTab == 4) active else inactive,
modifier = Modifier.size(27.dp),
)
}
}
}
Spacer(Modifier.width(LumaSpacing.sm))
LiquidBottomTabs(
selectedTabIndex = { 0 },
onTabSelected = { onSelect(4) },
backdrop = backdrop,
tabsCount = 1,
showSelection = { selectedTab == 4 },
modifier = Modifier.size(64.dp),
) {
LiquidBottomTab(onClick = { onSelect(4) }) {
LumaAnimatedIcon(
icon = LumaIcon.Search,
selected = selectedTab == 4,
contentDescription = stringResource(com.example.myapplication.R.string.search),
tint = if (selectedTab == 4) active else inactive,
modifier = Modifier.size(27.dp),
)
}
// iOS 26: subtle content-aware tint overlay (visible only when contentTint is provided)
tintOverlay?.let { brush ->
Box(
modifier = Modifier
.fillMaxWidth()
.navigationBarsPadding()
.height(80.dp)
.background(brush),
)
}
}
}
@@ -14,6 +14,7 @@ import com.kyant.backdrop.effects.colorControls
import com.kyant.backdrop.effects.lens
import com.kyant.backdrop.effects.vibrancy
import com.kyant.backdrop.highlight.Highlight
import com.kyant.backdrop.highlight.HighlightStyle
import com.kyant.backdrop.shadow.Shadow
import com.kyant.shapes.RoundedRectangle
import androidx.compose.foundation.border
@@ -591,7 +592,12 @@ fun LiquidGlassPopupOverlay(
)
}
},
highlight = { Highlight.Plain.copy(alpha = 0.58f) },
highlight = {
Highlight(
alpha = 0.60f,
style = HighlightStyle.Default(angle = 180f, falloff = 1.1f),
)
},
shadow = { Shadow(alpha = 0.18f) },
onDrawSurface = { drawRect(surface) },
)
@@ -17,6 +17,9 @@ import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
@@ -33,6 +36,8 @@ import com.kyant.backdrop.effects.colorControls
import com.kyant.backdrop.effects.lens
import com.kyant.backdrop.effects.vibrancy
import com.kyant.backdrop.highlight.Highlight
import com.kyant.backdrop.highlight.HighlightStyle
import com.kyant.shapes.Capsule
import com.kyant.backdrop.shadow.Shadow
import com.kyant.shapes.RoundedRectangle
@@ -174,6 +179,25 @@ object LumaColors {
get() = if (isDark) Color.White.copy(alpha = 0.07f) else Color.White.copy(alpha = 0.24f)
val glassShadow: Color
get() = if (isDark) Color.Black.copy(alpha = 0.32f) else Color(0xFF344054).copy(alpha = 0.14f)
/** Sweep-gradient border for iOS 26 liquid glass edge effect */
val glassBorderGradient: Brush
get() = if (isDark) {
Brush.sweepGradient(
0.00f to Color.White.copy(alpha = 0.35f),
0.25f to Color.White.copy(alpha = 0.10f),
0.50f to Color.White.copy(alpha = 0.20f),
0.75f to Color.White.copy(alpha = 0.10f),
1.00f to Color.White.copy(alpha = 0.35f),
)
} else {
Brush.sweepGradient(
0.00f to Color.White.copy(alpha = 0.65f),
0.25f to Color.White.copy(alpha = 0.18f),
0.50f to Color.White.copy(alpha = 0.35f),
0.75f to Color.White.copy(alpha = 0.18f),
1.00f to Color.White.copy(alpha = 0.65f),
)
}
// ── Settings popup colors ──
val settingsPopupBg: Color get() = if (isDark) Color(0xFF111821) else Color(0xFFF4F7FA)
@@ -192,28 +216,34 @@ fun Modifier.lumaFallbackGlass(
val fill = when {
liquidFallback && light && selected -> Brush.verticalGradient(
listOf(
Color.White.copy(alpha = 0.46f),
Color(0xFFE5EFFA).copy(alpha = 0.38f),
Color.White.copy(alpha = 0.28f),
Color.White.copy(alpha = 0.50f),
Color(0xFFE0ECF8).copy(alpha = 0.42f),
Color(0xFFD4E2F0).copy(alpha = 0.36f),
Color(0xFFE8F0F8).copy(alpha = 0.32f),
Color.White.copy(alpha = 0.26f),
),
)
liquidFallback && light -> Brush.verticalGradient(
listOf(
Color.White.copy(alpha = 0.32f),
Color(0xFFE4EDF7).copy(alpha = 0.27f),
Color.White.copy(alpha = 0.20f),
Color.White.copy(alpha = 0.36f),
Color(0xFFE4EDF7).copy(alpha = 0.30f),
Color(0xFFD8E4EF).copy(alpha = 0.26f),
Color(0xFFE2EBF4).copy(alpha = 0.28f),
Color.White.copy(alpha = 0.18f),
),
)
liquidFallback && selected -> Brush.verticalGradient(
listOf(
Color(0xFF60758A).copy(alpha = 0.70f),
Color(0xFF304253).copy(alpha = 0.64f),
Color(0xFF657B92).copy(alpha = 0.74f),
Color(0xFF405870).copy(alpha = 0.68f),
Color(0xFF2D4258).copy(alpha = 0.62f),
),
)
liquidFallback -> Brush.verticalGradient(
listOf(
Color(0xFF44586C).copy(alpha = 0.60f),
Color(0xFF233140).copy(alpha = 0.66f),
Color(0xFF4A6278).copy(alpha = 0.64f),
Color(0xFF2E4254).copy(alpha = 0.58f),
Color(0xFF1E3040).copy(alpha = 0.52f),
),
)
light && selected -> Brush.verticalGradient(
@@ -262,25 +292,38 @@ fun Modifier.lumaFallbackGlass(
},
)
}
val topHighlight = if (light) {
Color.White.copy(
alpha = when {
liquidFallback && selected -> 0.34f
liquidFallback -> 0.24f
selected -> 0.28f
else -> 0.18f
},
)
// Pre-compute highlight alphas (size not available outside DrawScope)
val highlightTop = if (light) {
when {
liquidFallback && selected -> 0.50f
liquidFallback -> 0.38f
selected -> 0.42f
else -> 0.30f
}
} else {
Color.White.copy(
alpha = when {
liquidFallback && selected -> 0.14f
liquidFallback -> 0.08f
selected -> 0.10f
else -> 0.06f
},
)
when {
liquidFallback && selected -> 0.22f
liquidFallback -> 0.14f
selected -> 0.16f
else -> 0.10f
}
}
val highlightMid = if (light) {
when {
liquidFallback && selected -> 0.12f
liquidFallback -> 0.08f
selected -> 0.10f
else -> 0.06f
}
} else {
when {
liquidFallback && selected -> 0.06f
liquidFallback -> 0.04f
selected -> 0.05f
else -> 0.02f
}
}
val highlightEndFraction = if (light) 0.55f else 0.50f
return this
.shadow(
elevation = if (selected) 2.dp else 4.dp,
@@ -294,16 +337,17 @@ fun Modifier.lumaFallbackGlass(
neutralVeil?.let { Modifier.background(it, shape) } ?: Modifier,
)
.drawWithCache {
val highlight = Brush.verticalGradient(
listOf(topHighlight, Color.Transparent),
val topHighlight = Brush.verticalGradient(
colors = listOf(
Color.White.copy(alpha = highlightTop),
Color.White.copy(alpha = highlightMid),
Color.Transparent,
),
startY = 0f,
endY = size.height * 0.72f,
endY = size.height * highlightEndFraction,
)
onDrawWithContent {
// The fallback surface is a material layer. Its highlight must
// sit behind the content; drawing it after drawContent makes
// labels and icons look washed out on devices without Backdrop.
drawRect(highlight)
drawRect(topHighlight)
drawContent()
}
}
@@ -311,6 +355,68 @@ fun Modifier.lumaFallbackGlass(
.clip(shape)
}
/**
* iOS 26 liquid glass gradient border.
* Uses [LumaColors.glassBorderGradient] to draw a sweep-gradient stroke
* that simulates light refraction around the glass surface edge.
*/
fun Modifier.liquidBorder(
shape: Shape,
strokeWidth: Dp = 0.8.dp,
gradientBrush: Brush = LumaColors.glassBorderGradient,
): Modifier = this.then(
Modifier.drawWithCache {
val strokePx = strokeWidth.toPx()
// Compute corner radii by probing the shape at each corner
val cornerRadii = when (shape) {
is Capsule -> floatArrayOf(size.minDimension / 2f)
is androidx.compose.foundation.shape.RoundedCornerShape -> floatArrayOf(
shape.topStart.toPx(size, this@drawWithCache),
shape.topEnd.toPx(size, this@drawWithCache),
shape.bottomEnd.toPx(size, this@drawWithCache),
shape.bottomStart.toPx(size, this@drawWithCache),
)
else -> floatArrayOf(0f)
}
val inset = strokePx / 2f
val roundedRect = when {
cornerRadii.size == 1 -> {
val r = cornerRadii[0]
androidx.compose.ui.geometry.RoundRect(
inset, inset,
size.width - inset, size.height - inset,
CornerRadius(r, r),
)
}
cornerRadii.size == 4 -> {
androidx.compose.ui.geometry.RoundRect(
left = inset, top = inset,
right = size.width - inset, bottom = size.height - inset,
topLeftCornerRadius = CornerRadius(cornerRadii[0], cornerRadii[0]),
topRightCornerRadius = CornerRadius(cornerRadii[1], cornerRadii[1]),
bottomRightCornerRadius = CornerRadius(cornerRadii[2], cornerRadii[2]),
bottomLeftCornerRadius = CornerRadius(cornerRadii[3], cornerRadii[3]),
)
}
else -> {
androidx.compose.ui.geometry.RoundRect(
inset, inset,
size.width - inset, size.height - inset,
)
}
}
onDrawBehind {
drawRoundRect(
brush = gradientBrush,
topLeft = androidx.compose.ui.geometry.Offset(roundedRect.left, roundedRect.top),
size = androidx.compose.ui.geometry.Size(roundedRect.width, roundedRect.height),
cornerRadius = CornerRadius(cornerRadii.getOrElse(0) { 0f }),
style = Stroke(width = strokePx, cap = StrokeCap.Round),
)
}
}
)
@Immutable
enum class LumaGlassLevel {
Clear,
@@ -382,7 +488,13 @@ fun Modifier.lumaGlass(
)
}
},
highlight = if (highlight) ({ Highlight.Plain.copy(alpha = 0.58f) }) else null,
highlight = if (highlight) ({
// iOS 26: directional gradient highlight for consistent glass depth
Highlight(
alpha = 0.62f,
style = HighlightStyle.Default(angle = 180f, falloff = 1.1f),
)
}) else null,
shadow = { Shadow(alpha = spec.shadowAlpha) },
onDrawSurface = { drawRect(surfaceTint) },
)
@@ -28,16 +28,17 @@ class DampedDragAnimation(
val onDrag: DampedDragAnimation.(size: IntSize, dragAmount: Offset) -> Unit,
) {
// iOS 26 liquid glass: more fluid, bouncy spring physics
private val valueAnimationSpec =
spring(1f, 1000f, visibilityThreshold)
spring(0.65f, 260f, visibilityThreshold)
private val velocityAnimationSpec =
spring(0.5f, 300f, visibilityThreshold * 10f)
spring(0.55f, 240f, visibilityThreshold * 10f)
private val pressProgressAnimationSpec =
spring(1f, 1000f, 0.001f)
spring(0.7f, 280f, 0.001f)
private val scaleXAnimationSpec =
spring(0.6f, 250f, 0.001f)
spring(0.55f, 200f, 0.001f)
private val scaleYAnimationSpec =
spring(0.7f, 250f, 0.001f)
spring(0.6f, 200f, 0.001f)
private val valueAnimation =
Animatable(initialValue, visibilityThreshold)
@@ -39,7 +39,19 @@ class InteractiveHighlight(
Modifier.drawWithContent {
val progress = pressProgressAnimation.value
if (progress > 0f) {
// iOS 26: subtle ambient glow + directional top gradient
drawRect(Color.White.copy(0.08f * progress), blendMode = BlendMode.Plus)
// Top specular line for glass thickness illusion
val topGlow = Brush.verticalGradient(
listOf(
Color.White.copy(alpha = 0.12f * progress),
Color.Transparent,
),
startY = 0f,
endY = size.height * 0.40f,
)
drawRect(topGlow, blendMode = BlendMode.Plus)
// Touch-following radial highlight
val center = position(size, positionAnimation.value).let {
Offset(
it.x.fastCoerceIn(0f, size.width),
@@ -49,7 +61,7 @@ class InteractiveHighlight(
drawCircle(
brush = Brush.radialGradient(
colors = listOf(
Color.White.copy(alpha = 0.15f * progress),
Color.White.copy(alpha = 0.18f * progress),
Color.Transparent,
),
center = center,
@@ -28,6 +28,7 @@ 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.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalDensity
@@ -45,6 +46,7 @@ import com.example.myapplication.ui.design.LumaColors
import com.example.myapplication.ui.design.LumaGlassRuntime
import com.example.myapplication.ui.design.LumaSpacing
import com.example.myapplication.ui.design.lumaFallbackGlass
import com.example.myapplication.ui.design.liquidBorder
import com.kyant.backdrop.Backdrop
import com.kyant.backdrop.backdrops.layerBackdrop
import com.kyant.backdrop.backdrops.rememberCombinedBackdrop
@@ -54,6 +56,7 @@ 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.highlight.HighlightStyle
import com.kyant.backdrop.shadow.InnerShadow
import com.kyant.backdrop.shadow.Shadow
import com.kyant.shapes.Capsule
@@ -136,7 +139,8 @@ fun LiquidBottomTabs(
currentIndex = targetIndex
animateToValue(targetIndex.toFloat())
animationScope.launch {
offsetAnimation.animateTo(0f, spring(1f, 300f, 0.5f))
// iOS 26: softer spring for smoother panel offset recovery
offsetAnimation.animateTo(0f, spring(0.6f, 250f, 0.5f))
}
},
onDrag = { _, dragAmount ->
@@ -234,6 +238,13 @@ fun LiquidBottomTabs(
blur(8f.dp.toPx())
lens(24f.dp.toPx(), 24f.dp.toPx())
},
highlight = {
// iOS 26: stronger top-down directional gradient highlight
Highlight(
alpha = 0.72f,
style = HighlightStyle.Default(angle = 180f, falloff = 1.2f),
)
},
layerBlock = {
val progress = dampedDragAnimation.pressProgress
val scale = lerp(1f, 1f + 16f.dp.toPx() / size.width, progress)
@@ -243,7 +254,7 @@ fun LiquidBottomTabs(
onDrawSurface = { drawRect(containerColor) },
)
.then(if (pressHighlight) interactiveHighlight.modifier else Modifier)
.border(0.6.dp, LumaColors.glassBorder.copy(alpha = 0.72f), Capsule())
.liquidBorder(Capsule(), strokeWidth = 0.8.dp)
.height(height)
.then(interactiveHighlight.gestureModifier)
.then(dampedDragAnimation.modifier)
@@ -278,7 +289,11 @@ fun LiquidBottomTabs(
lens(24f.dp.toPx() * progress, 24f.dp.toPx() * progress)
},
highlight = {
Highlight.Default.copy(alpha = dampedDragAnimation.pressProgress * pressHighlightAlpha)
// Ghost layer: subtle directional highlight for depth
Highlight(
alpha = dampedDragAnimation.pressProgress * pressHighlightAlpha * 0.5f,
style = HighlightStyle.Default(angle = 180f, falloff = 1.3f),
)
},
onDrawSurface = { drawRect(containerColor) },
)
@@ -307,37 +322,56 @@ fun LiquidBottomTabs(
shape = { Capsule() },
effects = {
val progress = dampedDragAnimation.pressProgress
// Static lens for persistent depth + press-enhanced chromatic aberration
lens(
10f.dp.toPx() * progress,
14f.dp.toPx() * progress,
(6f + 10f * progress).dp.toPx(),
(8f + 8f * progress).dp.toPx(),
chromaticAberration = true,
)
},
highlight = {
Highlight.Default.copy(alpha = dampedDragAnimation.pressProgress * pressHighlightAlpha)
// iOS 26: directional gradient highlight with stronger top-down light
Highlight(
width = 0.8f.dp,
alpha = (0.15f + 0.55f * dampedDragAnimation.pressProgress) * pressHighlightAlpha,
style = HighlightStyle.Default(angle = 180f, falloff = 1.5f),
)
},
shadow = { Shadow(alpha = dampedDragAnimation.pressProgress) },
shadow = { Shadow(alpha = dampedDragAnimation.pressProgress * 0.9f) },
innerShadow = {
val progress = dampedDragAnimation.pressProgress
InnerShadow(radius = 8.dp * progress, alpha = progress)
InnerShadow(radius = 8.dp * progress, alpha = progress * 0.85f)
},
layerBlock = {
scaleX = dampedDragAnimation.scaleX
scaleY = dampedDragAnimation.scaleY
// iOS 26: enhanced velocity-based liquid deformation
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)
scaleX /= 1f - (velocity * 0.85f).fastCoerceIn(-0.22f, 0.22f)
scaleY *= 1f - (velocity * 0.30f).fastCoerceIn(-0.22f, 0.22f)
},
onDrawSurface = {
val progress = dampedDragAnimation.pressProgress
// Base tint
drawRect(
if (isLightTheme) Color.Black.copy(alpha = 0.10f)
else Color.White.copy(alpha = 0.10f),
alpha = 1f - progress,
)
drawRect(Color.Black.copy(alpha = 0.03f * progress))
// iOS 26: top-edge specular line for glass thickness illusion
val specular = Brush.verticalGradient(
listOf(
Color.White.copy(alpha = 0.12f * (1f - progress)),
Color.Transparent,
),
startY = 0f,
endY = size.height * 0.35f,
)
drawRect(specular)
},
)
.liquidBorder(Capsule(), strokeWidth = 0.6.dp)
.height(height - 8.dp)
.fillMaxWidth(1f / tabsCount),
)