图标和界面优化
@ -1,6 +1,5 @@
|
||||
package com.example.smarthome
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import com.example.smarthome.ui.theme.SmartHomeTheme
|
||||
@ -54,35 +53,22 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
}
|
||||
private fun hideStatusBar() {
|
||||
// 设置全屏模式,让内容延伸到系统栏区域
|
||||
// 让内容延伸到系统栏区域
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
// 设置全屏标志
|
||||
window.setFlags(
|
||||
android.view.WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
||||
android.view.WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||
)
|
||||
|
||||
// 设置状态栏和导航栏为透明
|
||||
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
|
||||
)
|
||||
// 使用现代 WindowInsetsController API 隐藏状态栏和导航栏
|
||||
androidx.core.view.WindowInsetsControllerCompat(window, window.decorView).apply {
|
||||
hide(androidx.core.view.WindowInsetsCompat.Type.statusBars())
|
||||
hide(androidx.core.view.WindowInsetsCompat.Type.navigationBars())
|
||||
systemBarsBehavior = androidx.core.view.WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
}
|
||||
|
||||
// 支持刘海屏设备
|
||||
val lp = window.attributes
|
||||
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
|
||||
}
|
||||
|
||||
// 初始化背景管理器并获取当前选中的背景
|
||||
androidx.compose.runtime.LaunchedEffect(Unit) {
|
||||
BackgroundManager.init(context)
|
||||
}
|
||||
// 初始化背景管理器并获取当前选中的背景(同步初始化,避免首帧闪烁默认背景)
|
||||
remember { BackgroundManager.init(context) }
|
||||
val selectedBgId by BackgroundManager.selectedBackground.collectAsState()
|
||||
val currentBg = BackgroundManager.backgrounds.getOrNull(selectedBgId)
|
||||
val backgroundRes = currentBg?.resourceId ?: R.drawable.background1
|
||||
|
||||
@ -10,7 +10,6 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.PressInteraction
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
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.scale
|
||||
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.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shadow
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
@ -71,8 +67,6 @@ import android.content.ContextWrapper
|
||||
import dev.chrisbanes.haze.HazeState
|
||||
import dev.chrisbanes.haze.HazeStyle
|
||||
import dev.chrisbanes.haze.HazeTint
|
||||
import dev.chrisbanes.haze.haze
|
||||
import dev.chrisbanes.haze.hazeChild
|
||||
import dev.chrisbanes.haze.hazeSource
|
||||
import dev.chrisbanes.haze.hazeEffect
|
||||
|
||||
@ -93,6 +87,12 @@ fun MainScaffold(
|
||||
// Haze state for blur effects
|
||||
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(
|
||||
@ -102,7 +102,7 @@ fun MainScaffold(
|
||||
) {
|
||||
// 背景图片
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.background),
|
||||
painter = painterResource(id = backgroundRes),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop,
|
||||
@ -181,7 +181,7 @@ fun MainScaffold(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ -349,11 +349,97 @@ fun AllDevicesOverview(modifier: Modifier = Modifier) {
|
||||
Device("影音室电视", "运行中", R.drawable.ic_media, true),
|
||||
Device("游戏房主机", "待机中", R.drawable.ic_media, false)
|
||||
)
|
||||
val rows = allDevices.chunked(2)
|
||||
Column(
|
||||
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
|
||||
fun QuickActionCard(title: String, colors: Brush, modifier: Modifier = Modifier, onClick: () -> Unit) {
|
||||
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(
|
||||
modifier = modifier
|
||||
.height(88.dp)
|
||||
.shadow(12.dp, shape, false)
|
||||
.clip(shape)
|
||||
.background(colors)
|
||||
.scale(scale)
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
pressed = !pressed
|
||||
onClick()
|
||||
}
|
||||
.padding(16.dp),
|
||||
@ -714,23 +792,6 @@ fun FloatingBottomNav(
|
||||
// 拖动时的临时选中项
|
||||
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
|
||||
LaunchedEffect(selectedNavItem) {
|
||||
if (!isDragging) {
|
||||
@ -738,15 +799,15 @@ fun FloatingBottomNav(
|
||||
}
|
||||
}
|
||||
|
||||
// 选中框的动画偏移量 - 跟随 dragIndex(拖动时)或 selectedNavItem(点击时)
|
||||
// 选中指示器偏移 - 拖动时 snap 跟随手指,松手后快速回弹
|
||||
val currentIndex = if (isDragging) dragIndex else selectedNavItem
|
||||
val animatedOffset by animateFloatAsState(
|
||||
targetValue = currentIndex * (itemWidth.value + itemSpacing.value),
|
||||
animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||
stiffness = Spring.StiffnessHigh
|
||||
animationSpec = if (isDragging) snap() else tween(
|
||||
durationMillis = 180,
|
||||
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 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(
|
||||
modifier = modifier
|
||||
.height(navBarHeight)
|
||||
.alpha(navBarAlpha)
|
||||
.shadow(24.dp, RoundedCornerShape(50))
|
||||
.clip(RoundedCornerShape(50))
|
||||
.background(
|
||||
@ -792,9 +842,6 @@ fun FloatingBottomNav(
|
||||
.pointerInput(Unit) {
|
||||
detectHorizontalDragGestures(
|
||||
onDragStart = { offset ->
|
||||
// 重置空闲状态
|
||||
isIdle = false
|
||||
lastInteractionTime = System.currentTimeMillis()
|
||||
isDragging = true
|
||||
val index = (offset.x / (itemWidth.toPx() + itemSpacing.toPx())).toInt()
|
||||
.coerceIn(0, navItems.size - 1)
|
||||
@ -802,12 +849,10 @@ fun FloatingBottomNav(
|
||||
},
|
||||
onDragEnd = {
|
||||
isDragging = false
|
||||
lastInteractionTime = System.currentTimeMillis()
|
||||
onNavItemSelect(dragIndex)
|
||||
},
|
||||
onDragCancel = {
|
||||
isDragging = false
|
||||
lastInteractionTime = System.currentTimeMillis()
|
||||
dragIndex = selectedNavItem
|
||||
},
|
||||
onHorizontalDrag = { change, _ ->
|
||||
@ -824,7 +869,8 @@ fun FloatingBottomNav(
|
||||
Box(
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
// 选中指示器 - 按住时放大贴合菜单栏边缘
|
||||
// 选中指示器
|
||||
val indicatorSize = innerHeight.value
|
||||
val horizontalOffset = (itemWidth.value - indicatorSize) / 2f
|
||||
|
||||
Box(
|
||||
@ -861,12 +907,7 @@ fun FloatingBottomNav(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
if (!isDragging) {
|
||||
// 重置空闲状态
|
||||
isIdle = false
|
||||
lastInteractionTime = System.currentTimeMillis()
|
||||
onNavItemSelect(index)
|
||||
}
|
||||
onNavItemSelect(index)
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
@ -962,18 +1003,11 @@ fun ThemeModeToggle(isDarkMode: Boolean, onToggle: () -> Unit) {
|
||||
|
||||
@Composable
|
||||
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(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
modifier = Modifier
|
||||
.width(80.dp) // 固定宽度,防止文字长度不一致导致图标不对齐
|
||||
.scale(scale)
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
@ -1182,20 +1216,20 @@ fun WeatherCard(weatherInfo: WeatherInfo, isLoading: Boolean) {
|
||||
color = accentColor.copy(alpha = 0.2f),
|
||||
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),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// 天气图标(带阴影的渐变背景)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.size(30.dp)
|
||||
.shadow(
|
||||
elevation = 8.dp,
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
elevation = 4.dp,
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
spotColor = weatherColors.second.copy(alpha = 0.3f)
|
||||
)
|
||||
.clip(RoundedCornerShape(14.dp))
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(
|
||||
Brush.linearGradient(
|
||||
colors = listOf(weatherColors.first, weatherColors.second),
|
||||
@ -1215,74 +1249,37 @@ fun WeatherCard(weatherInfo: WeatherInfo, isLoading: Boolean) {
|
||||
else -> R.drawable.ic_weather_cloudy
|
||||
}),
|
||||
contentDescription = "天气",
|
||||
modifier = Modifier.size(24.dp),
|
||||
modifier = Modifier.size(18.dp),
|
||||
colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(Color.White)
|
||||
)
|
||||
}
|
||||
|
||||
// 温度和天气状况
|
||||
Column {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
Text(
|
||||
text = if (isLoading) "--°C" else "${weatherInfo.temperature}°C",
|
||||
fontSize = 22.sp,
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
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(
|
||||
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 = if (isLoading) "--%" else "${weatherInfo.humidity}%",
|
||||
text = if (isLoading) "加载中" else weatherInfo.weather,
|
||||
fontSize = 11.sp,
|
||||
color = Color(0xFFD1D5DB)
|
||||
)
|
||||
}
|
||||
|
||||
// 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)
|
||||
color = accentColor.copy(alpha = 0.9f)
|
||||
)
|
||||
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(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(24.dp))
|
||||
.hazeEffect(
|
||||
state = hazeState,
|
||||
style = HazeStyle(
|
||||
blurRadius = 40.dp,
|
||||
tint = HazeTint(Color.White.copy(alpha = 0.15f)),
|
||||
noiseFactor = 0.0f
|
||||
)
|
||||
)
|
||||
.background(Color(0xFF1A1A1A).copy(alpha = 0.55f))
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = Color.White.copy(alpha = 0.3f),
|
||||
color = Color.White.copy(alpha = 0.15f),
|
||||
shape = RoundedCornerShape(24.dp)
|
||||
)
|
||||
.padding(20.dp)
|
||||
@ -1535,7 +1525,7 @@ fun EnergyTrendChart() {
|
||||
val prevX = (index - 1) * stepX
|
||||
val prevY = canvasHeight - (dataPoints[index - 1] * canvasHeight)
|
||||
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 prevY = canvasHeight - (dataPoints[index - 1] * canvasHeight)
|
||||
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 co2 = remember { mutableStateOf(420) }
|
||||
|
||||
// 外层大卡片 - 使用 Haze 毛玻璃效果
|
||||
// 外层大卡片 - 使用半透明背景替代实时模糊,降低滚动时 GPU 开销
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(24.dp))
|
||||
.hazeEffect(
|
||||
state = hazeState,
|
||||
style = HazeStyle(
|
||||
blurRadius = 40.dp,
|
||||
tint = HazeTint(Color.White.copy(alpha = 0.15f)),
|
||||
noiseFactor = 0.0f
|
||||
)
|
||||
)
|
||||
.background(Color(0xFF1A1A1A).copy(alpha = 0.55f))
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = Color.White.copy(alpha = 0.3f),
|
||||
color = Color.White.copy(alpha = 0.15f),
|
||||
shape = RoundedCornerShape(24.dp)
|
||||
)
|
||||
.padding(20.dp)
|
||||
@ -2023,8 +2006,6 @@ fun ModeButton(
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val shape = RoundedCornerShape(24.dp)
|
||||
val interaction = remember { MutableInteractionSource() }
|
||||
var pressed by remember { mutableStateOf(false) }
|
||||
|
||||
// 根据文本确定图标
|
||||
val iconRes = when (text) {
|
||||
@ -2035,40 +2016,16 @@ fun ModeButton(
|
||||
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
|
||||
|
||||
LaunchedEffect(interaction) {
|
||||
interaction.interactions.collect { i ->
|
||||
when (i) {
|
||||
is PressInteraction.Press -> pressed = true
|
||||
is PressInteraction.Release, is PressInteraction.Cancel -> pressed = false
|
||||
}
|
||||
}
|
||||
}
|
||||
val interaction = remember { MutableInteractionSource() }
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.scale(scale)
|
||||
.clip(shape)
|
||||
.then(
|
||||
if (selected) {
|
||||
Modifier
|
||||
.shadow(elevation, shape, clip = false)
|
||||
.shadow(12.dp, shape, clip = false)
|
||||
.background(Color.White, shape)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
@ -2312,130 +2269,41 @@ fun LightSlider(
|
||||
|
||||
@Composable
|
||||
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) {
|
||||
val width = size.width
|
||||
val height = size.height
|
||||
|
||||
when {
|
||||
// 晴天:绘制旋转的太阳
|
||||
weather.contains("晴") -> {
|
||||
// 太阳核心
|
||||
val sunCenterX = width - 25.dp.toPx()
|
||||
val sunCenterY = 25.dp.toPx()
|
||||
|
||||
drawCircle(
|
||||
color = Color(0xFFFFD700).copy(alpha = 0.6f),
|
||||
radius = 18.dp.toPx(),
|
||||
color = Color(0xFFFFD700).copy(alpha = 0.5f),
|
||||
radius = 16.dp.toPx(),
|
||||
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("雨") -> {
|
||||
val dropCount = 20
|
||||
val dropLength = 10.dp.toPx()
|
||||
|
||||
for (i in 0 until dropCount) {
|
||||
// 使用不同的起始位置和速度让雨滴错开
|
||||
weather.contains("雨") || weather.contains("雪") -> {
|
||||
// 静态雨滴/雪花装饰点
|
||||
val count = if (weather.contains("雪")) 8 else 12
|
||||
repeat(count) { i ->
|
||||
val xOffset = (i * 37 % 100) / 100f
|
||||
val x = width * xOffset
|
||||
val speedFactor = 3f + (i % 5) * 0.5f // 更快的速度,一个周期内多次循环
|
||||
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
|
||||
|
||||
val y = 12.dp.toPx() + (i % 4) * 10.dp.toPx()
|
||||
drawCircle(
|
||||
color = Color.White.copy(alpha = 0.5f),
|
||||
radius = (1.5f + i % 2).dp.toPx(),
|
||||
center = Offset(x, currentY)
|
||||
color = Color.White.copy(alpha = 0.25f),
|
||||
radius = (if (weather.contains("雪")) 1.5f else 1f).dp.toPx(),
|
||||
center = Offset(x, y)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 多云/阴天:绘制云朵
|
||||
weather.contains("云") || weather.contains("阴") -> {
|
||||
// 绘制几个叠加的圆组成云 - 缩小尺寸
|
||||
val cloudColor = Color.White.copy(alpha = 0.15f)
|
||||
val baseX = width - 35.dp.toPx() + cloudOffset * 0.5f
|
||||
val cloudColor = Color.White.copy(alpha = 0.12f)
|
||||
val baseX = width - 35.dp.toPx()
|
||||
val baseY = 25.dp.toPx()
|
||||
|
||||
drawCircle(color = cloudColor, radius = 15.dp.toPx(), center = Offset(baseX, baseY))
|
||||
drawCircle(color = cloudColor, radius = 12.dp.toPx(), center = Offset(baseX - 18.dp.toPx(), baseY + 5.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()))
|
||||
drawCircle(color = cloudColor, radius = 14.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 + 13.dp.toPx(), baseY + 2.dp.toPx()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2445,7 +2313,7 @@ fun WeatherEffectLayer(weather: String, modifier: Modifier = Modifier) {
|
||||
@Composable
|
||||
fun BlurGlassCard(
|
||||
modifier: Modifier = Modifier,
|
||||
blurRadius: Float = 25f,
|
||||
blurRadius: Float = 16f,
|
||||
overlayColor: String = "#40FFFFFF",
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
@ -2513,8 +2381,8 @@ fun GlassSidebar(
|
||||
.hazeEffect(
|
||||
state = hazeState,
|
||||
style = HazeStyle(
|
||||
blurRadius = 40.dp,
|
||||
tint = HazeTint(Color.White.copy(alpha = 0.15f)),
|
||||
blurRadius = 16.dp,
|
||||
tint = HazeTint(Color.White.copy(alpha = 0.12f)),
|
||||
noiseFactor = 0.0f
|
||||
)
|
||||
)
|
||||
@ -2542,14 +2410,14 @@ fun GlassSidebar(
|
||||
// 品牌标题
|
||||
Column {
|
||||
Text(
|
||||
text = "HEMS",
|
||||
text = "TZ",
|
||||
color = Color.White,
|
||||
fontSize = 32.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
letterSpacing = 1.sp
|
||||
)
|
||||
Text(
|
||||
text = "Smart Home",
|
||||
text = "Energy-saving system",
|
||||
color = Color.White.copy(alpha = 0.6f),
|
||||
fontSize = 14.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
@ -2665,6 +2533,7 @@ fun GlassSidebar(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 扩展函数用于获取 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")
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,6 @@
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground>
|
||||
<inset android:drawable="@drawable/taozhi_icon_v2" android:inset="10%" />
|
||||
<inset android:drawable="@drawable/taozhi_icon_v3" android:inset="17%" />
|
||||
</foreground>
|
||||
</adaptive-icon>
|
||||
|
||||
@ -2,6 +2,6 @@
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground>
|
||||
<inset android:drawable="@drawable/taozhi_icon_v2" android:inset="10%" />
|
||||
<inset android:drawable="@drawable/taozhi_icon_v3" android:inset="17%" />
|
||||
</foreground>
|
||||
</adaptive-icon>
|
||||
|
||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 68 KiB |