取消液态玻璃安全门并收敛底栏绘制
This commit is contained in:
1 parent
485538f296
commit
dafac3e509
11 files changed
+371
-277
No files matched your search
@@ -70,6 +70,7 @@ import com.example.myapplication.ui.bottomBarContentPadding
|
||||
import com.example.myapplication.ui.design.LumaIcon
|
||||
import com.example.myapplication.ui.design.LumaIconView
|
||||
import com.example.myapplication.ui.design.LumaAsyncImage
|
||||
import com.example.myapplication.ui.design.LumaMediaCard
|
||||
import com.example.myapplication.ui.design.LumaEmptyState
|
||||
import com.example.myapplication.ui.design.LumaErrorState
|
||||
import com.example.myapplication.ui.design.LumaSkeleton
|
||||
@@ -387,33 +388,19 @@ private fun MediaPoster(
|
||||
onItemClick: (EmbyItem) -> Unit = {},
|
||||
) {
|
||||
val posterUrl = EmbyRepository.buildImageUrl(serverUrl, item)
|
||||
val fallbackColors = listOf(Color(0xFF455A64), Color(0xFF607D8B))
|
||||
|
||||
Column(modifier.width(115.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Box(
|
||||
Modifier.fillMaxWidth().height(165.dp).clip(RoundedCornerShape(LumaShapes.small))
|
||||
.clickable { onItemClick(item) }
|
||||
.background(Brush.verticalGradient(fallbackColors)),
|
||||
) {
|
||||
if (posterUrl != null) {
|
||||
LumaAsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current).data(posterUrl).crossfade(true).build(),
|
||||
contentDescription = item.name,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
LumaMediaCard(
|
||||
model = posterUrl?.let {
|
||||
ImageRequest.Builder(LocalContext.current).data(it).crossfade(true).build()
|
||||
},
|
||||
title = item.seriesName ?: item.name,
|
||||
modifier = modifier.width(115.dp),
|
||||
imageAspectRatio = 115f / 165f,
|
||||
titleColor = homeContentColor(),
|
||||
titleSize = 12.sp,
|
||||
titleSpacing = 4.dp,
|
||||
onClick = { onItemClick(item) },
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = item.seriesName ?: item.name,
|
||||
color = homeContentColor(),
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContinueWatchingPoster(item: EmbyItem, serverUrl: String, onItemClick: (EmbyItem) -> Unit = {}) {
|
||||
@@ -421,25 +408,27 @@ private fun ContinueWatchingPoster(item: EmbyItem, serverUrl: String, onItemClic
|
||||
val imageUrl = backdropTag?.let {
|
||||
"$serverUrl/emby/Items/${item.id}/Images/Backdrop/0?tag=$it&maxWidth=700&quality=75"
|
||||
} ?: item.posterUrlPath?.let { "$serverUrl$it" }
|
||||
val fallbackColors = listOf(Color(0xFF37474F), Color(0xFF546E7A))
|
||||
|
||||
Column(Modifier.width(220.dp), verticalArrangement = Arrangement.spacedBy(5.dp)) {
|
||||
Box(
|
||||
Modifier.fillMaxWidth().aspectRatio(16f / 9f)
|
||||
.clip(RoundedCornerShape(LumaShapes.small))
|
||||
.clickable { onItemClick(item) }
|
||||
.background(Brush.linearGradient(fallbackColors)),
|
||||
) {
|
||||
if (imageUrl != null) {
|
||||
LumaAsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current).data(imageUrl).crossfade(true).build(),
|
||||
contentDescription = item.name,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
val progress = item.runTimeTicks?.takeIf { it > 0L }?.let { runtimeTicks ->
|
||||
(item.playbackPositionMs.toDouble() * 10_000.0 / runtimeTicks)
|
||||
.toFloat()
|
||||
.coerceIn(0f, 1f)
|
||||
}
|
||||
LumaMediaCard(
|
||||
model = imageUrl?.let {
|
||||
ImageRequest.Builder(LocalContext.current).data(it).crossfade(true).build()
|
||||
},
|
||||
title = item.seriesName ?: item.name,
|
||||
modifier = Modifier.width(220.dp),
|
||||
imageAspectRatio = 16f / 9f,
|
||||
titleColor = homeContentColor(),
|
||||
titleSize = 13.sp,
|
||||
titleWeight = FontWeight.SemiBold,
|
||||
titleSpacing = 5.dp,
|
||||
progress = progress,
|
||||
onClick = { onItemClick(item) },
|
||||
) {
|
||||
Box(
|
||||
Modifier.fillMaxSize().background(Color.Black.copy(alpha = 0.2f)),
|
||||
Modifier.fillMaxSize().background(Color.Black.copy(alpha = 0.20f)),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
LumaIconView(
|
||||
@@ -450,15 +439,6 @@ private fun ContinueWatchingPoster(item: EmbyItem, serverUrl: String, onItemClic
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = item.seriesName ?: item.name,
|
||||
color = homeContentColor(),
|
||||
fontSize = 13.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -8,10 +8,8 @@ import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -37,10 +35,8 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@@ -56,11 +52,10 @@ import com.example.myapplication.ui.GlassTokens
|
||||
import com.example.myapplication.ui.LiquidGlassTopBar
|
||||
import com.example.myapplication.ui.SegmentedControl
|
||||
import com.example.myapplication.ui.bottomBarContentPadding
|
||||
import com.example.myapplication.ui.design.LumaColors
|
||||
import com.example.myapplication.ui.design.LumaEmptyState
|
||||
import com.example.myapplication.ui.design.LumaErrorState
|
||||
import com.example.myapplication.ui.design.LumaIcon
|
||||
import com.example.myapplication.ui.design.LumaAsyncImage
|
||||
import com.example.myapplication.ui.design.LumaMediaCard
|
||||
import com.example.myapplication.ui.design.LumaSkeleton
|
||||
import com.example.myapplication.ui.design.LumaShapes
|
||||
import top.yukonga.miuix.kmp.basic.Text
|
||||
@@ -197,33 +192,14 @@ private fun RecordsEmptyState(
|
||||
@Composable
|
||||
private fun FavoriteMediaCard(item: EmbyItem, serverUrl: String, onClick: () -> Unit) {
|
||||
val imageUrl = EmbyRepository.buildImageUrl(serverUrl, item, maxWidth = 400)
|
||||
Column(
|
||||
modifier = Modifier.clickable(onClick = onClick),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(0.7f)
|
||||
.clip(RoundedCornerShape(LumaShapes.small))
|
||||
.background(LumaColors.backgroundElevated)
|
||||
) {
|
||||
if (imageUrl != null) {
|
||||
LumaAsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current).data(imageUrl).crossfade(true).build(),
|
||||
contentDescription = item.name,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
LumaMediaCard(
|
||||
model = imageUrl?.let {
|
||||
ImageRequest.Builder(LocalContext.current).data(it).crossfade(true).build()
|
||||
},
|
||||
title = item.name,
|
||||
titleColor = GlassTokens.text,
|
||||
titleSize = 14.sp,
|
||||
titleSpacing = 6.dp,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = item.name,
|
||||
color = GlassTokens.text,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,8 @@ package com.example.myapplication.screens
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
@@ -28,9 +26,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@@ -49,7 +45,7 @@ import com.example.myapplication.ui.bottomBarContentPadding
|
||||
import com.example.myapplication.ui.design.LumaEmptyState
|
||||
import com.example.myapplication.ui.design.LumaErrorState
|
||||
import com.example.myapplication.ui.design.LumaIcon
|
||||
import com.example.myapplication.ui.design.LumaAsyncImage
|
||||
import com.example.myapplication.ui.design.LumaMediaCard
|
||||
import com.example.myapplication.ui.design.LumaSkeleton
|
||||
import kotlinx.coroutines.delay
|
||||
import top.yukonga.miuix.kmp.basic.Text
|
||||
@@ -191,36 +187,14 @@ private fun SearchEmptyState(title: String, subtitle: String) {
|
||||
@Composable
|
||||
private fun SearchMediaCard(item: EmbyItem, serverUrl: String, onClick: () -> Unit) {
|
||||
val imageUrl = EmbyRepository.buildImageUrl(serverUrl, item, maxWidth = 500)
|
||||
val fallback = listOf(Color(0xFF344054), Color(0xFF667085))
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick),
|
||||
verticalArrangement = Arrangement.spacedBy(7.dp),
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(0.7f)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(Brush.verticalGradient(fallback)),
|
||||
) {
|
||||
if (imageUrl != null) {
|
||||
LumaAsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current).data(imageUrl).crossfade(true).build(),
|
||||
contentDescription = item.name,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
LumaMediaCard(
|
||||
model = imageUrl?.let {
|
||||
ImageRequest.Builder(LocalContext.current).data(it).crossfade(true).build()
|
||||
},
|
||||
title = item.name,
|
||||
titleColor = GlassTokens.text,
|
||||
titleSize = 14.sp,
|
||||
titleSpacing = 7.dp,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = item.name,
|
||||
color = GlassTokens.text,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -74,13 +74,13 @@ fun LiquidAppBottomBar(
|
||||
selected = selectedTab == index,
|
||||
contentDescription = stringResource(tab.labelRes),
|
||||
tint = if (isGhostLayer) Color.White else if (selectedTab == index) LumaColors.accent else inactive,
|
||||
modifier = Modifier.size(34.dp),
|
||||
modifier = Modifier.size(40.dp),
|
||||
)
|
||||
Spacer(Modifier.height(3.dp))
|
||||
Spacer(Modifier.height(2.dp))
|
||||
Text(
|
||||
text = stringResource(tab.labelRes),
|
||||
color = if (isGhostLayer) Color.White else if (selectedTab == index) LumaColors.accent else inactive,
|
||||
fontSize = 10.sp,
|
||||
fontSize = 8.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
maxLines = 1,
|
||||
)
|
||||
@@ -112,7 +112,7 @@ fun LiquidAppBottomBar(
|
||||
if (LumaColors.isDark) {
|
||||
Color(0xFF1C2430).copy(alpha = 0.54f)
|
||||
} else {
|
||||
Color(0xFFE1E6EC).copy(alpha = 0.62f)
|
||||
Color(0xFFD4D9DF).copy(alpha = 0.78f)
|
||||
},
|
||||
)
|
||||
if (searchSelected) {
|
||||
@@ -126,8 +126,10 @@ fun LiquidAppBottomBar(
|
||||
selected = searchSelected,
|
||||
neutralVeil = if (LumaColors.isDark) {
|
||||
Color(0xFF1C2430).copy(alpha = 0.72f)
|
||||
} else if (AppTheme.glassMode == GlassMode.Liquid) {
|
||||
Color(0xFFD7E3F0).copy(alpha = 0.58f)
|
||||
} else {
|
||||
Color(0xFFE1E6EC).copy(alpha = 0.82f)
|
||||
Color(0xFFD4D9DF).copy(alpha = 0.84f)
|
||||
},
|
||||
)
|
||||
},
|
||||
@@ -151,12 +153,12 @@ fun LiquidAppBottomBar(
|
||||
icon = LumaIcon.Search,
|
||||
contentDescription = null,
|
||||
tint = if (searchSelected) LumaColors.accent else inactive,
|
||||
modifier = Modifier.size(36.dp),
|
||||
modifier = Modifier.size(44.dp),
|
||||
)
|
||||
Text(
|
||||
text = stringResource(Res.string.search),
|
||||
color = if (searchSelected) LumaColors.accent else inactive,
|
||||
fontSize = 10.sp,
|
||||
fontSize = 8.sp,
|
||||
fontWeight = FontWeight.ExtraBold,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -20,6 +21,9 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.gestures.awaitEachGesture
|
||||
@@ -38,6 +42,7 @@ import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
@@ -48,16 +53,28 @@ import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.semantics.ProgressBarRangeInfo
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.progressBarRangeInfo
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.setProgress
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import top.yukonga.miuix.kmp.basic.Text
|
||||
import kotlin.math.abs
|
||||
|
||||
private enum class LumaImageState {
|
||||
Loading,
|
||||
Success,
|
||||
Error,
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LumaAsyncImage(
|
||||
model: Any?,
|
||||
@@ -66,14 +83,185 @@ fun LumaAsyncImage(
|
||||
contentScale: ContentScale = ContentScale.Crop,
|
||||
) {
|
||||
val fallback = ColorPainter(LumaColors.backgroundElevated)
|
||||
val imageKey = (model as? ImageRequest)?.data ?: model
|
||||
var imageState by remember(imageKey) {
|
||||
mutableStateOf(if (model == null) LumaImageState.Error else LumaImageState.Loading)
|
||||
}
|
||||
Box(modifier) {
|
||||
AsyncImage(
|
||||
model = model,
|
||||
contentDescription = contentDescription,
|
||||
contentScale = contentScale,
|
||||
modifier = modifier,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
placeholder = fallback,
|
||||
error = fallback,
|
||||
onLoading = { imageState = LumaImageState.Loading },
|
||||
onSuccess = { imageState = LumaImageState.Success },
|
||||
onError = { imageState = LumaImageState.Error },
|
||||
)
|
||||
when (imageState) {
|
||||
LumaImageState.Loading -> LumaImageLoadingPlaceholder()
|
||||
LumaImageState.Success -> Unit
|
||||
LumaImageState.Error -> LumaImageErrorPlaceholder()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LumaImageLoadingPlaceholder() {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
LumaColors.backgroundElevated,
|
||||
LumaColors.background,
|
||||
LumaColors.backgroundElevated,
|
||||
),
|
||||
),
|
||||
),
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(LumaSpacing.sm),
|
||||
verticalArrangement = Arrangement.Bottom,
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth(0.62f)
|
||||
.height(8.dp)
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.background(LumaColors.textSecondary.copy(alpha = 0.16f)),
|
||||
)
|
||||
Spacer(Modifier.height(LumaSpacing.xs))
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth(0.38f)
|
||||
.height(6.dp)
|
||||
.clip(RoundedCornerShape(3.dp))
|
||||
.background(LumaColors.textSecondary.copy(alpha = 0.12f)),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LumaImageErrorPlaceholder() {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(LumaColors.backgroundElevated.copy(alpha = 0.96f)),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
LumaIconView(
|
||||
icon = LumaIcon.Image,
|
||||
contentDescription = null,
|
||||
tint = LumaColors.textSecondary,
|
||||
modifier = Modifier.size(28.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared media card surface. The image and title keep their natural geometry;
|
||||
* only the card surface receives the small pressed-state feedback.
|
||||
*/
|
||||
@Composable
|
||||
fun LumaMediaCard(
|
||||
model: Any?,
|
||||
title: String,
|
||||
modifier: Modifier = Modifier,
|
||||
imageAspectRatio: Float = 0.7f,
|
||||
contentScale: ContentScale = ContentScale.Crop,
|
||||
titleColor: Color = LumaColors.text,
|
||||
titleSize: TextUnit = LumaTypography.body,
|
||||
titleWeight: FontWeight = FontWeight.Medium,
|
||||
titleSpacing: Dp = LumaSpacing.xs,
|
||||
progress: Float? = null,
|
||||
onClick: (() -> Unit)? = null,
|
||||
overlay: @Composable BoxScope.() -> Unit = {},
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val pressed by interactionSource.collectIsPressedAsState()
|
||||
val scale by animateFloatAsState(
|
||||
targetValue = if (pressed) 0.985f else 1f,
|
||||
animationSpec = tween(LumaMotion.fast),
|
||||
label = "mediaCardPress",
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.graphicsLayer {
|
||||
scaleX = scale
|
||||
scaleY = scale
|
||||
}
|
||||
.then(
|
||||
if (onClick != null) {
|
||||
Modifier.clickable(
|
||||
interactionSource = interactionSource,
|
||||
indication = null,
|
||||
role = Role.Button,
|
||||
onClick = onClick,
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
)
|
||||
.semantics(mergeDescendants = true) {
|
||||
contentDescription = title
|
||||
},
|
||||
verticalArrangement = Arrangement.spacedBy(titleSpacing),
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(imageAspectRatio)
|
||||
.clip(RoundedCornerShape(LumaShapes.small))
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
listOf(LumaColors.backgroundElevated, LumaColors.background),
|
||||
),
|
||||
),
|
||||
) {
|
||||
LumaAsyncImage(
|
||||
model = model,
|
||||
contentDescription = if (onClick == null) title else null,
|
||||
contentScale = contentScale,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
overlay()
|
||||
progress?.let { value ->
|
||||
val clamped = value.coerceIn(0f, 1f)
|
||||
Box(
|
||||
Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = LumaSpacing.xs, vertical = LumaSpacing.xs)
|
||||
.height(3.dp)
|
||||
.clip(RoundedCornerShape(2.dp))
|
||||
.background(Color.White.copy(alpha = 0.30f)),
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth(clamped)
|
||||
.height(3.dp)
|
||||
.clip(RoundedCornerShape(2.dp))
|
||||
.background(LumaColors.accent),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = title,
|
||||
color = titleColor,
|
||||
fontSize = titleSize,
|
||||
fontWeight = titleWeight,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -78,39 +78,10 @@ object LumaMotion {
|
||||
val native = CubicBezierEasing(0.2f, 0.75f, 0.25f, 1f)
|
||||
}
|
||||
|
||||
/** Kyant Backdrop uses RenderEffect/GraphicsLayer and is unstable on some emulator GPUs. */
|
||||
/** Kyant Backdrop requires the Android 12 RenderEffect/graphics pipeline. */
|
||||
internal object LumaGlassRuntime {
|
||||
val isBackdropSafe: Boolean by lazy {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return@lazy false
|
||||
val fingerprint = Build.FINGERPRINT.lowercase()
|
||||
val board = Build.BOARD.lowercase()
|
||||
val hardware = Build.HARDWARE.lowercase()
|
||||
val product = Build.PRODUCT.lowercase()
|
||||
val device = Build.DEVICE.lowercase()
|
||||
val model = Build.MODEL.lowercase()
|
||||
val brand = Build.BRAND.lowercase()
|
||||
val manufacturer = Build.MANUFACTURER.lowercase()
|
||||
val isX86Emulator = Build.SUPPORTED_ABIS.any { it.contains("x86") }
|
||||
!(isX86Emulator ||
|
||||
fingerprint.startsWith("generic") ||
|
||||
fingerprint.contains("emulator") ||
|
||||
board.contains("goldfish") ||
|
||||
board.contains("ranchu") ||
|
||||
board.contains("emulator") ||
|
||||
hardware.contains("goldfish") ||
|
||||
hardware.contains("ranchu") ||
|
||||
hardware.contains("vbox") ||
|
||||
product.contains("sdk_gphone") ||
|
||||
product.contains("emulator") ||
|
||||
device.contains("generic") ||
|
||||
device.contains("emulator") ||
|
||||
device.contains("emu") ||
|
||||
device.contains("vbox") ||
|
||||
model.contains("sdk_gphone") ||
|
||||
model.contains("emulator") ||
|
||||
model.contains("android sdk built for") ||
|
||||
brand == "generic" ||
|
||||
manufacturer.contains("genymotion"))
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
}
|
||||
|
||||
val useNativeBackdrop: Boolean
|
||||
@@ -167,7 +138,34 @@ fun Modifier.lumaFallbackGlass(
|
||||
neutralVeil: Color? = null,
|
||||
): Modifier {
|
||||
val light = !LumaColors.isDark
|
||||
val liquidFallback = AppTheme.glassMode == com.example.myapplication.ui.GlassMode.Liquid
|
||||
val fill = when {
|
||||
liquidFallback && light && selected -> Brush.verticalGradient(
|
||||
listOf(
|
||||
Color.White.copy(alpha = 0.62f),
|
||||
Color(0xFFE5EFFA).copy(alpha = 0.52f),
|
||||
Color.White.copy(alpha = 0.42f),
|
||||
),
|
||||
)
|
||||
liquidFallback && light -> Brush.verticalGradient(
|
||||
listOf(
|
||||
Color.White.copy(alpha = 0.48f),
|
||||
Color(0xFFE4EDF7).copy(alpha = 0.40f),
|
||||
Color.White.copy(alpha = 0.30f),
|
||||
),
|
||||
)
|
||||
liquidFallback && selected -> Brush.verticalGradient(
|
||||
listOf(
|
||||
Color(0xFF60758A).copy(alpha = 0.70f),
|
||||
Color(0xFF304253).copy(alpha = 0.64f),
|
||||
),
|
||||
)
|
||||
liquidFallback -> Brush.verticalGradient(
|
||||
listOf(
|
||||
Color(0xFF44586C).copy(alpha = 0.60f),
|
||||
Color(0xFF233140).copy(alpha = 0.66f),
|
||||
),
|
||||
)
|
||||
light && selected -> Brush.verticalGradient(
|
||||
listOf(
|
||||
Color.White.copy(alpha = 0.82f),
|
||||
@@ -196,14 +194,42 @@ fun Modifier.lumaFallbackGlass(
|
||||
)
|
||||
}
|
||||
val edge = if (light) {
|
||||
Color.White.copy(alpha = if (selected) 0.78f else 0.64f)
|
||||
Color.White.copy(
|
||||
alpha = when {
|
||||
liquidFallback && selected -> 0.86f
|
||||
liquidFallback -> 0.70f
|
||||
selected -> 0.78f
|
||||
else -> 0.64f
|
||||
},
|
||||
)
|
||||
} else {
|
||||
Color.White.copy(alpha = if (selected) 0.28f else 0.20f)
|
||||
Color.White.copy(
|
||||
alpha = when {
|
||||
liquidFallback && selected -> 0.34f
|
||||
liquidFallback -> 0.24f
|
||||
selected -> 0.28f
|
||||
else -> 0.20f
|
||||
},
|
||||
)
|
||||
}
|
||||
val topHighlight = if (light) {
|
||||
Color.White.copy(alpha = if (selected) 0.32f else 0.22f)
|
||||
Color.White.copy(
|
||||
alpha = when {
|
||||
liquidFallback && selected -> 0.42f
|
||||
liquidFallback -> 0.30f
|
||||
selected -> 0.32f
|
||||
else -> 0.22f
|
||||
},
|
||||
)
|
||||
} else {
|
||||
Color.White.copy(alpha = if (selected) 0.10f else 0.06f)
|
||||
Color.White.copy(
|
||||
alpha = when {
|
||||
liquidFallback && selected -> 0.14f
|
||||
liquidFallback -> 0.08f
|
||||
selected -> 0.10f
|
||||
else -> 0.06f
|
||||
},
|
||||
)
|
||||
}
|
||||
return this
|
||||
.shadow(
|
||||
|
||||
@@ -29,19 +29,15 @@ 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.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
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.drawBackdrop
|
||||
import com.kyant.backdrop.effects.blur
|
||||
import com.kyant.backdrop.effects.lens
|
||||
@@ -54,7 +50,6 @@ 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.rememberLumaLayerBackdrop
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -64,9 +59,7 @@ import kotlin.math.sign
|
||||
/** true = the hidden accent-capture layer, false = the visible base layer */
|
||||
val LocalLiquidBottomTabsGhostLayer = staticCompositionLocalOf { false }
|
||||
|
||||
/** Current liquid pill position. Used to hide duplicated visible content beneath the pill. */
|
||||
val LocalLiquidBottomTabsPillValue = staticCompositionLocalOf { 0f }
|
||||
|
||||
/** Local marker retained for the bottom-tab content layer. */
|
||||
@Composable
|
||||
fun LiquidBottomTabs(
|
||||
selectedTabIndex: () -> Int,
|
||||
@@ -75,25 +68,35 @@ fun LiquidBottomTabs(
|
||||
tabsCount: Int,
|
||||
showSelection: () -> Boolean = { true },
|
||||
modifier: Modifier = Modifier,
|
||||
accentColor: Color = LumaColors.accent,
|
||||
content: @Composable RowScope.() -> Unit
|
||||
) {
|
||||
val isLightTheme = !LumaColors.isDark
|
||||
val containerColor = LumaColors.clearTint
|
||||
val neutralTint = if (isLightTheme) {
|
||||
Color(0xFFE1E6EC).copy(alpha = 0.62f)
|
||||
Color(0xFFD4D9DF).copy(alpha = 0.78f)
|
||||
} else {
|
||||
Color(0xFF1C2430).copy(alpha = 0.54f)
|
||||
}
|
||||
val selectedNeutralTint = if (isLightTheme) {
|
||||
Color(0xFFD1D7DF).copy(alpha = 0.54f)
|
||||
Color(0xFFC1C8D0).copy(alpha = 0.82f)
|
||||
} else {
|
||||
Color(0xFF26313E).copy(alpha = 0.44f)
|
||||
}
|
||||
val liquidFallback = com.example.myapplication.ui.AppTheme.glassMode == com.example.myapplication.ui.GlassMode.Liquid
|
||||
val fallbackContainerVeil = if (isLightTheme) {
|
||||
if (liquidFallback) Color(0xFFD7E3F0).copy(alpha = 0.54f)
|
||||
else Color(0xFFD4D9DF).copy(alpha = 0.78f)
|
||||
} else {
|
||||
Color(0xFF1C2430).copy(alpha = 0.70f)
|
||||
}
|
||||
val fallbackSelectedVeil = if (isLightTheme) {
|
||||
if (liquidFallback) Color(0xFFC9D8E8).copy(alpha = 0.60f)
|
||||
else Color(0xFFC1C8D0).copy(alpha = 0.82f)
|
||||
} else {
|
||||
Color(0xFF26313E).copy(alpha = 0.62f)
|
||||
}
|
||||
val useBackdrop = LumaGlassRuntime.useNativeBackdrop && backdrop != null
|
||||
|
||||
val tabsBackdrop = rememberLumaLayerBackdrop()
|
||||
|
||||
BoxWithConstraints(
|
||||
modifier,
|
||||
contentAlignment = Alignment.CenterStart
|
||||
@@ -186,11 +189,7 @@ fun LiquidBottomTabs(
|
||||
.fillMaxWidth()
|
||||
.lumaFallbackGlass(
|
||||
shape = Capsule(),
|
||||
neutralVeil = if (isLightTheme) {
|
||||
Color(0xFFE1E6EC).copy(alpha = 0.58f)
|
||||
} else {
|
||||
Color(0xFF1C2430).copy(alpha = 0.70f)
|
||||
},
|
||||
neutralVeil = fallbackContainerVeil,
|
||||
),
|
||||
)
|
||||
if (showSelection()) {
|
||||
@@ -209,25 +208,18 @@ fun LiquidBottomTabs(
|
||||
.lumaFallbackGlass(
|
||||
shape = Capsule(),
|
||||
selected = true,
|
||||
neutralVeil = if (isLightTheme) {
|
||||
Color(0xFFD1D7DF).copy(alpha = 0.56f)
|
||||
} else {
|
||||
Color(0xFF26313E).copy(alpha = 0.62f)
|
||||
},
|
||||
neutralVeil = fallbackSelectedVeil,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
Modifier
|
||||
.graphicsLayer {
|
||||
translationX = panelOffset
|
||||
}
|
||||
.clip(Capsule())
|
||||
.then(
|
||||
if (useBackdrop) {
|
||||
Modifier.drawBackdrop(
|
||||
Box(
|
||||
Modifier
|
||||
.graphicsLayer { translationX = panelOffset }
|
||||
.clip(Capsule())
|
||||
.drawBackdrop(
|
||||
backdrop = backdrop,
|
||||
shape = { Capsule() },
|
||||
effects = {
|
||||
@@ -246,86 +238,15 @@ fun LiquidBottomTabs(
|
||||
drawRect(neutralTint)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
)
|
||||
.then(
|
||||
if (useBackdrop) {
|
||||
Modifier.border(0.6.dp, LumaColors.glassBorder.copy(alpha = 0.72f), Capsule())
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
)
|
||||
.then(interactiveHighlight.modifier)
|
||||
.border(0.6.dp, LumaColors.glassBorder.copy(alpha = 0.72f), Capsule())
|
||||
.height(64f.dp)
|
||||
.fillMaxWidth()
|
||||
.padding(LumaSpacing.xxs)
|
||||
.clip(Capsule()),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
content = {
|
||||
CompositionLocalProvider(
|
||||
LocalLiquidBottomTabsGhostLayer provides false,
|
||||
LocalLiquidBottomTabsPillValue provides dampedDragAnimation.value,
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalLiquidBottomTabScale provides {
|
||||
lerp(1f, 1.2f, dampedDragAnimation.pressProgress)
|
||||
}
|
||||
) {
|
||||
Row(
|
||||
Modifier
|
||||
.clearAndSetSemantics {}
|
||||
.alpha(0f)
|
||||
.clip(Capsule())
|
||||
.then(if (useBackdrop && tabsBackdrop != null) Modifier.layerBackdrop(tabsBackdrop) else Modifier)
|
||||
.graphicsLayer {
|
||||
translationX = panelOffset
|
||||
}
|
||||
.then(
|
||||
if (useBackdrop) {
|
||||
Modifier.drawBackdrop(
|
||||
backdrop = backdrop,
|
||||
shape = { Capsule() },
|
||||
effects = {
|
||||
val progress = dampedDragAnimation.pressProgress
|
||||
vibrancy()
|
||||
blur(8f.dp.toPx())
|
||||
lens(
|
||||
24f.dp.toPx() * progress,
|
||||
24f.dp.toPx() * progress,
|
||||
)
|
||||
},
|
||||
highlight = {
|
||||
val progress = dampedDragAnimation.pressProgress
|
||||
Highlight.Plain.copy(alpha = progress)
|
||||
},
|
||||
onDrawSurface = { drawRect(containerColor) },
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
)
|
||||
.then(interactiveHighlight.modifier)
|
||||
.height(56f.dp)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 4f.dp)
|
||||
.graphicsLayer(colorFilter = ColorFilter.tint(accentColor)),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
content = {
|
||||
CompositionLocalProvider(LocalLiquidBottomTabsGhostLayer provides true) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (useBackdrop) {
|
||||
if (useBackdrop && showSelection()) {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(horizontal = 4f.dp)
|
||||
@@ -337,9 +258,8 @@ fun LiquidBottomTabs(
|
||||
}
|
||||
.then(interactiveHighlight.gestureModifier)
|
||||
.then(dampedDragAnimation.modifier)
|
||||
.then(
|
||||
Modifier.drawBackdrop(
|
||||
backdrop = rememberCombinedBackdrop(backdrop, tabsBackdrop!!),
|
||||
.drawBackdrop(
|
||||
backdrop = backdrop,
|
||||
shape = { Capsule() },
|
||||
effects = {
|
||||
lens(
|
||||
@@ -380,12 +300,32 @@ fun LiquidBottomTabs(
|
||||
)
|
||||
drawRect(Color.Black.copy(alpha = 0.03f * progress))
|
||||
},
|
||||
),
|
||||
)
|
||||
.height(56f.dp)
|
||||
.fillMaxWidth(1f / tabsCount)
|
||||
.alpha(if (showSelection()) 1f else 0f)
|
||||
.alpha(1f),
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
Modifier
|
||||
.graphicsLayer {
|
||||
translationX = panelOffset
|
||||
}
|
||||
.clip(Capsule())
|
||||
.then(interactiveHighlight.modifier)
|
||||
.height(64f.dp)
|
||||
.fillMaxWidth()
|
||||
.padding(LumaSpacing.xxs)
|
||||
.clip(Capsule()),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
content = {
|
||||
CompositionLocalProvider(
|
||||
LocalLiquidBottomTabsGhostLayer provides false,
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,3 +9,9 @@
|
||||
文件变更: ui/design/LumaDesignSystem.kt、screens/HomeScreen.kt、navigation/AppNavHost.kt、screens/MediaDetailScreen.kt。
|
||||
教训: 页面请求异常需保留取消异常;可保存滚动状态和 launchSingleTop 能减少返回闪烁与重复页面。
|
||||
标签: #lumaplayer #navigation #state-restoration #fallback-ui
|
||||
|
||||
[PROJECT:lumaplayer] 取消液态玻璃安全门
|
||||
结论: API 31+ 液态模式直接使用 Kyant Backdrop,底栏玻璃层级和媒体卡片三态完成收尾,API35 模拟器切换模式未复现 RenderThread 崩溃。
|
||||
文件变更: ui/design/LumaDesignSystem.kt、ui/liquidglass/LiquidBottomTabs.kt、ui/AppBottomBar.kt、ui/design/LumaComponents.kt、screens/HomeScreen.kt、screens/RecordsScreen.kt、screens/SearchScreen.kt。
|
||||
教训: 直接启用原生 Backdrop 后必须保持玻璃层与清晰内容分层;实体机 GPU 尚未验证。
|
||||
标签: #lumaplayer #liquid-glass #backdrop #fallback-ui #renderthread
|
||||
+3
-3
@@ -3,10 +3,10 @@
|
||||
- 项目:lumaplayer,Android Kotlin/Compose 媒体库客户端。
|
||||
- 当前 Git 分支:`master`。
|
||||
- 最近同步:`master` 已完成底栏图标与安全降级玻璃收尾,提交前工作区仅保留本地参考素材未跟踪。
|
||||
- 当前实现:玻璃模式改为设置页手动选择“液态玻璃/毛玻璃”,不再按帧率自动升降;液态模式在安全真机使用 Kyant0 Backdrop 2.0.1,模拟器/低版本仍在创建 Backdrop 前安全降级为渐变毛玻璃;页面捕获层仅服务根 Tab/底栏,子页玻璃无源时安全降级;添加服务器为独立普通表单页面并支持 HTTP/HTTPS;页面导航使用 Navigation Compose。
|
||||
- 最新稳定性策略:通过 `LumaGlassRuntime` 识别常见 Android Emulator GPU,模拟器跳过 Kyant RenderEffect/GraphicsLayer 采样,真机保留 Backdrop;播放器不捕获 AndroidView。
|
||||
- 当前实现:玻璃模式改为设置页手动选择“液态玻璃/毛玻璃”,不再按帧率自动升降;API 31+ 的液态模式直接使用 Kyant0 Backdrop 2.0.1,API 30 及以下降级为渐变毛玻璃;页面捕获层仅服务根 Tab/底栏,子页玻璃无源时安全降级;添加服务器为独立普通表单页面并支持 HTTP/HTTPS;页面导航使用 Navigation Compose。
|
||||
- 最新稳定性策略:按需求移除设备/模拟器安全门,只保留 Android 12+ API 能力判断;播放器不捕获 AndroidView。实体机 GPU 稳定性仍需单独验证。
|
||||
- 当前 UI:新增 `ui/design` 令牌、公共加载/空状态/按钮、Aurora 图标子集和授权说明;主要页面已接入统一颜色、间距、圆角和动效。
|
||||
- 构建验证:Backdrop 层级收敛、模拟器安全降级、播放器采样隔离和 GlassSheet 收尾后 `assembleDebug`、`lintDebug`、`testDebugUnitTest` 成功;已在 `sdk_gphone64_arm64` API 35 模拟器安装启动并点击 Vision/Settings/语言菜单,未出现 `SIGSEGV/RenderThread`,尚未做真机录屏比对。
|
||||
- 构建验证:Backdrop 层级收敛、播放器采样隔离和 GlassSheet 收尾后 `assembleDebug`、`lintDebug`、`testDebugUnitTest` 成功;已在 `sdk_gphone64_arm64` API 35 模拟器安装启动并切换液态/毛玻璃,当前测试未出现 `SIGSEGV/RenderThread`,尚未做真机录屏比对。
|
||||
- 详细状态见 [projects.md](projects.md),踩坑见 [lessons.md](lessons.md)。
|
||||
- 最新收尾:公共分段控件、按钮、输入框、主题/语言/资源库弹层统一走同一套降级毛玻璃;底栏降级选中胶囊改为内容下层绘制;新增玻璃模式持久化设置;状态栏/导航栏图标随深色主题同步;Debug/Lint/Unit 与 API35 模拟器回归通过。
|
||||
- 最新改动:播放器进度条改为自绘可访问 SeekBar,媒体首页、Vision 收藏和搜索加载态改为统一骨架占位;移除未使用的 Material 图标依赖,完整 Debug/Lint/Unit 通过。
|
||||
|
||||
@@ -20,3 +20,4 @@
|
||||
- 高:液态玻璃不能以“API 版本高”作为唯一启用条件;Kyant Backdrop 还需通过设备安全门和运行帧率门控,低帧率/不安全 GPU 必须在创建 LayerBackdrop 前降级,避免 RenderThread 崩溃。
|
||||
- 高:玻璃模式已改为用户手动选择,不能再用 Choreographer 帧率变化自动切换;但模拟器/低版本仍必须在创建 Kyant LayerBackdrop 前保留硬件安全门,避免 RenderThread SIGSEGV。
|
||||
- 中:降级玻璃的边缘高光应在 `drawContent()` 之前绘制;否则公共玻璃容器中的文字和图标会被高光层洗白。
|
||||
- 高:用户明确要求真实液态玻璃时,不能继续保留设备/模拟器安全门;当前仅保留 API 31+ 能力判断,底栏原生 Backdrop 必须先绘制容器与选中胶囊,再绘制清晰图标/文字,避免内容被二次采样覆盖。该取舍需在实体机 GPU 上继续验证 RenderThread 稳定性。
|
||||
@@ -38,3 +38,4 @@
|
||||
- 2026-08-27:底栏图标调整为 36dp、搜索图标 41dp,资源库无图标服务器使用正确的服务器图标;搜索清除与图标库返回补足 48dp 触控范围。完整 Debug/Lint/Unit 构建和 API35 模拟器回归通过。
|
||||
- 2026-08-28:完整读取本地 `luna/README.txt`、`AI_PROMPT.txt` 与降级参考 contact sheet;修复降级底栏选中胶囊覆盖内容的问题,统一为稳定渐变毛玻璃;移除 Choreographer 帧率自动切换,设置页新增液态玻璃/毛玻璃手动选择并持久化;合并远端 3 个播放器提交后 `assembleDebug`、`lintDebug` 与 API35 模拟器启动回归通过。
|
||||
- 2026-08-28:继续收尾降级 UI;修复 fallback 高光绘制顺序,首页接入可保存滚动状态,详情/播放器导航启用 `launchSingleTop` 防重复 Push,详情刷新和剧集加载异常进入已有错误态;`assembleDebug`、`lintDebug`、`testDebugUnitTest` 及 API35 模拟器启动回归成功。
|
||||
- 2026-08-28:按需求取消液态玻璃设备/模拟器安全门,API 31+ 液态模式直接进入 Kyant Backdrop 原生路径;底栏改为玻璃底层、选中胶囊、清晰内容的正确绘制顺序,主图标 40dp、搜索图标 44dp、文字 8sp;补充媒体卡片加载/成功/失败三态和统一按压反馈。`assembleDebug`、`lintDebug`、`testDebugUnitTest` 及 API35 模拟器液态/毛玻璃切换回归成功,未发现 RenderThread 崩溃。
|
||||
Reference in new issue
Block a user