图标和界面优化

This commit is contained in:
zzh 2026-07-14 09:47:22 +08:00
parent 8051ff242a
commit e2c6741afc
14 changed files with 183 additions and 331 deletions

View File

@ -1,6 +1,5 @@
package com.example.smarthome package com.example.smarthome
import android.graphics.drawable.Drawable
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import com.example.smarthome.ui.theme.SmartHomeTheme import com.example.smarthome.ui.theme.SmartHomeTheme
@ -54,35 +53,22 @@ class MainActivity : ComponentActivity() {
} }
private fun hideStatusBar() { private fun hideStatusBar() {
// 设置全屏模式,让内容延伸到系统栏区域 // 让内容延伸到系统栏区域
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
// 设置全屏标志 // 使用现代 WindowInsetsController API 隐藏状态栏和导航栏
window.setFlags( androidx.core.view.WindowInsetsControllerCompat(window, window.decorView).apply {
android.view.WindowManager.LayoutParams.FLAG_FULLSCREEN, hide(androidx.core.view.WindowInsetsCompat.Type.statusBars())
android.view.WindowManager.LayoutParams.FLAG_FULLSCREEN hide(androidx.core.view.WindowInsetsCompat.Type.navigationBars())
) systemBarsBehavior = androidx.core.view.WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
// 设置状态栏和导航栏为透明
window.statusBarColor = android.graphics.Color.TRANSPARENT
window.navigationBarColor = android.graphics.Color.TRANSPARENT
// 隐藏状态栏和导航栏
window.decorView.systemUiVisibility = (
android.view.View.SYSTEM_UI_FLAG_FULLSCREEN
or android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or android.view.View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or android.view.View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or android.view.View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
)
// 支持刘海屏设备 // 支持刘海屏设备
val lp = window.attributes
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
lp.layoutInDisplayCutoutMode = android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES window.attributes = window.attributes.also {
it.layoutInDisplayCutoutMode = android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
}
} }
window.attributes = lp
} }
} }
@ -134,10 +120,8 @@ fun MainContent() {
rooms = roomList rooms = roomList
} }
// 初始化背景管理器并获取当前选中的背景 // 初始化背景管理器并获取当前选中的背景(同步初始化,避免首帧闪烁默认背景)
androidx.compose.runtime.LaunchedEffect(Unit) { remember { BackgroundManager.init(context) }
BackgroundManager.init(context)
}
val selectedBgId by BackgroundManager.selectedBackground.collectAsState() val selectedBgId by BackgroundManager.selectedBackground.collectAsState()
val currentBg = BackgroundManager.backgrounds.getOrNull(selectedBgId) val currentBg = BackgroundManager.backgrounds.getOrNull(selectedBgId)
val backgroundRes = currentBg?.resourceId ?: R.drawable.background1 val backgroundRes = currentBg?.resourceId ?: R.drawable.background1

View File

@ -10,7 +10,6 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.animation.core.* import androidx.compose.animation.core.*
@ -20,13 +19,10 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.draw.scale import androidx.compose.ui.draw.scale
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.blur
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@ -71,8 +67,6 @@ import android.content.ContextWrapper
import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.HazeStyle import dev.chrisbanes.haze.HazeStyle
import dev.chrisbanes.haze.HazeTint import dev.chrisbanes.haze.HazeTint
import dev.chrisbanes.haze.haze
import dev.chrisbanes.haze.hazeChild
import dev.chrisbanes.haze.hazeSource import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.hazeEffect import dev.chrisbanes.haze.hazeEffect
@ -93,6 +87,12 @@ fun MainScaffold(
// Haze state for blur effects // Haze state for blur effects
val hazeState = remember { HazeState() } val hazeState = remember { HazeState() }
// 读取当前选中的背景壁纸
val context = androidx.compose.ui.platform.LocalContext.current
remember { BackgroundManager.init(context) }
val selectedBgId by BackgroundManager.selectedBackground.collectAsState()
val backgroundRes = BackgroundManager.backgrounds.getOrNull(selectedBgId)?.resourceId ?: R.drawable.background1
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
// 背景作为模糊源 // 背景作为模糊源
Box( Box(
@ -102,7 +102,7 @@ fun MainScaffold(
) { ) {
// 背景图片 // 背景图片
Image( Image(
painter = painterResource(id = R.drawable.background), painter = painterResource(id = backgroundRes),
contentDescription = null, contentDescription = null,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
@ -181,7 +181,7 @@ fun MainScaffold(
} }
} }
} }
} }
} }
@Composable @Composable
@ -349,11 +349,97 @@ fun AllDevicesOverview(modifier: Modifier = Modifier) {
Device("影音室电视", "运行中", R.drawable.ic_media, true), Device("影音室电视", "运行中", R.drawable.ic_media, true),
Device("游戏房主机", "待机中", R.drawable.ic_media, false) Device("游戏房主机", "待机中", R.drawable.ic_media, false)
) )
val rows = allDevices.chunked(2)
Column( Column(
modifier = modifier, modifier = modifier,
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
allDevices.forEach { d -> DeviceCard(d) } rows.forEach { rowDevices ->
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
rowDevices.forEach { d ->
DeviceSquareCard(d, modifier = Modifier.weight(1f))
}
if (rowDevices.size < 2) {
Spacer(modifier = Modifier.weight(1f))
}
}
}
}
}
@Composable
fun DeviceSquareCard(d: Device, modifier: Modifier = Modifier) {
var active by remember { mutableStateOf(d.on) }
val accentColor = if (active) Color(0xFF00D1FF) else Color(0xFF5A5A6E)
val bgAlpha = if (active) 0.75f else 0.45f
Box(
modifier = modifier
.aspectRatio(1f)
.clip(RoundedCornerShape(20.dp))
.background(Color(0xFF1A1A1A).copy(alpha = bgAlpha))
.border(
width = 1.dp,
color = if (active) accentColor.copy(alpha = 0.4f) else Color.White.copy(alpha = 0.12f),
shape = RoundedCornerShape(20.dp)
)
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) { active = !active }
.padding(16.dp)
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.SpaceBetween
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Top
) {
Box(
modifier = Modifier
.size(44.dp)
.clip(RoundedCornerShape(12.dp))
.background(accentColor.copy(alpha = if (active) 0.2f else 0.1f)),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(id = d.icon),
contentDescription = null,
modifier = Modifier.size(26.dp),
colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(
if (active) accentColor else Color(0xFF6A6A7E)
)
)
}
Box(
modifier = Modifier
.size(8.dp)
.clip(RoundedCornerShape(4.dp))
.background(if (active) Color(0xFF00E676) else Color(0xFF4A4A5E))
)
}
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(
text = d.name,
color = Color.White,
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
maxLines = 1
)
Text(
text = d.sub,
color = if (active) accentColor else Color(0xFF6A6A7E),
fontSize = 12.sp,
maxLines = 1
)
}
}
} }
} }
@ -401,24 +487,16 @@ fun QuickActions() {
@Composable @Composable
fun QuickActionCard(title: String, colors: Brush, modifier: Modifier = Modifier, onClick: () -> Unit) { fun QuickActionCard(title: String, colors: Brush, modifier: Modifier = Modifier, onClick: () -> Unit) {
val shape = RoundedCornerShape(24.dp) val shape = RoundedCornerShape(24.dp)
var pressed by remember { mutableStateOf(false) }
val scale by androidx.compose.animation.core.animateFloatAsState(
targetValue = if (pressed) 0.97f else 1f,
animationSpec = androidx.compose.animation.core.spring(),
label = "quick_action_scale"
)
Box( Box(
modifier = modifier modifier = modifier
.height(88.dp) .height(88.dp)
.shadow(12.dp, shape, false) .shadow(12.dp, shape, false)
.clip(shape) .clip(shape)
.background(colors) .background(colors)
.scale(scale)
.clickable( .clickable(
indication = null, indication = null,
interactionSource = remember { MutableInteractionSource() } interactionSource = remember { MutableInteractionSource() }
) { ) {
pressed = !pressed
onClick() onClick()
} }
.padding(16.dp), .padding(16.dp),
@ -714,23 +792,6 @@ fun FloatingBottomNav(
// 拖动时的临时选中项 // 拖动时的临时选中项
var dragIndex by remember { mutableStateOf(selectedNavItem) } var dragIndex by remember { mutableStateOf(selectedNavItem) }
// 呼吸效果:不操作时变透明
var lastInteractionTime by remember { mutableStateOf(System.currentTimeMillis()) }
var isIdle by remember { mutableStateOf(false) }
// 检测空闲状态3秒无操作
LaunchedEffect(lastInteractionTime) {
kotlinx.coroutines.delay(3000)
isIdle = true
}
// 菜单栏透明度动画
val navBarAlpha by animateFloatAsState(
targetValue = if (isIdle && !isDragging) 0.4f else 1f,
animationSpec = tween(durationMillis = 500, easing = androidx.compose.animation.core.EaseInOut),
label = "nav_bar_alpha"
)
// 当外部 selectedNavItem 变化时,同步更新 dragIndex // 当外部 selectedNavItem 变化时,同步更新 dragIndex
LaunchedEffect(selectedNavItem) { LaunchedEffect(selectedNavItem) {
if (!isDragging) { if (!isDragging) {
@ -738,15 +799,15 @@ fun FloatingBottomNav(
} }
} }
// 选中框的动画偏移量 - 跟随 dragIndex拖动时或 selectedNavItem点击时 // 选中指示器偏移 - 拖动时 snap 跟随手指,松手后快速回弹
val currentIndex = if (isDragging) dragIndex else selectedNavItem val currentIndex = if (isDragging) dragIndex else selectedNavItem
val animatedOffset by animateFloatAsState( val animatedOffset by animateFloatAsState(
targetValue = currentIndex * (itemWidth.value + itemSpacing.value), targetValue = currentIndex * (itemWidth.value + itemSpacing.value),
animationSpec = spring( animationSpec = if (isDragging) snap() else tween(
dampingRatio = Spring.DampingRatioMediumBouncy, durationMillis = 180,
stiffness = Spring.StiffnessHigh easing = androidx.compose.animation.core.FastOutSlowInEasing
), ),
label = "liquid_glass_offset" label = "nav_indicator_offset"
) )
// 菜单栏高度(胶囊形) // 菜单栏高度(胶囊形)
@ -754,20 +815,9 @@ fun FloatingBottomNav(
val navBarPadding = 10.dp val navBarPadding = 10.dp
val innerHeight = navBarHeight - navBarPadding * 2 // 48dp val innerHeight = navBarHeight - navBarPadding * 2 // 48dp
// 选中指示器尺寸 - 按住时放大到贴合菜单栏边缘
val indicatorSize by animateFloatAsState(
targetValue = if (isDragging) (navBarHeight.value - 4f) else innerHeight.value,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessMedium
),
label = "indicator_size"
)
Box( Box(
modifier = modifier modifier = modifier
.height(navBarHeight) .height(navBarHeight)
.alpha(navBarAlpha)
.shadow(24.dp, RoundedCornerShape(50)) .shadow(24.dp, RoundedCornerShape(50))
.clip(RoundedCornerShape(50)) .clip(RoundedCornerShape(50))
.background( .background(
@ -792,9 +842,6 @@ fun FloatingBottomNav(
.pointerInput(Unit) { .pointerInput(Unit) {
detectHorizontalDragGestures( detectHorizontalDragGestures(
onDragStart = { offset -> onDragStart = { offset ->
// 重置空闲状态
isIdle = false
lastInteractionTime = System.currentTimeMillis()
isDragging = true isDragging = true
val index = (offset.x / (itemWidth.toPx() + itemSpacing.toPx())).toInt() val index = (offset.x / (itemWidth.toPx() + itemSpacing.toPx())).toInt()
.coerceIn(0, navItems.size - 1) .coerceIn(0, navItems.size - 1)
@ -802,12 +849,10 @@ fun FloatingBottomNav(
}, },
onDragEnd = { onDragEnd = {
isDragging = false isDragging = false
lastInteractionTime = System.currentTimeMillis()
onNavItemSelect(dragIndex) onNavItemSelect(dragIndex)
}, },
onDragCancel = { onDragCancel = {
isDragging = false isDragging = false
lastInteractionTime = System.currentTimeMillis()
dragIndex = selectedNavItem dragIndex = selectedNavItem
}, },
onHorizontalDrag = { change, _ -> onHorizontalDrag = { change, _ ->
@ -824,7 +869,8 @@ fun FloatingBottomNav(
Box( Box(
contentAlignment = Alignment.CenterStart contentAlignment = Alignment.CenterStart
) { ) {
// 选中指示器 - 按住时放大贴合菜单栏边缘 // 选中指示器
val indicatorSize = innerHeight.value
val horizontalOffset = (itemWidth.value - indicatorSize) / 2f val horizontalOffset = (itemWidth.value - indicatorSize) / 2f
Box( Box(
@ -861,12 +907,7 @@ fun FloatingBottomNav(
indication = null, indication = null,
interactionSource = remember { MutableInteractionSource() } interactionSource = remember { MutableInteractionSource() }
) { ) {
if (!isDragging) { onNavItemSelect(index)
// 重置空闲状态
isIdle = false
lastInteractionTime = System.currentTimeMillis()
onNavItemSelect(index)
}
}, },
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
@ -962,18 +1003,11 @@ fun ThemeModeToggle(isDarkMode: Boolean, onToggle: () -> Unit) {
@Composable @Composable
fun NavRailItem(text: String, iconRes: Int, selected: Boolean, onClick: () -> Unit = {}) { fun NavRailItem(text: String, iconRes: Int, selected: Boolean, onClick: () -> Unit = {}) {
val scale by androidx.compose.animation.core.animateFloatAsState(
targetValue = if (selected) 1.05f else 1f,
animationSpec = androidx.compose.animation.core.spring(),
label = "nav_scale"
)
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(6.dp), verticalArrangement = Arrangement.spacedBy(6.dp),
modifier = Modifier modifier = Modifier
.width(80.dp) // 固定宽度,防止文字长度不一致导致图标不对齐 .width(80.dp) // 固定宽度,防止文字长度不一致导致图标不对齐
.scale(scale)
.clickable( .clickable(
indication = null, indication = null,
interactionSource = remember { MutableInteractionSource() } interactionSource = remember { MutableInteractionSource() }
@ -1182,20 +1216,20 @@ fun WeatherCard(weatherInfo: WeatherInfo, isLoading: Boolean) {
color = accentColor.copy(alpha = 0.2f), color = accentColor.copy(alpha = 0.2f),
shape = RoundedCornerShape(16.dp) shape = RoundedCornerShape(16.dp)
) )
.padding(start = 4.dp, top = 4.dp, bottom = 4.dp, end = 20.dp), .padding(start = 4.dp, top = 2.dp, bottom = 2.dp, end = 12.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
// 天气图标(带阴影的渐变背景) // 天气图标(带阴影的渐变背景)
Box( Box(
modifier = Modifier modifier = Modifier
.size(48.dp) .size(30.dp)
.shadow( .shadow(
elevation = 8.dp, elevation = 4.dp,
shape = RoundedCornerShape(14.dp), shape = RoundedCornerShape(8.dp),
spotColor = weatherColors.second.copy(alpha = 0.3f) spotColor = weatherColors.second.copy(alpha = 0.3f)
) )
.clip(RoundedCornerShape(14.dp)) .clip(RoundedCornerShape(8.dp))
.background( .background(
Brush.linearGradient( Brush.linearGradient(
colors = listOf(weatherColors.first, weatherColors.second), colors = listOf(weatherColors.first, weatherColors.second),
@ -1215,74 +1249,37 @@ fun WeatherCard(weatherInfo: WeatherInfo, isLoading: Boolean) {
else -> R.drawable.ic_weather_cloudy else -> R.drawable.ic_weather_cloudy
}), }),
contentDescription = "天气", contentDescription = "天气",
modifier = Modifier.size(24.dp), modifier = Modifier.size(18.dp),
colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(Color.White) colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(Color.White)
) )
} }
// 温度和天气状况 // 温度和天气状况
Column { Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text( Text(
text = if (isLoading) "--°C" else "${weatherInfo.temperature}°C", text = if (isLoading) "--°C" else "${weatherInfo.temperature}°C",
fontSize = 22.sp, fontSize = 15.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = Color.White, color = Color.White,
lineHeight = 22.sp lineHeight = 15.sp
) )
Text(
text = if (isLoading) "加载中" else weatherInfo.weather,
fontSize = 11.sp,
fontWeight = FontWeight.Medium,
color = accentColor.copy(alpha = 0.8f)
)
}
// 分隔线
Box(
modifier = Modifier
.width(1.dp)
.height(32.dp)
.background(Color.White.copy(alpha = 0.1f))
)
// 湿度和AQI
Column(
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
// 湿度
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp) horizontalArrangement = Arrangement.spacedBy(4.dp)
) { ) {
Image(
painter = painterResource(id = R.drawable.ic_humidity),
contentDescription = "湿度",
modifier = Modifier.size(12.dp),
colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(Color(0xFFD1D5DB))
)
Text( Text(
text = if (isLoading) "--%" else "${weatherInfo.humidity}%", text = if (isLoading) "加载中" else weatherInfo.weather,
fontSize = 11.sp, fontSize = 11.sp,
color = Color(0xFFD1D5DB) color = accentColor.copy(alpha = 0.9f)
)
}
// AQI
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
Image(
painter = painterResource(id = R.drawable.ic_wind),
contentDescription = "AQI",
modifier = Modifier.size(12.dp),
colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(Color(0xFFD1D5DB))
)
Text(
text = if (isLoading) "AQI --" else "AQI ${weatherInfo.aqi}",
fontSize = 11.sp,
color = Color(0xFFD1D5DB)
) )
if (!isLoading && weatherInfo.city.isNotEmpty() && weatherInfo.city != "未知") {
Text(text = "·", fontSize = 11.sp, color = Color.White.copy(alpha = 0.3f))
Text(
text = weatherInfo.city,
fontSize = 11.sp,
color = Color.White.copy(alpha = 0.55f)
)
}
} }
} }
} }
@ -1404,17 +1401,10 @@ fun UsageStatusChart(modifier: Modifier = Modifier, roomName: String = "房间",
Box( Box(
modifier = modifier modifier = modifier
.clip(RoundedCornerShape(24.dp)) .clip(RoundedCornerShape(24.dp))
.hazeEffect( .background(Color(0xFF1A1A1A).copy(alpha = 0.55f))
state = hazeState,
style = HazeStyle(
blurRadius = 40.dp,
tint = HazeTint(Color.White.copy(alpha = 0.15f)),
noiseFactor = 0.0f
)
)
.border( .border(
width = 1.dp, width = 1.dp,
color = Color.White.copy(alpha = 0.3f), color = Color.White.copy(alpha = 0.15f),
shape = RoundedCornerShape(24.dp) shape = RoundedCornerShape(24.dp)
) )
.padding(20.dp) .padding(20.dp)
@ -1535,7 +1525,7 @@ fun EnergyTrendChart() {
val prevX = (index - 1) * stepX val prevX = (index - 1) * stepX
val prevY = canvasHeight - (dataPoints[index - 1] * canvasHeight) val prevY = canvasHeight - (dataPoints[index - 1] * canvasHeight)
val controlX = (prevX + x) / 2 val controlX = (prevX + x) / 2
quadraticBezierTo(controlX, prevY, x, y) quadraticTo(controlX, prevY, x, y)
} }
} }
@ -1565,7 +1555,7 @@ fun EnergyTrendChart() {
val prevX = (index - 1) * stepX val prevX = (index - 1) * stepX
val prevY = canvasHeight - (dataPoints[index - 1] * canvasHeight) val prevY = canvasHeight - (dataPoints[index - 1] * canvasHeight)
val controlX = (prevX + x) / 2 val controlX = (prevX + x) / 2
quadraticBezierTo(controlX, prevY, x, y) quadraticTo(controlX, prevY, x, y)
} }
} }
} }
@ -1610,21 +1600,14 @@ fun AirConditionerCard(
val pm25 = remember { mutableStateOf(35) } val pm25 = remember { mutableStateOf(35) }
val co2 = remember { mutableStateOf(420) } val co2 = remember { mutableStateOf(420) }
// 外层大卡片 - 使用 Haze 毛玻璃效果 // 外层大卡片 - 使用半透明背景替代实时模糊,降低滚动时 GPU 开销
Box( Box(
modifier = modifier modifier = modifier
.clip(RoundedCornerShape(24.dp)) .clip(RoundedCornerShape(24.dp))
.hazeEffect( .background(Color(0xFF1A1A1A).copy(alpha = 0.55f))
state = hazeState,
style = HazeStyle(
blurRadius = 40.dp,
tint = HazeTint(Color.White.copy(alpha = 0.15f)),
noiseFactor = 0.0f
)
)
.border( .border(
width = 1.dp, width = 1.dp,
color = Color.White.copy(alpha = 0.3f), color = Color.White.copy(alpha = 0.15f),
shape = RoundedCornerShape(24.dp) shape = RoundedCornerShape(24.dp)
) )
.padding(20.dp) .padding(20.dp)
@ -2023,8 +2006,6 @@ fun ModeButton(
onClick: () -> Unit onClick: () -> Unit
) { ) {
val shape = RoundedCornerShape(24.dp) val shape = RoundedCornerShape(24.dp)
val interaction = remember { MutableInteractionSource() }
var pressed by remember { mutableStateOf(false) }
// 根据文本确定图标 // 根据文本确定图标
val iconRes = when (text) { val iconRes = when (text) {
@ -2035,40 +2016,16 @@ fun ModeButton(
else -> R.drawable.ic_dashboard else -> R.drawable.ic_dashboard
} }
val scale by androidx.compose.animation.core.animateFloatAsState(
targetValue = if (pressed) 0.96f else 1f,
animationSpec = androidx.compose.animation.core.spring(
dampingRatio = androidx.compose.animation.core.Spring.DampingRatioMediumBouncy,
stiffness = androidx.compose.animation.core.Spring.StiffnessLow
),
label = "scale"
)
val elevation by androidx.compose.animation.core.animateDpAsState(
targetValue = if (selected) 16.dp else if (pressed) 4.dp else 8.dp,
animationSpec = androidx.compose.animation.core.tween(300),
label = "elevation"
)
val view = LocalView.current val view = LocalView.current
val interaction = remember { MutableInteractionSource() }
LaunchedEffect(interaction) {
interaction.interactions.collect { i ->
when (i) {
is PressInteraction.Press -> pressed = true
is PressInteraction.Release, is PressInteraction.Cancel -> pressed = false
}
}
}
Box( Box(
modifier = modifier modifier = modifier
.scale(scale)
.clip(shape) .clip(shape)
.then( .then(
if (selected) { if (selected) {
Modifier Modifier
.shadow(elevation, shape, clip = false) .shadow(12.dp, shape, clip = false)
.background(Color.White, shape) .background(Color.White, shape)
.border( .border(
width = 1.dp, width = 1.dp,
@ -2312,130 +2269,41 @@ fun LightSlider(
@Composable @Composable
fun WeatherEffectLayer(weather: String, modifier: Modifier = Modifier) { fun WeatherEffectLayer(weather: String, modifier: Modifier = Modifier) {
val infiniteTransition = rememberInfiniteTransition(label = "weather_effect") // 静态天气装饰,避免持续动画导致的帧率下降
// 太阳旋转动画
val sunRotation by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(20000, easing = LinearEasing)
),
label = "sun_rotation"
)
// 雨雪下落动画 - 使用更长的周期避免明显停顿
val dropOffset by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(8000, easing = LinearEasing),
repeatMode = RepeatMode.Restart
),
label = "drop_offset"
)
// 云朵飘动动画
val cloudOffset by infiniteTransition.animateFloat(
initialValue = -20f,
targetValue = 20f,
animationSpec = infiniteRepeatable(
animation = tween(5000, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
),
label = "cloud_offset"
)
androidx.compose.foundation.Canvas(modifier = modifier) { androidx.compose.foundation.Canvas(modifier = modifier) {
val width = size.width val width = size.width
val height = size.height
when { when {
// 晴天:绘制旋转的太阳
weather.contains("") -> { weather.contains("") -> {
// 太阳核心
val sunCenterX = width - 25.dp.toPx() val sunCenterX = width - 25.dp.toPx()
val sunCenterY = 25.dp.toPx() val sunCenterY = 25.dp.toPx()
drawCircle( drawCircle(
color = Color(0xFFFFD700).copy(alpha = 0.6f), color = Color(0xFFFFD700).copy(alpha = 0.5f),
radius = 18.dp.toPx(), radius = 16.dp.toPx(),
center = Offset(sunCenterX, sunCenterY) center = Offset(sunCenterX, sunCenterY)
) )
// 太阳光芒 - 使用数学计算位置
val rayLength = 10.dp.toPx()
val rayStart = 20.dp.toPx()
for (i in 0 until 8) {
val angle = Math.toRadians((sunRotation + i * 45f).toDouble())
val startX = sunCenterX + kotlin.math.cos(angle).toFloat() * rayStart
val startY = sunCenterY + kotlin.math.sin(angle).toFloat() * rayStart
val endX = sunCenterX + kotlin.math.cos(angle).toFloat() * (rayStart + rayLength)
val endY = sunCenterY + kotlin.math.sin(angle).toFloat() * (rayStart + rayLength)
drawLine(
color = Color(0xFFFFD700).copy(alpha = 0.5f),
start = Offset(startX, startY),
end = Offset(endX, endY),
strokeWidth = 2.dp.toPx()
)
}
} }
weather.contains("") || weather.contains("") -> {
// 雨天:绘制下落的雨滴 // 静态雨滴/雪花装饰点
weather.contains("") -> { val count = if (weather.contains("")) 8 else 12
val dropCount = 20 repeat(count) { i ->
val dropLength = 10.dp.toPx()
for (i in 0 until dropCount) {
// 使用不同的起始位置和速度让雨滴错开
val xOffset = (i * 37 % 100) / 100f val xOffset = (i * 37 % 100) / 100f
val x = width * xOffset val x = width * xOffset
val speedFactor = 3f + (i % 5) * 0.5f // 更快的速度,一个周期内多次循环 val y = 12.dp.toPx() + (i % 4) * 10.dp.toPx()
val startOffset = (i * 73 % 100) / 100f // 不同起始位置
val currentY = ((startOffset + dropOffset * speedFactor) % 1f) * height
drawLine(
color = Color.White.copy(alpha = 0.35f),
start = Offset(x, currentY),
end = Offset(x, currentY + dropLength), // 垂直下落
strokeWidth = 1.5f.dp.toPx()
)
}
}
// 雪天:绘制飘落的雪花(圆点)
weather.contains("") -> {
val snowCount = 18
for (i in 0 until snowCount) {
// 使用不同的起始位置和速度让雪花错开
val xOffset = (i * 41 % 100) / 100f
val x = width * xOffset
val speedFactor = 1.5f + (i % 4) * 0.3f // 雪花下落更慢
val startOffset = (i * 67 % 100) / 100f
val currentY = ((startOffset + dropOffset * speedFactor) % 1f) * height
drawCircle( drawCircle(
color = Color.White.copy(alpha = 0.5f), color = Color.White.copy(alpha = 0.25f),
radius = (1.5f + i % 2).dp.toPx(), radius = (if (weather.contains("")) 1.5f else 1f).dp.toPx(),
center = Offset(x, currentY) center = Offset(x, y)
) )
} }
} }
// 多云/阴天:绘制云朵
weather.contains("") || weather.contains("") -> { weather.contains("") || weather.contains("") -> {
// 绘制几个叠加的圆组成云 - 缩小尺寸 val cloudColor = Color.White.copy(alpha = 0.12f)
val cloudColor = Color.White.copy(alpha = 0.15f) val baseX = width - 35.dp.toPx()
val baseX = width - 35.dp.toPx() + cloudOffset * 0.5f
val baseY = 25.dp.toPx() val baseY = 25.dp.toPx()
drawCircle(color = cloudColor, radius = 14.dp.toPx(), center = Offset(baseX, baseY))
drawCircle(color = cloudColor, radius = 15.dp.toPx(), center = Offset(baseX, baseY)) drawCircle(color = cloudColor, radius = 11.dp.toPx(), center = Offset(baseX - 16.dp.toPx(), baseY + 4.dp.toPx()))
drawCircle(color = cloudColor, radius = 12.dp.toPx(), center = Offset(baseX - 18.dp.toPx(), baseY + 5.dp.toPx())) drawCircle(color = cloudColor, radius = 12.dp.toPx(), center = Offset(baseX + 13.dp.toPx(), baseY + 2.dp.toPx()))
drawCircle(color = cloudColor, radius = 14.dp.toPx(), center = Offset(baseX + 15.dp.toPx(), baseY + 3.dp.toPx()))
drawCircle(color = cloudColor, radius = 10.dp.toPx(), center = Offset(baseX - 8.dp.toPx(), baseY - 8.dp.toPx()))
} }
} }
} }
@ -2445,7 +2313,7 @@ fun WeatherEffectLayer(weather: String, modifier: Modifier = Modifier) {
@Composable @Composable
fun BlurGlassCard( fun BlurGlassCard(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
blurRadius: Float = 25f, blurRadius: Float = 16f,
overlayColor: String = "#40FFFFFF", overlayColor: String = "#40FFFFFF",
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
@ -2513,8 +2381,8 @@ fun GlassSidebar(
.hazeEffect( .hazeEffect(
state = hazeState, state = hazeState,
style = HazeStyle( style = HazeStyle(
blurRadius = 40.dp, blurRadius = 16.dp,
tint = HazeTint(Color.White.copy(alpha = 0.15f)), tint = HazeTint(Color.White.copy(alpha = 0.12f)),
noiseFactor = 0.0f noiseFactor = 0.0f
) )
) )
@ -2542,14 +2410,14 @@ fun GlassSidebar(
// 品牌标题 // 品牌标题
Column { Column {
Text( Text(
text = "HEMS", text = "TZ",
color = Color.White, color = Color.White,
fontSize = 32.sp, fontSize = 32.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
letterSpacing = 1.sp letterSpacing = 1.sp
) )
Text( Text(
text = "Smart Home", text = "Energy-saving system",
color = Color.White.copy(alpha = 0.6f), color = Color.White.copy(alpha = 0.6f),
fontSize = 14.sp, fontSize = 14.sp,
letterSpacing = 0.5.sp letterSpacing = 0.5.sp
@ -2665,6 +2533,7 @@ fun GlassSidebar(
} }
} }
} }
}
// 扩展函数用于获取 Activity // 扩展函数用于获取 Activity
fun Context.findActivity(): Activity { fun Context.findActivity(): Activity {
@ -2675,4 +2544,3 @@ fun Context.findActivity(): Activity {
} }
throw IllegalStateException("Permissions should be called in the context of an Activity") throw IllegalStateException("Permissions should be called in the context of an Activity")
} }
}

View File

@ -2,6 +2,6 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/> <background android:drawable="@drawable/ic_launcher_background"/>
<foreground> <foreground>
<inset android:drawable="@drawable/taozhi_icon_v2" android:inset="10%" /> <inset android:drawable="@drawable/taozhi_icon_v3" android:inset="17%" />
</foreground> </foreground>
</adaptive-icon> </adaptive-icon>

View File

@ -2,6 +2,6 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/> <background android:drawable="@drawable/ic_launcher_background"/>
<foreground> <foreground>
<inset android:drawable="@drawable/taozhi_icon_v2" android:inset="10%" /> <inset android:drawable="@drawable/taozhi_icon_v3" android:inset="17%" />
</foreground> </foreground>
</adaptive-icon> </adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 68 KiB