111/app/src/main/java/com/example/smarthome/ui/MainScaffold.kt
2025-11-29 14:08:15 +08:00

3091 lines
124 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package com.example.smarthome.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.GridCells
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.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.foundation.border
import com.example.smarthome.R
import com.example.smarthome.data.WeatherInfo
import com.example.smarthome.data.WeatherService
import com.example.smarthome.data.LanguageManager
import com.example.smarthome.data.tr
import kotlinx.coroutines.launch
import kotlinx.coroutines.delay
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView
import android.view.SoundEffectConstants
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.foundation.layout.offset
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.layout.ContentScale
@Composable
fun MainScaffold(
selectedRoom: Int,
onRoomSelect: (Int) -> Unit,
selectedNavItem: Int = 0,
onNavItemSelect: (Int) -> Unit = {},
rooms: List<String> = listOf("总览", "客厅", "厨房", "卧室", "影音室", "游戏房"),
onAddRoom: (String) -> Unit = {},
onDeleteRoom: (Int) -> Unit = {},
onRenameRoom: (Int, String) -> Unit = { _, _ -> }
) {
Box(modifier = Modifier.fillMaxSize()) {
// 内容区域 - 全屏显示
Box(
modifier = Modifier.fillMaxSize()
) {
// 根据选中的导航项显示不同的内容
// 底部导航: 0-Dashboard, 1-Scene, 2-Automation, 3-Security, 4-Settings
when (selectedNavItem) {
0 -> DashboardContent(selectedRoom, onRoomSelect, rooms, onAddRoom, onDeleteRoom, onRenameRoom)
1 -> SceneScreen()
2 -> AutomationScreen()
3 -> SecurityScreen()
4 -> SettingsContent()
else -> DashboardContent(selectedRoom, onRoomSelect, rooms, onAddRoom, onDeleteRoom, onRenameRoom)
}
}
// 底部悬浮导航栏 - 置于内容之上
FloatingBottomNav(
selectedNavItem = selectedNavItem,
onNavItemSelect = onNavItemSelect,
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 24.dp)
)
}
}
@Composable
fun DashboardContent(
selectedRoom: Int,
onRoomSelect: (Int) -> Unit,
rooms: List<String> = listOf("总览", "客厅", "厨房", "卧室", "影音室", "游戏房"),
onAddRoom: (String) -> Unit = {},
onDeleteRoom: (Int) -> Unit = {},
onRenameRoom: (Int, String) -> Unit = { _, _ -> }
) {
var showAddRoomDialog by remember { mutableStateOf(false) }
// 总览和客厅使用默认壁纸
val roomName = rooms.getOrNull(selectedRoom) ?: "总览"
val hasWallpaper = selectedRoom == 0 || roomName == "客厅"
Box(
modifier = Modifier
.fillMaxSize()
) {
// 房间壁纸背景(总览和客厅)- 放在最底层
if (hasWallpaper) {
Image(
painter = painterResource(id = R.drawable.room_wallpaper_default),
contentDescription = null,
modifier = Modifier
.fillMaxSize()
.clip(RoundedCornerShape(24.dp)),
contentScale = ContentScale.Crop,
alpha = 0.25f
)
}
Column(modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp, vertical = 16.dp)) {
TopBar()
RoomTabs(
selectedRoom = selectedRoom,
onRoomSelect = onRoomSelect,
rooms = rooms,
onAddRoomClick = { showAddRoomDialog = true },
onDeleteRoom = onDeleteRoom,
onRenameRoom = onRenameRoom
)
// 根据选中的房间显示不同的内容
RoomContent(selectedRoom = selectedRoom, roomName = roomName)
}
if (showAddRoomDialog) {
AddRoomDialog(
onDismiss = { showAddRoomDialog = false },
onConfirm = { roomName ->
onAddRoom(roomName)
showAddRoomDialog = false
}
)
}
}
}
@Composable
fun RoomContent(selectedRoom: Int, roomName: String) {
// 根据房间索引显示不同的内容
when (selectedRoom) {
0 -> OverviewRoomContent() // 总览
else -> SpecificRoomContent(selectedRoom, roomName) // 具体房间
}
}
@Composable
fun OverviewRoomContent() {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp)) {
AirConditionerCard(modifier = Modifier.weight(1f), roomName = tr("room_all"))
UsageStatusChart(modifier = Modifier.weight(1f), roomName = tr("room_all"))
}
LightRow(modifier = Modifier.fillMaxWidth().padding(top = 16.dp, bottom = 16.dp), roomName = tr("room_all"))
ModeButtonsRow(onModeSelected = { })
Spacer(modifier = Modifier.height(24.dp))
// 显示所有房间的设备摘要
Text(
text = "所有设备",
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = Color.White,
modifier = Modifier.padding(bottom = 12.dp)
)
AllDevicesOverview(modifier = Modifier.fillMaxWidth())
}
}
@Composable
fun SpecificRoomContent(selectedRoom: Int, roomName: String) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp)) {
AirConditionerCard(modifier = Modifier.weight(1f), roomName = roomName)
UsageStatusChart(modifier = Modifier.weight(1f), roomName = roomName)
}
LightRow(modifier = Modifier.fillMaxWidth().padding(top = 16.dp, bottom = 16.dp), roomName = roomName)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = "$roomName 设备",
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = Color.White,
modifier = Modifier.padding(bottom = 12.dp)
)
MyDevicesGrid(selectedRoom = selectedRoom, modifier = Modifier.fillMaxWidth())
}
}
@Composable
fun AllDevicesOverview(modifier: Modifier = Modifier) {
val allDevices = listOf(
Device("客厅空调", "24°C", R.drawable.ic_ac, true),
Device("客厅灯光", "80%", R.drawable.ic_light, true),
Device("厨房灯光", "60%", R.drawable.ic_light, true),
Device("卧室空调", "22°C", R.drawable.ic_ac, false),
Device("影音室电视", "运行中", R.drawable.ic_media, true),
Device("游戏房主机", "待机中", R.drawable.ic_media, false)
)
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
allDevices.forEach { d -> DeviceCard(d) }
}
}
@Composable
fun HeaderHero() {
TopBar()
}
@Composable
fun StatusChips() {
SideNavRail()
}
@Composable
fun Chip(text: String) {
Box(modifier = Modifier.clip(RoundedCornerShape(20.dp)).background(Color(0x331A1A1A)).padding(horizontal = 12.dp, vertical = 8.dp)) {
Text(text = text, color = Color.White)
}
}
@Composable
fun QuickActions() {
Row(modifier = Modifier.fillMaxWidth().padding(24.dp), horizontalArrangement = Arrangement.spacedBy(12.dp)) {
QuickActionCard(
title = "回家模式",
colors = Brush.linearGradient(listOf(Color(0xFFFFD54F), Color(0xFFFF8F00))),
modifier = Modifier.weight(1f),
onClick = { /* TODO: 触发回家场景 */ }
)
QuickActionCard(
title = "出门模式",
colors = Brush.linearGradient(listOf(Color(0xFFFF8A65), Color(0xFFFF7043))),
modifier = Modifier.weight(1f),
onClick = { /* TODO: 触发出门场景 */ }
)
QuickActionCard(
title = "玩乐模式",
colors = Brush.linearGradient(listOf(Color(0xFF6A3DFF), Color(0xFF3A0CA3))),
modifier = Modifier.weight(1f),
onClick = { /* TODO: 触发玩乐场景 */ }
)
}
}
@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),
contentAlignment = Alignment.BottomStart
) {
Text(text = title, fontWeight = FontWeight.Medium, color = Color.White)
}
}
@Composable
fun RoomTabs(
selectedRoom: Int,
onRoomSelect: (Int) -> Unit,
rooms: List<String> = listOf("总览", "客厅", "厨房", "卧室", "影音室", "游戏房"),
onAddRoomClick: () -> Unit = {},
onDeleteRoom: (Int) -> Unit = {},
onRenameRoom: (Int, String) -> Unit = { _, _ -> }
) {
// 编辑房间对话框状态
var showEditRoomsDialog by remember { mutableStateOf(false) }
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
// 可滚动的房间标签区域
Row(
modifier = Modifier
.weight(1f)
.horizontalScroll(rememberScrollState()),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
rooms.forEachIndexed { index, name ->
RoomTab(
name = name,
selected = index == selectedRoom,
onClick = { onRoomSelect(index) }
)
}
}
// 编辑按钮
Box(
modifier = Modifier
.size(44.dp)
.clip(RoundedCornerShape(12.dp))
.background(Color(0xFF2A2A3E))
.border(
width = 1.dp,
color = Color(0xFF3A3A4E),
shape = RoundedCornerShape(12.dp)
)
.clickable { showEditRoomsDialog = true },
contentAlignment = Alignment.Center
) {
Text(text = "✏️", fontSize = 18.sp)
}
// 添加按钮
GradientButton("+ 添加", onClick = onAddRoomClick)
}
// 编辑房间对话框
if (showEditRoomsDialog) {
EditRoomsDialog(
rooms = rooms,
onDismiss = { showEditRoomsDialog = false },
onRenameRoom = onRenameRoom,
onDeleteRoom = onDeleteRoom
)
}
}
@Composable
fun RoomTab(
name: String,
selected: Boolean,
onClick: () -> Unit
) {
val shape = RoundedCornerShape(20.dp)
Box(
modifier = Modifier
.clip(shape)
.then(
if (selected) {
Modifier.background(brush = Brush.linearGradient(listOf(Color(0xFFA9F0FF), Color(0xFFB89CFF))))
} else {
Modifier
.background(Color(0xFF1E2530).copy(alpha = 0.8f))
.border(
width = 1.dp,
color = Color.White.copy(alpha = 0.15f),
shape = shape
)
}
)
.clickable { onClick() }
.padding(horizontal = 14.dp, vertical = 10.dp)
) {
Text(
text = name,
color = if (selected) Color.Black else Color(0xFFB0B0B0),
maxLines = 1
)
}
}
@Composable
fun DeviceGrid() {
val devices = listOf(
Device("空调", "室内 25℃", R.drawable.ic_ac, true),
Device("智能开关", "待机中", R.drawable.ic_light, false),
Device("吊灯", "6500K", R.drawable.ic_light, true),
Device("扫地机", "清扫中", R.drawable.ic_media, true),
Device("空气净化", "运行中", R.drawable.ic_security, true),
Device("饮水机", "待机中", R.drawable.ic_media, false)
)
LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 140.dp), modifier = Modifier.fillMaxSize().padding(24.dp), horizontalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
items(devices) { d -> DeviceCard(d) }
}
}
data class Device(val name: String, val sub: String, val icon: Int, val on: Boolean)
@Composable
fun DeviceCard(d: Device, roomName: String = "") {
var active by remember { mutableStateOf(d.on) }
val accentColor = if (active) Color(0xFFFFB74D) else Color(0xFF5A5A6E)
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(Color(0xFF1E2530).copy(alpha = 0.85f))
.border(
width = 1.dp,
color = Color.White.copy(alpha = 0.15f),
shape = RoundedCornerShape(16.dp)
)
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
// 左侧图标
Box(
modifier = Modifier
.size(48.dp)
.clip(RoundedCornerShape(12.dp))
.background(accentColor.copy(alpha = if (active) 0.3f else 0.15f)),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(id = d.icon),
contentDescription = null,
modifier = Modifier.size(28.dp),
colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(
if (active) accentColor else Color(0xFF9AA0A6)
)
)
}
// 中间文字
Column(modifier = Modifier.weight(1f)) {
Text(
text = if (roomName.isNotEmpty()) "$roomName ${d.name}" else d.name,
fontWeight = FontWeight.SemiBold,
color = Color.White,
fontSize = 15.sp
)
Text(
text = d.sub,
color = if (active) Color(0xFF00E676) else Color(0xFF9AA0A6),
fontSize = 13.sp
)
}
// 右侧控制 - 根据设备类型显示不同控制方式
val isLight = d.icon == R.drawable.ic_light || d.name.contains("")
val isAC = d.icon == R.drawable.ic_ac || d.name.contains("空调")
val isCurtain = d.icon == R.drawable.ic_curtain || d.name.contains("窗帘")
when {
isLight -> {
// 灯光:亮度调节点
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
val brightnessLevel = d.sub.replace("%", "").toIntOrNull()?.let { it / 20 } ?: 4
repeat(6) { index ->
Box(
modifier = Modifier
.size(10.dp)
.clip(RoundedCornerShape(5.dp))
.background(
if (active && index < brightnessLevel) Color(0xFF00D1FF)
else Color(0xFF00D1FF).copy(alpha = 0.3f)
)
)
}
}
}
isAC -> {
// 空调:显示温度
Text(
text = d.sub,
color = if (active) Color(0xFF00D1FF) else Color(0xFF9AA0A6),
fontSize = 16.sp,
fontWeight = FontWeight.Bold
)
}
isCurtain -> {
// 窗帘:开关状态图标
Text(
text = if (active) "" else "",
color = if (active) Color(0xFF00E676) else Color(0xFF9AA0A6),
fontSize = 18.sp
)
}
else -> {
// 其他设备:开关按钮
Switch(
checked = active,
onCheckedChange = { active = it },
colors = SwitchDefaults.colors(
checkedThumbColor = Color.White,
checkedTrackColor = Color(0xFF00D1FF),
uncheckedThumbColor = Color.Gray,
uncheckedTrackColor = Color(0xFF3A3A4E)
),
modifier = Modifier.scale(0.8f)
)
}
}
}
}
@Composable
fun Toggle(on: Boolean, change: (Boolean) -> Unit) {
Box(modifier = Modifier.size(48.dp, 28.dp).clip(RoundedCornerShape(18.dp)).background(if (on) Color(0xFF3A0CA3) else Color(0x33222222)).clickable { change(!on) }.padding(4.dp)) {
Box(modifier = Modifier.size(20.dp).clip(RoundedCornerShape(10.dp)).background(Color.White).align(if (on) Alignment.CenterEnd else Alignment.CenterStart))
}
}
@Composable
fun BottomNav() {
Row(modifier = Modifier.fillMaxWidth().padding(16.dp).clip(RoundedCornerShape(28.dp)).background(Color(0x50121212)).padding(8.dp), horizontalArrangement = Arrangement.SpaceEvenly) {
NavItem("首页", true)
NavItem("设备", false)
NavItem("我的", false)
}
}
@Composable
fun NavItem(title: String, selected: Boolean) {
Box(modifier = Modifier.clip(RoundedCornerShape(20.dp)).background(if (selected) Color(0x55121212) else Color.Transparent).padding(horizontal = 18.dp, vertical = 10.dp)) {
Text(text = title, color = if (selected) Color.White else Color(0xFFB0B0B0))
}
}
// 底部悬浮导航栏 - 带液态玻璃滑动效果
@Composable
fun FloatingBottomNav(
selectedNavItem: Int = 0,
onNavItemSelect: (Int) -> Unit = {},
modifier: Modifier = Modifier
) {
// 监听语言变化以触发重组
val currentLang by LanguageManager.currentLanguage.collectAsState()
val navItems = listOf(
R.drawable.ic_dashboard,
R.drawable.ic_scene,
R.drawable.ic_automation,
R.drawable.ic_security,
R.drawable.ic_settings
)
val itemWidth = 52.dp
val itemSpacing = 4.dp
val totalItemWidth = itemWidth + itemSpacing
// 是否正在拖动
var isDragging by remember { mutableStateOf(false) }
// 拖动时的临时选中项
var dragIndex by remember { mutableStateOf(selectedNavItem) }
// 当外部 selectedNavItem 变化时,同步更新 dragIndex
LaunchedEffect(selectedNavItem) {
if (!isDragging) {
dragIndex = selectedNavItem
}
}
// 选中框的动画偏移量 - 跟随 dragIndex拖动时或 selectedNavItem点击时
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
),
label = "liquid_glass_offset"
)
val density = LocalDensity.current
// 菜单栏高度(胶囊形)
val navBarHeight = 68.dp
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)
.shadow(24.dp, RoundedCornerShape(50))
.clip(RoundedCornerShape(50))
.background(
Brush.linearGradient(
colors = listOf(
Color(0xFF1A1A2E).copy(alpha = 0.95f),
Color(0xFF16213E).copy(alpha = 0.95f)
)
)
)
.border(
width = 1.dp,
brush = Brush.linearGradient(
colors = listOf(
Color.White.copy(alpha = 0.1f),
Color.White.copy(alpha = 0.05f)
)
),
shape = RoundedCornerShape(50)
)
.padding(horizontal = 10.dp, vertical = 10.dp)
.pointerInput(Unit) {
detectHorizontalDragGestures(
onDragStart = { offset ->
isDragging = true
val index = (offset.x / (itemWidth.toPx() + itemSpacing.toPx())).toInt()
.coerceIn(0, navItems.size - 1)
dragIndex = index
},
onDragEnd = {
isDragging = false
onNavItemSelect(dragIndex)
},
onDragCancel = {
isDragging = false
dragIndex = selectedNavItem
},
onHorizontalDrag = { change, _ ->
change.consume()
val index = (change.position.x / (itemWidth.toPx() + itemSpacing.toPx())).toInt()
.coerceIn(0, navItems.size - 1)
if (index != dragIndex) {
dragIndex = index
}
}
)
}
) {
Box(
contentAlignment = Alignment.CenterStart
) {
// 选中指示器 - 按住时放大贴合菜单栏边缘
val horizontalOffset = (itemWidth.value - indicatorSize) / 2f
Box(
modifier = Modifier
.align(Alignment.CenterStart)
.offset(x = animatedOffset.dp + horizontalOffset.dp)
.size(indicatorSize.dp)
.clip(RoundedCornerShape(50))
.background(Color.White.copy(alpha = if (isDragging) 0.2f else 0.12f))
.border(
width = if (isDragging) 2.dp else 1.5.dp,
color = Color.White.copy(alpha = if (isDragging) 0.6f else 0.25f),
shape = RoundedCornerShape(50)
)
)
// 图标层
Row(
horizontalArrangement = Arrangement.spacedBy(itemSpacing),
verticalAlignment = Alignment.CenterVertically
) {
navItems.forEachIndexed { index, iconRes ->
val isCurrentSelected = if (isDragging) index == dragIndex else index == selectedNavItem
Box(
modifier = Modifier
.size(itemWidth)
.clip(RoundedCornerShape(16.dp))
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {
if (!isDragging) {
onNavItemSelect(index)
}
},
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(id = iconRes),
contentDescription = null,
modifier = Modifier.size(24.dp),
colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(
if (isCurrentSelected) Color.White else Color(0xFFB0B0B0)
)
)
}
}
}
}
}
}
@Composable
fun SideNavRail(selectedNavItem: Int = 0, onNavItemSelect: (Int) -> Unit = {}) {
val context = androidx.compose.ui.platform.LocalContext.current
val selectedBg by com.example.smarthome.data.BackgroundManager.selectedBackground.collectAsState()
val isDarkMode = com.example.smarthome.data.BackgroundManager.backgrounds.getOrNull(selectedBg)?.isDark ?: false
Column(
modifier = Modifier
.width(100.dp)
.fillMaxHeight()
.padding(start = 12.dp, end = 4.dp, top = 12.dp, bottom = 12.dp),
verticalArrangement = Arrangement.SpaceBetween
) {
// 上半部分:导航项
// 监听语言变化以触发重组
val currentLang by LanguageManager.currentLanguage.collectAsState()
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
Spacer(modifier = Modifier.height(48.dp))
NavRailItem(tr("nav_dashboard"), R.drawable.ic_dashboard, selectedNavItem == 0, onClick = { onNavItemSelect(0) })
NavRailItem(tr("nav_scene"), R.drawable.ic_scene, selectedNavItem == 1, onClick = { onNavItemSelect(1) })
NavRailItem(tr("nav_automation"), R.drawable.ic_automation, selectedNavItem == 2, onClick = { onNavItemSelect(2) })
NavRailItem(tr("nav_statistics"), R.drawable.ic_statistics, selectedNavItem == 3, onClick = { onNavItemSelect(3) })
NavRailItem(tr("nav_security"), R.drawable.ic_security, selectedNavItem == 4, onClick = { onNavItemSelect(4) })
NavRailItem(tr("nav_settings"), R.drawable.ic_settings, selectedNavItem == 5, onClick = { onNavItemSelect(5) })
}
// 底部:模式切换按钮(暂时注释)
/*
ThemeModeToggle(
isDarkMode = isDarkMode,
onToggle = {
if (isDarkMode) {
com.example.smarthome.data.BackgroundManager.setBackground(context, 0) // 丝绸白
} else {
com.example.smarthome.data.BackgroundManager.setBackground(context, 2) // 紫金绸
}
}
)
*/
}
}
/*
@Composable
fun ThemeModeToggle(isDarkMode: Boolean, onToggle: () -> Unit) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) { onToggle() }
.padding(vertical = 4.dp)
) {
Box(
modifier = Modifier
.size(48.dp)
.clip(RoundedCornerShape(14.dp))
.background(
if (isDarkMode) {
Brush.linearGradient(
listOf(Color(0xFF2C2C54), Color(0xFF1A1A2E))
)
} else {
Brush.linearGradient(
listOf(Color(0xFFFFE082), Color(0xFFFFD54F))
)
}
),
contentAlignment = Alignment.Center
) {
Text(
text = if (isDarkMode) "🌙" else "☀️",
fontSize = 20.sp
)
}
}
}
*/
@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() }
) { onClick() }
.padding(vertical = 4.dp)
) {
Box(
modifier = Modifier
.size(48.dp)
.shadow(if (selected) 8.dp else 0.dp, RoundedCornerShape(14.dp))
.clip(RoundedCornerShape(14.dp))
.background(
if (selected) {
Brush.linearGradient(
listOf(Color(0xFFA9F0FF), Color(0xFFB89CFF))
)
} else {
Brush.linearGradient(
listOf(Color(0x22FFFFFF), Color(0x22FFFFFF))
)
}
),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(id = iconRes),
contentDescription = text,
modifier = Modifier.size(24.dp),
colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(
if (selected) Color.Black else Color(0xFFB0B0B0)
)
)
}
Text(
text = text,
color = if (selected) Color.White else Color(0xFFB0B0B0),
fontSize = 12.sp,
fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal,
maxLines = 1,
textAlign = TextAlign.Center
)
}
}
@Composable
fun TopBar() {
val context = androidx.compose.ui.platform.LocalContext.current
var weatherInfo by remember { mutableStateOf(WeatherService.getSimulatedWeather()) }
var isLoading by remember { mutableStateOf(true) }
// 获取用户信息
val userInfo by com.example.smarthome.data.UserManager.userInfo.collectAsState()
LaunchedEffect(Unit) {
try {
weatherInfo = WeatherService.getWeather(context)
} catch (e: Exception) {
// 使用模拟数据
}
isLoading = false
}
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
Text(text = tr("dashboard_title"), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, color = Color.White)
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
WeatherCard(weatherInfo = weatherInfo, isLoading = isLoading)
// 用户头像
UserAvatar(
nickname = userInfo.nickname,
avatarUrl = userInfo.avatarUrl,
onClick = { /* 点击头像可以打开用户菜单 */ }
)
}
}
}
@Composable
fun UserAvatar(
nickname: String,
avatarUrl: String,
onClick: () -> Unit
) {
val context = androidx.compose.ui.platform.LocalContext.current
Box(
modifier = Modifier
.size(40.dp)
.shadow(8.dp, androidx.compose.foundation.shape.CircleShape)
.clip(androidx.compose.foundation.shape.CircleShape)
.background(
Brush.linearGradient(
listOf(Color(0xFFA9F0FF), Color(0xFFB89CFF))
)
)
.border(
width = 2.dp,
brush = Brush.linearGradient(
listOf(Color.White.copy(alpha = 0.5f), Color.White.copy(alpha = 0.2f))
),
shape = androidx.compose.foundation.shape.CircleShape
)
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) { onClick() },
contentAlignment = Alignment.Center
) {
if (avatarUrl.isNotEmpty()) {
// 如果有头像URL可以用Coil加载网络图片
// 这里暂时用首字母代替
Text(
text = nickname.firstOrNull()?.uppercase() ?: "U",
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = Color.White
)
} else {
// 显示昵称首字母
Text(
text = nickname.firstOrNull()?.uppercase() ?: "U",
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = Color.White
)
}
}
}
@Composable
fun WeatherCard(weatherInfo: WeatherInfo, isLoading: Boolean) {
// 动画:创建无限循环的偏移量
val infiniteTransition = rememberInfiniteTransition(label = "weather")
val offsetAnim by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 1500f, // 增加偏移量,让流动范围更大
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 15000, easing = LinearEasing), // 加快一点速度
repeatMode = RepeatMode.Reverse
),
label = "gradient"
)
// 根据天气状况决定背景颜色
val colors = if (isLoading) {
listOf(
Color(0xFF2C3E50).copy(alpha = 0.6f),
Color(0xFF3498DB).copy(alpha = 0.6f),
Color(0xFF2C3E50).copy(alpha = 0.6f)
)
} else {
when {
// 晴天:深橙色流动
weatherInfo.weather.contains("") ->
listOf(
Color(0xFF8B3000).copy(alpha = 0.65f),
Color(0xFF9A4500).copy(alpha = 0.65f),
Color(0xFF8B3000).copy(alpha = 0.65f)
)
// 雨天:深紫深蓝流动
weatherInfo.weather.contains("") ->
listOf(
Color(0xFF2C3E50).copy(alpha = 0.6f),
Color(0xFF4CA1AF).copy(alpha = 0.6f),
Color(0xFF2C3E50).copy(alpha = 0.6f)
)
// 阴天/多云:灰蓝流动
weatherInfo.weather.contains("") || weatherInfo.weather.contains("") ->
listOf(
Color(0xFF606c88).copy(alpha = 0.6f),
Color(0xFF3f4c6b).copy(alpha = 0.6f),
Color(0xFF606c88).copy(alpha = 0.6f)
)
// 雪天:冰蓝流动
weatherInfo.weather.contains("") ->
listOf(
Color(0xFF5a7fa4).copy(alpha = 0.6f),
Color(0xFF8ad0d4).copy(alpha = 0.6f),
Color(0xFF5a7fa4).copy(alpha = 0.6f)
)
// 默认:蓝青流动
else ->
listOf(
Color(0xFF1090B0).copy(alpha = 0.6f),
Color(0xFF000046).copy(alpha = 0.6f),
Color(0xFF1090B0).copy(alpha = 0.6f)
)
}
}
val backgroundBrush = Brush.linearGradient(
colors = colors,
start = Offset(offsetAnim, offsetAnim),
end = Offset(offsetAnim + 500f, offsetAnim + 500f)
)
Box(
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.background(backgroundBrush) // 使用动态渐变背景
) {
// 天气特效层(在背景之上,内容之下)- 使用固定大小
WeatherEffectLayer(
weather = weatherInfo.weather,
modifier = Modifier.size(width = 300.dp, height = 60.dp)
)
Row(
modifier = Modifier.padding(horizontal = 20.dp, vertical = 12.dp),
horizontalArrangement = Arrangement.spacedBy(20.dp),
verticalAlignment = Alignment.CenterVertically
) {
// 温度和天气
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = if (isLoading) "--" else "${weatherInfo.temperature}°C",
fontSize = 22.sp,
fontWeight = FontWeight.Bold,
color = Color.White,
style = androidx.compose.ui.text.TextStyle(
shadow = Shadow(color = Color.Black.copy(alpha = 0.3f), blurRadius = 4f)
)
)
Text(
text = if (isLoading) "加载中" else weatherInfo.weather,
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
color = Color.White,
style = androidx.compose.ui.text.TextStyle(
shadow = Shadow(color = Color.Black.copy(alpha = 0.3f), blurRadius = 4f)
)
)
}
// 分隔线
Box(
modifier = Modifier
.width(1.dp)
.height(36.dp)
.background(Color.White.copy(alpha = 0.3f))
)
// 湿度
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = if (isLoading) "--" else "${weatherInfo.humidity}%",
fontSize = 18.sp,
fontWeight = FontWeight.SemiBold,
color = Color.White,
style = androidx.compose.ui.text.TextStyle(
shadow = Shadow(color = Color.Black.copy(alpha = 0.3f), blurRadius = 4f)
)
)
Text(
text = "湿度",
fontSize = 13.sp,
color = Color.White.copy(alpha = 0.8f),
style = androidx.compose.ui.text.TextStyle(
shadow = Shadow(color = Color.Black.copy(alpha = 0.3f), blurRadius = 4f)
)
)
}
// 分隔线
Box(
modifier = Modifier
.width(1.dp)
.height(36.dp)
.background(Color.White.copy(alpha = 0.3f))
)
// 空气质量
Column(horizontalAlignment = Alignment.CenterHorizontally) {
val aqiColor = when {
weatherInfo.aqi <= 50 -> Color(0xFF4CAF50)
weatherInfo.aqi <= 100 -> Color(0xFFFFEB3B)
weatherInfo.aqi <= 150 -> Color(0xFFFF9800)
else -> Color(0xFFFF5252)
}
Text(
text = if (isLoading) "--" else weatherInfo.airQuality,
fontSize = 18.sp,
fontWeight = FontWeight.SemiBold,
color = aqiColor,
style = androidx.compose.ui.text.TextStyle(
shadow = Shadow(color = Color.Black.copy(alpha = 0.3f), blurRadius = 4f)
)
)
Text(
text = if (isLoading) "AQI" else "AQI ${weatherInfo.aqi}",
fontSize = 13.sp,
color = Color.White.copy(alpha = 0.8f),
style = androidx.compose.ui.text.TextStyle(
shadow = Shadow(color = Color.Black.copy(alpha = 0.3f), blurRadius = 4f)
)
)
}
// 城市
Text(
text = weatherInfo.city,
fontSize = 15.sp,
fontWeight = FontWeight.Medium,
color = Color.White,
style = androidx.compose.ui.text.TextStyle(
shadow = Shadow(color = Color.Black.copy(alpha = 0.3f), blurRadius = 4f)
)
)
}
}
}
@Composable
fun GradientButton(text: String, onClick: () -> Unit = {}) {
Box(modifier = Modifier.height(36.dp).clip(RoundedCornerShape(20.dp)).background(Brush.linearGradient(listOf(Color(0xFFA9F0FF), Color(0xFFB89CFF)))).clickable(indication = null, interactionSource = remember { MutableInteractionSource() }) { onClick() }.padding(horizontal = 16.dp), contentAlignment = Alignment.Center) {
Text(text = text, color = Color.Black, fontWeight = FontWeight.Medium)
}
}
@Composable
fun MyDevicesGrid(selectedRoom: Int, modifier: Modifier = Modifier) {
val devices = when (selectedRoom) {
1 -> listOf( // 客厅
Device("智能电视", "运行中", R.drawable.ic_media, true),
Device("音响", "待机中", R.drawable.ic_media, false),
Device("空调", "24°C", R.drawable.ic_ac, true),
Device("主灯", "80%", R.drawable.ic_light, true),
Device("窗帘", "已打开", R.drawable.ic_curtain, true),
Device("插座", "运行中", R.drawable.ic_light, true)
)
2 -> listOf( // 厨房
Device("冰箱", "运行中", R.drawable.ic_ac, true),
Device("微波炉", "待机中", R.drawable.ic_media, false),
Device("油烟机", "待机中", R.drawable.ic_media, false),
Device("灯光", "60%", R.drawable.ic_light, true)
)
3 -> listOf( // 卧室
Device("空调", "22°C", R.drawable.ic_ac, true),
Device("床头灯", "40%", R.drawable.ic_light, true),
Device("窗帘", "已关闭", R.drawable.ic_curtain, false),
Device("加湿器", "运行中", R.drawable.ic_media, true),
Device("空气净化器", "运行中", R.drawable.ic_security, true)
)
4 -> listOf( // 影音室
Device("投影仪", "运行中", R.drawable.ic_media, true),
Device("功放", "运行中", R.drawable.ic_media, true),
Device("音响系统", "运行中", R.drawable.ic_media, true),
Device("灯光", "20%", R.drawable.ic_light, true),
Device("窗帘", "已关闭", R.drawable.ic_curtain, false),
Device("空调", "23°C", R.drawable.ic_ac, true)
)
5 -> listOf( // 游戏房
Device("游戏主机", "运行中", R.drawable.ic_media, true),
Device("显示器", "运行中", R.drawable.ic_media, true),
Device("RGB灯带", "运行中", R.drawable.ic_light, true),
Device("空调", "20°C", R.drawable.ic_ac, true),
Device("路由器", "运行中", R.drawable.ic_security, true)
)
else -> emptyList()
}
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
devices.forEach { d -> DeviceCard(d) }
}
}
// 各房间的固定能耗数据 (用电量kWh, 运行时长h, 设备数, 趋势, 趋势百分比, 费用, 峰值功率W)
private val roomEnergyData = mapOf(
"客厅" to RoomEnergy(3.2, 8, 6, "", 8, 1.8, 320),
"厨房" to RoomEnergy(2.1, 4, 4, "", 12, 1.2, 1800),
"卧室" to RoomEnergy(1.5, 6, 5, "", 5, 0.9, 180),
"影音室" to RoomEnergy(2.8, 5, 6, "", 15, 1.6, 450),
"游戏房" to RoomEnergy(3.5, 7, 5, "", 18, 2.0, 380)
)
private data class RoomEnergy(
val power: Double, // 今日用电量 kWh
val hours: Int, // 今日运行时长 h
val devices: Int, // 活跃设备数
val trend: String, // 趋势方向
val trendPercent: Int, // 趋势百分比
val cost: Double, // 今日费用 元
val peakWatt: Int // 峰值功率 W
)
@Composable
fun UsageStatusChart(modifier: Modifier = Modifier, roomName: String = "房间") {
// 判断是否为总览
val isOverview = roomName == tr("room_all") || roomName == "总览" || roomName == "全部"
// 根据房间获取数据,总览时汇总所有房间
val roomData = if (isOverview) {
// 汇总所有房间数据
val allRooms = roomEnergyData.values.toList()
RoomEnergy(
power = allRooms.sumOf { it.power },
hours = allRooms.maxOf { it.hours }, // 取最长运行时间(今日已过时间)
devices = allRooms.sumOf { it.devices },
trend = if (allRooms.count { it.trend == "" } > allRooms.size / 2) "" else "",
trendPercent = allRooms.map { it.trendPercent }.average().toInt(),
cost = allRooms.sumOf { it.cost },
peakWatt = allRooms.maxOf { it.peakWatt } // 取最大峰值
)
} else {
roomEnergyData[roomName] ?: RoomEnergy(2.0, 5, 4, "", 8, 1.2, 200)
}
val values = remember(roomName) {
if (isOverview) {
// 总览:生成更高的数值
List(12) { (40..90).random().toFloat() }
} else {
List(12) { (15..45).random().toFloat() }
}
}
val maxValue = remember(values) { values.maxOrNull() ?: 45f }
val totalPower = String.format("%.1f", roomData.power)
val totalHours = roomData.hours
val activeDevices = roomData.devices
val trend = roomData.trend
val trendPercent = roomData.trendPercent
val costToday = String.format("%.1f", roomData.cost)
val peakWatt = roomData.peakWatt
// 主题色
val accentColor = Color(0xFF00D1FF)
Box(
modifier = modifier
.height(260.dp)
.clip(RoundedCornerShape(24.dp))
.background(Color(0xFF1E2530).copy(alpha = 0.85f))
.border(
width = 1.dp,
color = Color.White.copy(alpha = 0.15f),
shape = RoundedCornerShape(24.dp)
)
.padding(16.dp)
) {
Column(
verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier.fillMaxSize()
) {
// 标题区域
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
// 图标
Box(
modifier = Modifier
.size(36.dp)
.clip(RoundedCornerShape(10.dp))
.background(accentColor.copy(alpha = 0.2f)),
contentAlignment = Alignment.Center
) {
Text(text = "", fontSize = 18.sp)
}
Column {
Text(
text = "能耗统计",
fontWeight = FontWeight.SemiBold,
color = Color.White,
fontSize = 16.sp
)
Text(
text = roomName,
fontSize = 11.sp,
color = Color(0xFF9AA0A6)
)
}
}
// 趋势指示器
Box(
modifier = Modifier
.clip(RoundedCornerShape(8.dp))
.background(
if (trend == "") Color(0x33FF5252) else Color(0x3300E676)
)
.padding(horizontal = 8.dp, vertical = 4.dp)
) {
Text(
text = "$trend${trendPercent}%",
color = if (trend == "") Color(0xFFFF5252) else Color(0xFF00E676),
fontSize = 12.sp,
fontWeight = FontWeight.Bold
)
}
}
// 主数据显示
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Bottom
) {
// 今日用电
Column {
Row(verticalAlignment = Alignment.Top) {
Text(
text = totalPower,
fontSize = 42.sp,
color = Color.White,
fontWeight = FontWeight.Light
)
Text(
text = "kWh",
fontSize = 14.sp,
color = Color.White.copy(alpha = 0.6f),
modifier = Modifier.padding(top = 10.dp, start = 4.dp)
)
}
Text(
text = "今日用电",
fontSize = 12.sp,
color = Color(0xFF9AA0A6)
)
}
// 预计费用
Column(horizontalAlignment = Alignment.End) {
Text(
text = "¥$costToday",
fontSize = 20.sp,
color = accentColor,
fontWeight = FontWeight.Bold
)
Text(
text = "预计费用",
fontSize = 11.sp,
color = Color(0xFF9AA0A6)
)
}
}
// 详细数据行
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.background(Color(0xFF2A2A3E).copy(alpha = 0.5f))
.padding(12.dp),
horizontalArrangement = Arrangement.SpaceEvenly
) {
// 运行时长
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = "$totalHours",
color = Color(0xFFB89CFF),
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
Text(text = "小时", fontSize = 10.sp, color = Color(0xFF9AA0A6))
}
// 分隔线
Box(
modifier = Modifier
.width(1.dp)
.height(30.dp)
.background(Color(0xFF3A3A4E))
)
// 活跃设备
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = "$activeDevices",
color = Color(0xFF00E676),
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
Text(text = "设备", fontSize = 10.sp, color = Color(0xFF9AA0A6))
}
// 分隔线
Box(
modifier = Modifier
.width(1.dp)
.height(30.dp)
.background(Color(0xFF3A3A4E))
)
// 峰值功率
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = "$peakWatt",
color = Color(0xFFFFB74D),
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
Text(text = "W峰值", fontSize = 10.sp, color = Color(0xFF9AA0A6))
}
}
// 图表区域
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.Bottom
) {
values.forEachIndexed { index, v ->
val normalizedHeight = (v / maxValue) * 50
val isHighlight = index == values.size - 1
Box(
modifier = Modifier
.weight(1f)
.height(normalizedHeight.dp)
.clip(RoundedCornerShape(topStart = 3.dp, topEnd = 3.dp))
.background(
if (isHighlight) {
Brush.verticalGradient(
colors = listOf(accentColor, accentColor.copy(alpha = 0.5f))
)
} else {
Brush.verticalGradient(
colors = listOf(Color(0xFF4A4A5A), Color(0xFF3A3A4A))
)
}
)
)
}
}
}
}
}
}
@Composable
fun AirConditionerCard(modifier: Modifier = Modifier, roomName: String = "房间") {
var temp by remember { mutableStateOf(24f) }
var isOn by remember { mutableStateOf(true) }
var selectedMode by remember { mutableStateOf(0) } // 0:制冷 1:制热 2:除湿 3:送风
var fanSpeed by remember { mutableStateOf(1) } // 0:自动 1:低 2:中 3:高
// 模式emoji图标
val modeIcons = listOf(
"❄️" to tr("ac_mode_cool"),
"☀️" to tr("ac_mode_heat"),
"💧" to tr("ac_mode_dry"),
"🌀" to tr("ac_mode_fan")
)
// 风速旋转速度(根据档位变化)
val fanRotationDuration = when (fanSpeed) {
0 -> 2000 // 自动
1 -> 3000 // 低速
2 -> 1500 // 中速
else -> 800 // 高速
}
// 图标动画
val infiniteTransition = rememberInfiniteTransition(label = "ac_icon")
// 旋转动画(制冷、制热、送风)
val iconRotation by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 3000, easing = LinearEasing),
repeatMode = RepeatMode.Restart
),
label = "icon_rotation"
)
// 水滴下落动画(除湿)
val dropOffset by infiniteTransition.animateFloat(
initialValue = -2f,
targetValue = 2f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 800, easing = androidx.compose.animation.core.EaseInOut),
repeatMode = RepeatMode.Reverse
),
label = "drop_offset"
)
// 风扇旋转动画
val fanRotation by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = fanRotationDuration, easing = LinearEasing),
repeatMode = RepeatMode.Restart
),
label = "fan_rotation"
)
// 风速标签
val fanSpeedLabel = when (fanSpeed) {
0 -> tr("ac_fan_auto")
1 -> tr("ac_fan_low")
2 -> tr("ac_fan_medium")
else -> tr("ac_fan_high")
}
// 根据模式决定主题色
val accentColor = when (selectedMode) {
0 -> Color(0xFF4FC3F7) // 制冷 - 蓝色
1 -> Color(0xFFFF8A65) // 制热 - 橙色
2 -> Color(0xFF81C784) // 除湿 - 绿色
else -> Color(0xFFB0BEC5) // 送风 - 灰色
}
// 当前模式的emoji
val currentEmoji = modeIcons[selectedMode].first
Box(
modifier = modifier
.height(260.dp)
.clip(RoundedCornerShape(24.dp))
.background(Color(0xFF1E2530).copy(alpha = 0.85f))
.border(
width = 1.dp,
color = Color.White.copy(alpha = 0.15f),
shape = RoundedCornerShape(24.dp)
)
.padding(16.dp)
) {
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
// 顶部:标题和开关
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
// 空调图标 - 根据模式应用不同动画
Box(
modifier = Modifier
.size(36.dp)
.clip(RoundedCornerShape(10.dp))
.background(accentColor.copy(alpha = if (isOn) 0.3f else 0.1f)),
contentAlignment = Alignment.Center
) {
// 根据模式选择动画:除湿用上下移动,其他用旋转
val emojiModifier = when {
!isOn -> Modifier
selectedMode == 2 -> Modifier.offset(y = dropOffset.dp) // 除湿:上下移动
else -> Modifier.rotate(iconRotation) // 其他:旋转
}
Text(
text = currentEmoji,
fontSize = 18.sp,
modifier = emojiModifier
)
}
Column {
Text(text = tr("ac_title"), fontWeight = FontWeight.SemiBold, color = Color.White, fontSize = 16.sp)
Text(text = roomName, fontSize = 11.sp, color = Color(0xFF9AA0A6))
}
}
Switch(
checked = isOn,
onCheckedChange = { isOn = it },
colors = SwitchDefaults.colors(
checkedThumbColor = Color.White,
checkedTrackColor = accentColor,
uncheckedThumbColor = Color.Gray,
uncheckedTrackColor = Color(0xFF3A3A4E)
)
)
}
// 温度显示和调节
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
// 大温度显示
Row(verticalAlignment = Alignment.Top) {
Text(
text = "${temp.toInt()}",
fontSize = 52.sp,
color = if (isOn) Color.White else Color.Gray,
fontWeight = FontWeight.Light
)
Text(
text = "°C",
fontSize = 20.sp,
color = if (isOn) Color.White.copy(alpha = 0.7f) else Color.Gray,
modifier = Modifier.padding(top = 8.dp)
)
}
// 风速按钮 + 温度调节按钮
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
// 风速按钮(点击循环切换)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.clip(RoundedCornerShape(12.dp))
.background(if (isOn) accentColor.copy(alpha = 0.2f) else Color(0xFF3A3A4E))
.clickable(enabled = isOn) { fanSpeed = (fanSpeed + 1) % 4 }
.padding(horizontal = 12.dp, vertical = 8.dp)
) {
// 风扇图标(根据风速显示不同图片,带旋转动画)
val fanIconRes = when (fanSpeed) {
1 -> R.drawable.fan_low // 低速 - 3叶片
2 -> R.drawable.fan_medium // 中速 - 4叶片
3 -> R.drawable.fan_high // 高速 - 5叶片
else -> R.drawable.fan_medium // 自动 - 默认4叶片
}
Image(
painter = painterResource(id = fanIconRes),
contentDescription = "Fan Speed",
modifier = Modifier
.size(24.dp)
.then(if (isOn) Modifier.rotate(fanRotation) else Modifier)
)
Text(
text = fanSpeedLabel,
fontSize = 10.sp,
color = if (isOn) Color.White else Color.Gray,
maxLines = 1
)
}
// 温度调节按钮
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
// 升温按钮
Box(
modifier = Modifier
.size(36.dp)
.clip(RoundedCornerShape(10.dp))
.background(if (isOn) accentColor.copy(alpha = 0.3f) else Color(0xFF3A3A4E))
.clickable(enabled = isOn && temp < 32f) { temp += 1f },
contentAlignment = Alignment.Center
) {
Text(text = "", color = if (isOn) Color.White else Color.Gray, fontSize = 14.sp)
}
// 降温按钮
Box(
modifier = Modifier
.size(36.dp)
.clip(RoundedCornerShape(10.dp))
.background(if (isOn) accentColor.copy(alpha = 0.3f) else Color(0xFF3A3A4E))
.clickable(enabled = isOn && temp > 16f) { temp -= 1f },
contentAlignment = Alignment.Center
) {
Text(text = "", color = if (isOn) Color.White else Color.Gray, fontSize = 14.sp)
}
}
}
}
// 模式选择
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
modeIcons.forEachIndexed { index, (emoji, name) ->
val isSelected = selectedMode == index
val modeColor = when (index) {
0 -> Color(0xFF4FC3F7)
1 -> Color(0xFFFF8A65)
2 -> Color(0xFF81C784)
else -> Color(0xFFB0BEC5)
}
Box(
modifier = Modifier
.weight(1f)
.heightIn(min = 44.dp)
.clip(RoundedCornerShape(12.dp))
.background(
if (isSelected && isOn) modeColor.copy(alpha = 0.4f)
else Color(0xFF2A2A3E).copy(alpha = 0.6f)
)
.clickable(enabled = isOn) { selectedMode = index }
.padding(vertical = 4.dp),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = emoji, fontSize = 14.sp)
Text(
text = name,
fontSize = 10.sp,
color = if (isSelected && isOn) Color.White else Color(0xFF9AA0A6),
maxLines = 1
)
}
}
}
}
}
}
}
@OptIn(androidx.compose.material3.ExperimentalMaterial3Api::class)
@Composable
fun CustomSlider(
value: Float,
onValueChange: (Float) -> Unit,
valueRange: ClosedFloatingPointRange<Float>,
enabled: Boolean = true
) {
val interactionSource = remember { MutableInteractionSource() }
Box(
modifier = Modifier
.fillMaxWidth()
.height(32.dp),
contentAlignment = Alignment.Center
) {
Slider(
value = value,
onValueChange = onValueChange,
valueRange = valueRange,
enabled = enabled,
interactionSource = interactionSource,
colors = androidx.compose.material3.SliderDefaults.colors(
thumbColor = Color.White,
activeTrackColor = Brush.linearGradient(
listOf(Color(0xFF00D1FF), Color(0xFFA9F0FF))
).let { Color(0xFF00D1FF) },
inactiveTrackColor = Color(0x55FFFFFF),
disabledThumbColor = Color(0x66FFFFFF),
disabledActiveTrackColor = Color(0x55FFFFFF),
disabledInactiveTrackColor = Color(0x55FFFFFF)
),
modifier = Modifier
.fillMaxWidth()
.height(24.dp),
thumb = {
Box(
modifier = Modifier
.size(20.dp)
.shadow(4.dp, RoundedCornerShape(10.dp))
.clip(RoundedCornerShape(10.dp))
.background(Color.White)
)
},
track = { sliderState ->
val fraction = (sliderState.value - valueRange.start) / (valueRange.endInclusive - valueRange.start)
Box(
modifier = Modifier
.fillMaxWidth()
.height(6.dp)
.clip(RoundedCornerShape(3.dp))
) {
// 未激活轨道
Box(
modifier = Modifier
.fillMaxSize()
.background(if (enabled) Color(0x55FFFFFF) else Color(0x55FFFFFF))
)
// 激活轨道
Box(
modifier = Modifier
.fillMaxWidth(fraction)
.fillMaxHeight()
.background(
if (enabled) {
Brush.linearGradient(
listOf(Color(0xFF00D1FF), Color(0xFFA9F0FF))
)
} else {
Brush.linearGradient(
listOf(Color(0x55FFFFFF), Color(0x55FFFFFF))
)
}
)
)
}
}
)
}
}
@Composable
fun LightList(modifier: Modifier = Modifier) {
val lights = listOf(
"灯光 1" to 0.6f,
"灯光 2" to 0.8f,
"灯光 3" to 0.45f,
"灯光 4" to 0.6f,
"灯光 5" to 0.6f
)
Column(modifier = modifier.fillMaxHeight(), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Text(text = "灯光", fontWeight = FontWeight.SemiBold)
lights.forEach { (name, percent) ->
Row(horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.clip(RoundedCornerShape(16.dp)).background(Color(0x50121212)).padding(12.dp)) {
Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically) {
Box(modifier = Modifier.size(36.dp).clip(RoundedCornerShape(10.dp)).background(Color(0x50121212)))
Column { Text(text = name); Text(text = "${(percent * 100).toInt()}%", color = Color(0xFF9AA0A6)) }
}
DotsProgress(percent)
}
}
}
}
@Composable
fun DotsProgress(percent: Float, onPercentChange: (Float) -> Unit = {}, enabled: Boolean = true) {
val total = 12
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.pointerInput(enabled) {
if (enabled) {
val dotWidth = 8.dp.toPx()
val spacing = 4.dp.toPx()
val totalWidth = (dotWidth + spacing) * total - spacing
// 支持拖动手势
detectHorizontalDragGestures(
onDragStart = { offset ->
// 开始拖动时立即更新
val newPercent = (offset.x / totalWidth).coerceIn(0f, 1f)
onPercentChange(newPercent)
},
onDragEnd = { },
onDragCancel = { },
onHorizontalDrag = { change, _ ->
change.consume()
val newPercent = (change.position.x / totalWidth).coerceIn(0f, 1f)
onPercentChange(newPercent)
}
)
}
}
.pointerInput(enabled) {
if (enabled) {
val dotWidth = 8.dp.toPx()
val spacing = 4.dp.toPx()
val totalWidth = (dotWidth + spacing) * total - spacing
// 也支持点击
detectTapGestures { offset ->
val newPercent = (offset.x / totalWidth).coerceIn(0f, 1f)
onPercentChange(newPercent)
}
}
}
) {
repeat(total) { i ->
val filled = i < (percent * total).toInt()
Box(
modifier = Modifier
.size(8.dp)
.clip(RoundedCornerShape(4.dp))
.background(if (filled) Color(0xFFA9F0FF) else Color(0xFF3A3A3A))
)
}
}
}
enum class Mode { HOME, AWAY, FUN }
@Composable
fun ModeButtonsRow(onModeSelected: (Mode) -> Unit) {
BoxWithConstraints(modifier = Modifier.fillMaxWidth()) {
// 固定高度,确保在滚动前完全显示
val buttonHeight = 100.dp
var selected by remember { mutableStateOf<Mode?>(null) }
val isVertical = maxWidth < 480.dp
val containerModifier = Modifier.fillMaxWidth()
val arrangement = Arrangement.spacedBy(16.dp)
val items = listOf(
Triple(Mode.HOME, Brush.linearGradient(listOf(Color(0xFF00D1FF), Color(0xFF00C9A7))), "回家"),
Triple(Mode.AWAY, Brush.linearGradient(listOf(Color(0xFFFF8A65), Color(0xFFFF7043))), "出门"),
Triple(Mode.FUN, Brush.linearGradient(listOf(Color(0xFF6A3DFF), Color(0xFF3A0CA3))), "玩乐"),
)
if (isVertical) {
Column(modifier = containerModifier, verticalArrangement = arrangement) {
items.forEach { (mode, brush, label) ->
ModeButton(
text = label,
modifier = Modifier.fillMaxWidth(),
height = buttonHeight,
brush = brush,
selected = selected == mode,
onClick = {
selected = mode
onModeSelected(mode)
}
)
}
}
} else {
Row(modifier = containerModifier, horizontalArrangement = arrangement) {
items.forEach { (mode, brush, label) ->
ModeButton(
text = label,
modifier = Modifier.weight(1f),
height = buttonHeight,
brush = brush,
selected = selected == mode,
onClick = {
selected = mode
onModeSelected(mode)
}
)
}
}
}
}
}
@Composable
fun ModeButton(
text: String,
modifier: Modifier,
height: androidx.compose.ui.unit.Dp,
brush: Brush,
selected: Boolean,
onClick: () -> Unit
) {
val shape = RoundedCornerShape(24.dp)
val interaction = remember { MutableInteractionSource() }
var pressed by remember { mutableStateOf(false) }
// 根据文本确定图标
val iconRes = when (text) {
"回家" -> R.drawable.ic_mode_home
"出门" -> R.drawable.ic_mode_away
"玩乐" -> R.drawable.ic_mode_fun
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 iconScale by androidx.compose.animation.core.animateFloatAsState(
targetValue = if (selected) 1.1f else 1f,
animationSpec = androidx.compose.animation.core.spring(
dampingRatio = androidx.compose.animation.core.Spring.DampingRatioMediumBouncy
),
label = "icon_scale"
)
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
}
}
}
Box(
modifier = modifier
.height(height)
.scale(scale)
.clip(shape)
.then(
if (selected) {
Modifier
.shadow(elevation, shape, clip = false)
.background(brush, shape)
.border(
width = 2.dp,
brush = Brush.linearGradient(
listOf(
Color(0x99FFFFFF),
Color(0x66FFFFFF)
)
),
shape = shape
)
} else {
Modifier
.background(Color(0xFF1E2530).copy(alpha = 0.85f))
.border(
width = 1.dp,
color = Color.White.copy(alpha = 0.15f),
shape = shape
)
}
)
.clickable(indication = null, interactionSource = interaction) {
view.playSoundEffect(SoundEffectConstants.CLICK)
onClick()
}
.padding(24.dp),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
// 图标容器
Box(
modifier = Modifier
.size(if (selected) 72.dp else 64.dp)
.scale(iconScale),
contentAlignment = Alignment.Center
) {
// 外层光晕效果(仅选中时)
if (selected) {
Box(
modifier = Modifier
.fillMaxSize()
.clip(RoundedCornerShape(20.dp))
.background(
Brush.radialGradient(
colors = listOf(
Color.White.copy(alpha = 0.2f),
Color.Transparent
),
radius = 100f
)
)
)
}
// 图标背景
Box(
modifier = Modifier
.size(if (selected) 64.dp else 56.dp)
.clip(RoundedCornerShape(18.dp))
.background(
if (selected) {
Brush.linearGradient(
colors = listOf(
Color.White.copy(alpha = 0.25f),
Color.White.copy(alpha = 0.15f)
)
)
} else {
Brush.linearGradient(
colors = listOf(
Color(0x55FFFFFF),
Color(0x55FFFFFF)
)
)
}
)
.then(
if (selected) {
Modifier.border(
width = 1.dp,
brush = Brush.linearGradient(
colors = listOf(
Color.White.copy(alpha = 0.4f),
Color.White.copy(alpha = 0.1f)
)
),
shape = RoundedCornerShape(18.dp)
)
} else {
Modifier
}
),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(id = iconRes),
contentDescription = text,
modifier = Modifier.size(if (selected) 38.dp else 34.dp),
colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(
Color.White
)
)
}
}
// 文字
Text(
text = text,
color = Color.White,
fontWeight = if (selected) FontWeight.Bold else FontWeight.Medium,
fontSize = if (selected) 20.sp else 18.sp,
style = MaterialTheme.typography.titleMedium
)
}
}
}
@Composable
fun LightRow(modifier: Modifier = Modifier, roomName: String = "房间") {
// 根据房间生成不同的灯光数据
val lights = remember(roomName) {
listOf(
"$roomName 主灯" to (0.5f + Math.random().toFloat() * 0.4f),
"$roomName 辅灯" to (0.5f + Math.random().toFloat() * 0.4f)
)
}
Row(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(16.dp)) {
lights.forEach { (name, percent) ->
LightCard(name = name, percent = percent, modifier = Modifier.weight(1f))
}
}
}
@Composable
fun LightCard(name: String, percent: Float, modifier: Modifier = Modifier) {
var brightness by remember { mutableStateOf(percent) }
Box(
modifier = modifier
.height(72.dp)
.clip(RoundedCornerShape(16.dp))
.background(Color(0xFF1E2530).copy(alpha = 0.85f))
.border(
width = 1.dp,
color = Color.White.copy(alpha = 0.15f),
shape = RoundedCornerShape(16.dp)
)
.padding(horizontal = 12.dp, vertical = 8.dp)
) {
Row(horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically) {
// 灯光图标 - 带发光效果
Box(
modifier = Modifier
.size(40.dp)
.shadow(
elevation = if (brightness > 0.3f) (brightness * 12).dp else 0.dp,
shape = RoundedCornerShape(12.dp),
spotColor = Color(0xFFFFD54F).copy(alpha = brightness * 0.6f)
)
.clip(RoundedCornerShape(12.dp))
.background(
if (brightness > 0.1f) {
Brush.radialGradient(
colors = listOf(
Color(0xFFFFE082).copy(alpha = brightness),
Color(0xFFFFD54F).copy(alpha = brightness * 0.9f),
Color(0xFFFF8F00).copy(alpha = brightness * 0.7f)
),
radius = 60f
)
} else {
Brush.linearGradient(
listOf(
Color(0xFF3A3A3A),
Color(0xFF2A2A2A)
)
)
}
),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(id = R.drawable.ic_light),
contentDescription = "灯光",
modifier = Modifier.size(24.dp),
colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(
if (brightness > 0.1f) {
Color.White.copy(alpha = 0.95f)
} else {
Color(0xFF666666)
}
)
)
}
Column {
Text(text = name, color = Color.White, fontWeight = FontWeight.Medium)
Text(
text = "${(brightness * 100).toInt()}%",
color = if (brightness > 0.1f) Color(0xFFFFD54F) else Color(0xFF9AA0A6),
fontSize = 13.sp,
fontWeight = if (brightness > 0.5f) FontWeight.Medium else FontWeight.Normal
)
}
}
DotsProgress(
percent = brightness,
onPercentChange = { brightness = it },
enabled = true
)
}
}
}
@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(),
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) {
// 使用不同的起始位置和速度让雨滴错开
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
drawCircle(
color = Color.White.copy(alpha = 0.5f),
radius = (1.5f + i % 2).dp.toPx(),
center = Offset(x, currentY)
)
}
}
// 多云/阴天:绘制云朵
weather.contains("") || weather.contains("") -> {
// 绘制几个叠加的圆组成云 - 缩小尺寸
val cloudColor = Color.White.copy(alpha = 0.15f)
val baseX = width - 35.dp.toPx() + cloudOffset * 0.5f
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()))
}
}
}
}
@Composable
fun AddRoomDialog(
onDismiss: () -> Unit,
onConfirm: (String) -> Unit
) {
var roomName by remember { mutableStateOf("") }
var selectedIcon by remember { mutableStateOf(0) }
// 预设房间图标
val roomIcons = listOf("🛋️", "🍳", "🛏️", "🎬", "🎮", "📚", "🛁", "🌿")
val roomSuggestions = listOf("书房", "阳台", "浴室", "储物间", "健身房", "儿童房")
// 背景遮罩(点击关闭)
Box(
modifier = Modifier
.fillMaxSize()
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) { onDismiss() },
contentAlignment = Alignment.Center
) {
// 对话框主体
Box(
modifier = Modifier
.width(420.dp)
.clip(RoundedCornerShape(28.dp))
.background(
Brush.verticalGradient(
listOf(
Color(0xFF2A2A3E),
Color(0xFF1A1A2E)
)
)
)
.border(
width = 1.dp,
brush = Brush.linearGradient(
listOf(Color(0xFF00D1FF).copy(alpha = 0.3f), Color(0xFFB89CFF).copy(alpha = 0.3f))
),
shape = RoundedCornerShape(28.dp)
)
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) { /* 阻止点击穿透 */ }
.padding(28.dp)
) {
Column(
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
// 标题区域
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
// 图标
Box(
modifier = Modifier
.size(44.dp)
.clip(RoundedCornerShape(12.dp))
.background(
Brush.linearGradient(
listOf(Color(0xFF00D1FF).copy(alpha = 0.3f), Color(0xFFB89CFF).copy(alpha = 0.3f))
)
),
contentAlignment = Alignment.Center
) {
Text(text = "🏠", fontSize = 22.sp)
}
Column {
Text(
text = "添加新房间",
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
color = Color.White
)
Text(
text = "创建一个新的智能空间",
fontSize = 12.sp,
color = Color(0xFF9AA0A6)
)
}
}
// 关闭按钮
Box(
modifier = Modifier
.size(32.dp)
.clip(RoundedCornerShape(8.dp))
.background(Color(0x33FFFFFF))
.clickable { onDismiss() },
contentAlignment = Alignment.Center
) {
Text(text = "", color = Color(0xFF9AA0A6), fontSize = 14.sp)
}
}
// 房间名称输入
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
text = "房间名称",
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
color = Color(0xFFB0B0B0)
)
androidx.compose.material3.TextField(
value = roomName,
onValueChange = { roomName = it },
placeholder = { Text("例如:书房、阳台...", color = Color(0xFF6A6A7A)) },
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
colors = androidx.compose.material3.TextFieldDefaults.colors(
focusedContainerColor = Color(0xFF1A1A2E),
unfocusedContainerColor = Color(0xFF1A1A2E),
focusedTextColor = Color.White,
unfocusedTextColor = Color.White,
cursorColor = Color(0xFF00D1FF),
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent
),
shape = RoundedCornerShape(14.dp),
singleLine = true,
textStyle = androidx.compose.ui.text.TextStyle(fontSize = 16.sp)
)
}
// 快速选择建议
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
text = "快速选择",
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
color = Color(0xFFB0B0B0)
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
roomSuggestions.take(3).forEach { suggestion ->
Box(
modifier = Modifier
.clip(RoundedCornerShape(20.dp))
.background(
if (roomName == suggestion) Color(0xFF00D1FF).copy(alpha = 0.3f)
else Color(0xFF1A1A2E)
)
.border(
width = 1.dp,
color = if (roomName == suggestion) Color(0xFF00D1FF) else Color(0xFF3A3A4E),
shape = RoundedCornerShape(20.dp)
)
.clickable { roomName = suggestion }
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
Text(
text = suggestion,
color = if (roomName == suggestion) Color.White else Color(0xFF9AA0A6),
fontSize = 13.sp
)
}
}
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
roomSuggestions.drop(3).forEach { suggestion ->
Box(
modifier = Modifier
.clip(RoundedCornerShape(20.dp))
.background(
if (roomName == suggestion) Color(0xFF00D1FF).copy(alpha = 0.3f)
else Color(0xFF1A1A2E)
)
.border(
width = 1.dp,
color = if (roomName == suggestion) Color(0xFF00D1FF) else Color(0xFF3A3A4E),
shape = RoundedCornerShape(20.dp)
)
.clickable { roomName = suggestion }
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
Text(
text = suggestion,
color = if (roomName == suggestion) Color.White else Color(0xFF9AA0A6),
fontSize = 13.sp
)
}
}
}
}
// 选择图标
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
text = "选择图标",
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
color = Color(0xFFB0B0B0)
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
roomIcons.forEachIndexed { index, icon ->
Box(
modifier = Modifier
.size(44.dp)
.clip(RoundedCornerShape(12.dp))
.then(
if (selectedIcon == index)
Modifier.background(Brush.linearGradient(listOf(Color(0xFF00D1FF), Color(0xFFB89CFF))))
else Modifier.background(Color(0xFF1A1A2E))
)
.border(
width = if (selectedIcon == index) 0.dp else 1.dp,
color = Color(0xFF3A3A4E),
shape = RoundedCornerShape(12.dp)
)
.clickable { selectedIcon = index },
contentAlignment = Alignment.Center
) {
Text(text = icon, fontSize = 20.sp)
}
}
}
}
// 按钮区域
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
// 取消按钮
Box(
modifier = Modifier
.weight(1f)
.height(52.dp)
.clip(RoundedCornerShape(14.dp))
.background(Color(0xFF1A1A2E))
.border(
width = 1.dp,
color = Color(0xFF3A3A4E),
shape = RoundedCornerShape(14.dp)
)
.clickable { onDismiss() },
contentAlignment = Alignment.Center
) {
Text(
text = "取消",
color = Color(0xFFB0B0B0),
fontWeight = FontWeight.Medium,
fontSize = 15.sp
)
}
// 确定按钮
Box(
modifier = Modifier
.weight(1f)
.height(52.dp)
.clip(RoundedCornerShape(14.dp))
.background(
if (roomName.isNotBlank())
Brush.linearGradient(listOf(Color(0xFF00D1FF), Color(0xFFB89CFF)))
else
Brush.linearGradient(listOf(Color(0xFF3A3A4E), Color(0xFF3A3A4E)))
)
.clickable(enabled = roomName.isNotBlank()) {
if (roomName.isNotBlank()) {
onConfirm(roomName.trim())
}
},
contentAlignment = Alignment.Center
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = "",
color = if (roomName.isNotBlank()) Color.White else Color(0xFF6A6A7A),
fontSize = 16.sp
)
Text(
text = "添加房间",
color = if (roomName.isNotBlank()) Color.White else Color(0xFF6A6A7A),
fontWeight = FontWeight.Bold,
fontSize = 15.sp
)
}
}
}
}
}
}
}
@Composable
fun RenameRoomDialog(
currentName: String,
onDismiss: () -> Unit,
onConfirm: (String) -> Unit
) {
var roomName by remember { mutableStateOf(currentName) }
// 背景遮罩(点击关闭)
Box(
modifier = Modifier
.fillMaxSize()
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) { onDismiss() },
contentAlignment = Alignment.Center
) {
// 对话框主体
Box(
modifier = Modifier
.width(380.dp)
.clip(RoundedCornerShape(28.dp))
.background(
Brush.verticalGradient(
listOf(
Color(0xFF2A2A3E),
Color(0xFF1A1A2E)
)
)
)
.border(
width = 1.dp,
brush = Brush.linearGradient(
listOf(Color(0xFF00D1FF).copy(alpha = 0.3f), Color(0xFFB89CFF).copy(alpha = 0.3f))
),
shape = RoundedCornerShape(28.dp)
)
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) { /* 阻止点击穿透 */ }
.padding(28.dp)
) {
Column(
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
// 标题区域
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Box(
modifier = Modifier
.size(44.dp)
.clip(RoundedCornerShape(12.dp))
.background(
Brush.linearGradient(
listOf(Color(0xFF00D1FF).copy(alpha = 0.3f), Color(0xFFB89CFF).copy(alpha = 0.3f))
)
),
contentAlignment = Alignment.Center
) {
Text(text = "✏️", fontSize = 22.sp)
}
Column {
Text(
text = "重命名房间",
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
color = Color.White
)
Text(
text = "修改房间名称",
fontSize = 12.sp,
color = Color(0xFF9AA0A6)
)
}
}
Box(
modifier = Modifier
.size(32.dp)
.clip(RoundedCornerShape(8.dp))
.background(Color(0x33FFFFFF))
.clickable { onDismiss() },
contentAlignment = Alignment.Center
) {
Text(text = "", color = Color(0xFF9AA0A6), fontSize = 14.sp)
}
}
// 房间名称输入
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
text = "房间名称",
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
color = Color(0xFFB0B0B0)
)
TextField(
value = roomName,
onValueChange = { roomName = it },
placeholder = { Text("输入新名称...", color = Color(0xFF6A6A7A)) },
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
colors = TextFieldDefaults.colors(
focusedContainerColor = Color(0xFF1A1A2E),
unfocusedContainerColor = Color(0xFF1A1A2E),
focusedTextColor = Color.White,
unfocusedTextColor = Color.White,
cursorColor = Color(0xFF00D1FF),
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent
),
shape = RoundedCornerShape(14.dp),
singleLine = true,
textStyle = androidx.compose.ui.text.TextStyle(fontSize = 16.sp)
)
}
// 按钮区域
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Box(
modifier = Modifier
.weight(1f)
.height(52.dp)
.clip(RoundedCornerShape(14.dp))
.background(Color(0xFF1A1A2E))
.border(
width = 1.dp,
color = Color(0xFF3A3A4E),
shape = RoundedCornerShape(14.dp)
)
.clickable { onDismiss() },
contentAlignment = Alignment.Center
) {
Text(
text = "取消",
color = Color(0xFFB0B0B0),
fontWeight = FontWeight.Medium,
fontSize = 15.sp
)
}
val canConfirm = roomName.isNotBlank() && roomName.trim() != currentName
Box(
modifier = Modifier
.weight(1f)
.height(52.dp)
.clip(RoundedCornerShape(14.dp))
.background(
if (canConfirm)
Brush.linearGradient(listOf(Color(0xFF00D1FF), Color(0xFFB89CFF)))
else
Brush.linearGradient(listOf(Color(0xFF3A3A4E), Color(0xFF3A3A4E)))
)
.clickable(enabled = canConfirm) {
onConfirm(roomName.trim())
},
contentAlignment = Alignment.Center
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = "",
color = if (canConfirm) Color.White else Color(0xFF6A6A7A),
fontSize = 16.sp
)
Text(
text = "确认修改",
color = if (canConfirm) Color.White else Color(0xFF6A6A7A),
fontWeight = FontWeight.Bold,
fontSize = 15.sp
)
}
}
}
}
}
}
}
@Composable
fun EditRoomsDialog(
rooms: List<String>,
onDismiss: () -> Unit,
onRenameRoom: (Int, String) -> Unit,
onDeleteRoom: (Int) -> Unit
) {
// 当前编辑的房间索引
var editingIndex by remember { mutableStateOf(-1) }
var editingName by remember { mutableStateOf("") }
// 背景遮罩
Box(
modifier = Modifier
.fillMaxSize()
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) { onDismiss() },
contentAlignment = Alignment.Center
) {
// 对话框主体
Box(
modifier = Modifier
.width(420.dp)
.clip(RoundedCornerShape(28.dp))
.background(
Brush.verticalGradient(
listOf(
Color(0xFF2A2A3E),
Color(0xFF1A1A2E)
)
)
)
.border(
width = 1.dp,
brush = Brush.linearGradient(
listOf(Color(0xFF00D1FF).copy(alpha = 0.3f), Color(0xFFB89CFF).copy(alpha = 0.3f))
),
shape = RoundedCornerShape(28.dp)
)
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) { /* 阻止点击穿透 */ }
.padding(28.dp)
) {
Column(
verticalArrangement = Arrangement.spacedBy(20.dp)
) {
// 标题区域
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Box(
modifier = Modifier
.size(44.dp)
.clip(RoundedCornerShape(12.dp))
.background(
Brush.linearGradient(
listOf(Color(0xFF00D1FF).copy(alpha = 0.3f), Color(0xFFB89CFF).copy(alpha = 0.3f))
)
),
contentAlignment = Alignment.Center
) {
Text(text = "🏠", fontSize = 22.sp)
}
Column {
Text(
text = "编辑房间",
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
color = Color.White
)
Text(
text = "管理你的房间列表",
fontSize = 12.sp,
color = Color(0xFF9AA0A6)
)
}
}
Box(
modifier = Modifier
.size(32.dp)
.clip(RoundedCornerShape(8.dp))
.background(Color(0x33FFFFFF))
.clickable { onDismiss() },
contentAlignment = Alignment.Center
) {
Text(text = "", color = Color(0xFF9AA0A6), fontSize = 14.sp)
}
}
// 房间列表
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.heightIn(max = 300.dp).verticalScroll(rememberScrollState())
) {
rooms.forEachIndexed { index, name ->
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(14.dp))
.background(Color(0xFF1A1A2E))
.padding(12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
if (editingIndex == index) {
// 编辑模式
TextField(
value = editingName,
onValueChange = { editingName = it },
modifier = Modifier
.weight(1f)
.height(48.dp),
colors = TextFieldDefaults.colors(
focusedContainerColor = Color(0xFF2A2A3E),
unfocusedContainerColor = Color(0xFF2A2A3E),
focusedTextColor = Color.White,
unfocusedTextColor = Color.White,
cursorColor = Color(0xFF00D1FF),
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent
),
shape = RoundedCornerShape(10.dp),
singleLine = true,
textStyle = androidx.compose.ui.text.TextStyle(fontSize = 15.sp)
)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
// 确认按钮
Box(
modifier = Modifier
.size(36.dp)
.clip(RoundedCornerShape(8.dp))
.background(Color(0xFF4CAF50))
.clickable {
if (editingName.isNotBlank() && editingName != name) {
onRenameRoom(index, editingName.trim())
}
editingIndex = -1
},
contentAlignment = Alignment.Center
) {
Text(text = "", color = Color.White, fontSize = 16.sp)
}
// 取消按钮
Box(
modifier = Modifier
.size(36.dp)
.clip(RoundedCornerShape(8.dp))
.background(Color(0xFF3A3A4E))
.clickable { editingIndex = -1 },
contentAlignment = Alignment.Center
) {
Text(text = "", color = Color(0xFFB0B0B0), fontSize = 16.sp)
}
}
} else {
// 显示模式
Text(
text = name,
color = Color.White,
fontSize = 15.sp,
modifier = Modifier.weight(1f)
)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
// 编辑按钮
Box(
modifier = Modifier
.size(36.dp)
.clip(RoundedCornerShape(8.dp))
.background(Color(0xFF3A3A4E))
.clickable {
editingIndex = index
editingName = name
},
contentAlignment = Alignment.Center
) {
Text(text = "✏️", fontSize = 16.sp)
}
// 删除按钮
if (rooms.size > 1) {
Box(
modifier = Modifier
.size(36.dp)
.clip(RoundedCornerShape(8.dp))
.background(Color(0xFFFF5252).copy(alpha = 0.2f))
.clickable { onDeleteRoom(index) },
contentAlignment = Alignment.Center
) {
Text(text = "🗑️", fontSize = 16.sp)
}
}
}
}
}
}
}
// 完成按钮
Box(
modifier = Modifier
.fillMaxWidth()
.height(52.dp)
.clip(RoundedCornerShape(14.dp))
.background(
Brush.linearGradient(listOf(Color(0xFF00D1FF), Color(0xFFB89CFF)))
)
.clickable { onDismiss() },
contentAlignment = Alignment.Center
) {
Text(
text = "完成",
color = Color.White,
fontWeight = FontWeight.Bold,
fontSize = 15.sp
)
}
}
}
}
}