From ae9723964eadf8e4a1c8aca3e8fcba2bb429dfa2 Mon Sep 17 00:00:00 2001 From: zzh Date: Thu, 27 Nov 2025 16:00:44 +0800 Subject: [PATCH] =?UTF-8?q?=20=E4=BC=98=E5=8C=96=E7=BB=86=E8=8A=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/smarthome/MainActivity.kt | 16 +- .../example/smarthome/data/WeatherService.kt | 21 ++- .../com/example/smarthome/ui/MainScaffold.kt | 151 +++++++++++++++--- .../example/smarthome/ui/SettingsScreen.kt | 138 +++++++++++++++- app/src/main/res/drawable/ic_automation.xml | 24 +-- app/src/main/res/drawable/ic_scene.xml | 22 ++- app/src/main/res/drawable/ic_security.xml | 12 +- app/src/main/res/drawable/ic_statistics.xml | 24 +-- 8 files changed, 328 insertions(+), 80 deletions(-) diff --git a/app/src/main/java/com/example/smarthome/MainActivity.kt b/app/src/main/java/com/example/smarthome/MainActivity.kt index 3ce39da..c2793aa 100644 --- a/app/src/main/java/com/example/smarthome/MainActivity.kt +++ b/app/src/main/java/com/example/smarthome/MainActivity.kt @@ -26,6 +26,8 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.layout.ContentScale import androidx.compose.foundation.background import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.runtime.collectAsState +import com.example.smarthome.data.BackgroundManager class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -113,10 +115,20 @@ fun AppRoot() { rooms = roomList } + // 初始化背景管理器并获取当前选中的背景 + androidx.compose.runtime.LaunchedEffect(Unit) { + BackgroundManager.init(context) + } + val selectedBgId by BackgroundManager.selectedBackground.collectAsState() + val currentBg = BackgroundManager.backgrounds.getOrNull(selectedBgId) + val backgroundRes = currentBg?.resourceId ?: R.drawable.background1 + // 根据背景类型调整遮罩透明度:暗色背景减少遮罩,亮色背景增加遮罩 + val overlayAlpha = if (currentBg?.isDark == true) 0x11 else 0x88 + Box(modifier = Modifier.fillMaxSize()) { // 背景图片 - 填充整个屏幕包括系统栏,使用FillBounds确保完全填充 Image( - painter = painterResource(id = R.drawable.background), + painter = painterResource(id = backgroundRes), contentDescription = null, modifier = Modifier.fillMaxSize(), contentScale = ContentScale.FillBounds, @@ -127,7 +139,7 @@ fun AppRoot() { Box( modifier = Modifier .fillMaxSize() - .background(Color(0x88121212)) + .background(Color(overlayAlpha shl 24 or 0x121212)) ) // 主内容 - 添加系统栏内边距 diff --git a/app/src/main/java/com/example/smarthome/data/WeatherService.kt b/app/src/main/java/com/example/smarthome/data/WeatherService.kt index 78f546e..2340249 100644 --- a/app/src/main/java/com/example/smarthome/data/WeatherService.kt +++ b/app/src/main/java/com/example/smarthome/data/WeatherService.kt @@ -26,6 +26,11 @@ data class WeatherInfo( object WeatherService { private const val TAG = "WeatherService" + // 缓存天气数据,避免重复获取 + private var cachedWeather: WeatherInfo? = null + private var lastFetchTime: Long = 0 + private const val CACHE_DURATION = 10 * 60 * 1000L // 10分钟缓存 + private val client = OkHttpClient.Builder() .connectTimeout(15, TimeUnit.SECONDS) .readTimeout(15, TimeUnit.SECONDS) @@ -34,8 +39,16 @@ object WeatherService { /** * 获取天气信息 * 优先使用GPS定位,失败则用IP定位 + * 带缓存机制,10分钟内不重复获取 */ - suspend fun getWeather(context: Context): WeatherInfo { + suspend fun getWeather(context: Context, forceRefresh: Boolean = false): WeatherInfo { + // 检查缓存 + val now = System.currentTimeMillis() + if (!forceRefresh && cachedWeather != null && (now - lastFetchTime) < CACHE_DURATION) { + Log.d(TAG, "Using cached weather data") + return cachedWeather!! + } + return withContext(Dispatchers.IO) { // 1. 优先尝试GPS定位 val gpsLocation = getGpsLocation(context) @@ -46,6 +59,8 @@ object WeatherService { // 用GPS坐标获取天气 val weatherResult = getWeatherByLocation(lat, lon, city) if (weatherResult != null) { + cachedWeather = weatherResult + lastFetchTime = now return@withContext weatherResult } } @@ -54,6 +69,8 @@ object WeatherService { Log.w(TAG, "GPS failed, trying wttr.in...") val wttrResult = tryWttrIn() if (wttrResult != null) { + cachedWeather = wttrResult + lastFetchTime = now return@withContext wttrResult } @@ -61,6 +78,8 @@ object WeatherService { Log.w(TAG, "wttr.in failed, trying backup...") val backupResult = tryBackupApi() if (backupResult != null) { + cachedWeather = backupResult + lastFetchTime = now return@withContext backupResult } diff --git a/app/src/main/java/com/example/smarthome/ui/MainScaffold.kt b/app/src/main/java/com/example/smarthome/ui/MainScaffold.kt index c451f51..b24cbf0 100644 --- a/app/src/main/java/com/example/smarthome/ui/MainScaffold.kt +++ b/app/src/main/java/com/example/smarthome/ui/MainScaffold.kt @@ -316,7 +316,7 @@ fun RoomTabs( } // 固定的添加按钮 - GradientButton("+ 添加", onClick = onAddRoomClick) + GradientButton("+ 添加房间", onClick = onAddRoomClick) } } @@ -490,17 +490,82 @@ fun NavItem(title: String, selected: Boolean) { @Composable fun SideNavRail(selectedNavItem: Int = 0, onNavItemSelect: (Int) -> Unit = {}) { - Column(modifier = Modifier.width(100.dp).fillMaxHeight().padding(start = 12.dp, end = 4.dp, top = 12.dp, bottom = 12.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { - Spacer(modifier = Modifier.height(48.dp)) - NavRailItem("控制台", R.drawable.ic_dashboard, selectedNavItem == 0, onClick = { onNavItemSelect(0) }) - NavRailItem("场景", R.drawable.ic_scene, selectedNavItem == 1, onClick = { onNavItemSelect(1) }) - NavRailItem("自动化", R.drawable.ic_automation, selectedNavItem == 2, onClick = { onNavItemSelect(2) }) - NavRailItem("统计", R.drawable.ic_statistics, selectedNavItem == 3, onClick = { onNavItemSelect(3) }) - NavRailItem("安全", R.drawable.ic_security, selectedNavItem == 4, onClick = { onNavItemSelect(4) }) - NavRailItem("设置", R.drawable.ic_settings, selectedNavItem == 5, onClick = { onNavItemSelect(5) }) + 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 + ) { + // 上半部分:导航项 + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + Spacer(modifier = Modifier.height(48.dp)) + NavRailItem("控制台", R.drawable.ic_dashboard, selectedNavItem == 0, onClick = { onNavItemSelect(0) }) + NavRailItem("场景", R.drawable.ic_scene, selectedNavItem == 1, onClick = { onNavItemSelect(1) }) + NavRailItem("自动化", R.drawable.ic_automation, selectedNavItem == 2, onClick = { onNavItemSelect(2) }) + NavRailItem("统计", R.drawable.ic_statistics, selectedNavItem == 3, onClick = { onNavItemSelect(3) }) + NavRailItem("安全", R.drawable.ic_security, selectedNavItem == 4, onClick = { onNavItemSelect(4) }) + NavRailItem("设置", 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( @@ -574,11 +639,7 @@ fun TopBar() { Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { Text(text = "控制台", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, color = Color.White) - Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically) { - // 天气信息卡片 - WeatherCard(weatherInfo = weatherInfo, isLoading = isLoading) - GradientButton("+ 添加设备") - } + WeatherCard(weatherInfo = weatherInfo, isLoading = isLoading) } } @@ -1060,9 +1121,40 @@ fun AirConditionerCard(modifier: Modifier = Modifier, roomName: String = "房间 var selectedMode by remember { mutableStateOf(0) } // 0:制冷 1:制热 2:除湿 3:送风 var fanSpeed by remember { mutableStateOf(1) } // 0:自动 1:低 2:中 3:高 - val modes = listOf("❄️" to "制冷", "🔥" to "制热", "💧" to "除湿", "🌀" to "送风") + // 模式emoji图标 + val modeIcons = listOf( + "❄️" to "制冷", + "☀️" to "制热", + "💧" to "除湿", + "🌀" to "送风" + ) val fanSpeeds = listOf("自动", "低", "中", "高") + // 图标动画 + 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 accentColor = when (selectedMode) { 0 -> Color(0xFF4FC3F7) // 制冷 - 蓝色 @@ -1071,6 +1163,9 @@ fun AirConditionerCard(modifier: Modifier = Modifier, roomName: String = "房间 else -> Color(0xFFB0BEC5) // 送风 - 灰色 } + // 当前模式的emoji + val currentEmoji = modeIcons[selectedMode].first + Box( modifier = modifier .height(260.dp) @@ -1096,7 +1191,7 @@ fun AirConditionerCard(modifier: Modifier = Modifier, roomName: String = "房间 modifier = Modifier.fillMaxWidth() ) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { - // 空调图标 + // 空调图标 - 根据模式应用不同动画 Box( modifier = Modifier .size(36.dp) @@ -1104,7 +1199,17 @@ fun AirConditionerCard(modifier: Modifier = Modifier, roomName: String = "房间 .background(accentColor.copy(alpha = if (isOn) 0.3f else 0.1f)), contentAlignment = Alignment.Center ) { - Text(text = "❄️", fontSize = 18.sp) + // 根据模式选择动画:除湿用上下移动,其他用旋转 + 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 = "空调", fontWeight = FontWeight.SemiBold, color = Color.White, fontSize = 16.sp) @@ -1177,22 +1282,28 @@ fun AirConditionerCard(modifier: Modifier = Modifier, roomName: String = "房间 modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - modes.forEachIndexed { index, (icon, name) -> + 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) .height(44.dp) .clip(RoundedCornerShape(12.dp)) .background( - if (isSelected && isOn) accentColor.copy(alpha = 0.4f) + if (isSelected && isOn) modeColor.copy(alpha = 0.4f) else Color(0xFF2A2A3E).copy(alpha = 0.6f) ) .clickable(enabled = isOn) { selectedMode = index }, contentAlignment = Alignment.Center ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text(text = icon, fontSize = 14.sp) + Text(text = emoji, fontSize = 14.sp) Text( text = name, fontSize = 10.sp, diff --git a/app/src/main/java/com/example/smarthome/ui/SettingsScreen.kt b/app/src/main/java/com/example/smarthome/ui/SettingsScreen.kt index e65a45e..103f99d 100644 --- a/app/src/main/java/com/example/smarthome/ui/SettingsScreen.kt +++ b/app/src/main/java/com/example/smarthome/ui/SettingsScreen.kt @@ -1,5 +1,6 @@ package com.example.smarthome.ui +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -15,9 +16,13 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.example.smarthome.data.BackgroundManager @Composable fun SettingsScreen(onBack: () -> Unit) { @@ -75,12 +80,25 @@ fun SettingsContentList() { }} item { SettingsSection(title = "显示设置") { - var darkMode by remember { mutableStateOf(true) } + BackgroundSelector() + + val context = LocalContext.current + val selectedBg by BackgroundManager.selectedBackground.collectAsState() + // 深色模式根据当前背景是否为暗色来判断 + val isDarkMode = BackgroundManager.backgrounds.getOrNull(selectedBg)?.isDark ?: false + SettingsSwitchItem( title = "深色模式", subtitle = "使用深色主题", - checked = darkMode, - onCheckedChange = { darkMode = it } + checked = isDarkMode, + onCheckedChange = { dark -> + // 切换深色/浅色模式时自动更换背景 + if (dark) { + BackgroundManager.setBackground(context, 2) // 紫金绸 + } else { + BackgroundManager.setBackground(context, 0) // 丝绸白 + } + } ) SettingsItem( @@ -355,3 +373,117 @@ fun SettingsContent() { SettingsContentList() } } + +@Composable +fun BackgroundSelector() { + val context = LocalContext.current + val selectedBackground by BackgroundManager.selectedBackground.collectAsState() + + val interactionSources = remember { + BackgroundManager.backgrounds.map { MutableInteractionSource() } + } + + Column( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .background(Color(0x33121212)) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = "背景壁纸", + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + color = Color.White + ) + Text( + text = "选择您喜欢的背景图片", + fontSize = 13.sp, + color = Color(0xFF9AA0A6) + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + BackgroundManager.backgrounds.forEachIndexed { index, bg -> + val isSelected = selectedBackground == bg.id + + Column( + modifier = Modifier.weight(1f), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(56.dp) + .clip(RoundedCornerShape(10.dp)) + .then( + if (isSelected) { + Modifier.border( + width = 2.dp, + brush = Brush.linearGradient( + listOf(Color(0xFFA9F0FF), Color(0xFFB89CFF)) + ), + shape = RoundedCornerShape(10.dp) + ) + } else Modifier + ) + .clickable( + indication = null, + interactionSource = interactionSources[index] + ) { + BackgroundManager.setBackground(context, bg.id) + } + ) { + Image( + painter = painterResource(id = bg.thumbId), + contentDescription = bg.name, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize() + ) + + if (isSelected) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.3f)), + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier + .size(20.dp) + .clip(RoundedCornerShape(10.dp)) + .background( + Brush.linearGradient( + listOf(Color(0xFFA9F0FF), Color(0xFFB89CFF)) + ) + ), + contentAlignment = Alignment.Center + ) { + Text( + text = "✓", + color = Color.White, + fontSize = 12.sp, + fontWeight = FontWeight.Bold + ) + } + } + } + } + + Text( + text = bg.name, + fontSize = 11.sp, + fontWeight = if (isSelected) FontWeight.Medium else FontWeight.Normal, + color = if (isSelected) Color(0xFFA9F0FF) else Color(0xFF9AA0A6) + ) + } + } + } + } +} diff --git a/app/src/main/res/drawable/ic_automation.xml b/app/src/main/res/drawable/ic_automation.xml index 497ab8e..8ce4ed6 100644 --- a/app/src/main/res/drawable/ic_automation.xml +++ b/app/src/main/res/drawable/ic_automation.xml @@ -4,28 +4,8 @@ android:viewportWidth="24" android:viewportHeight="24"> - + - - - - - - - - + android:pathData="M13,2 L4,14 L11,14 L11,22 L20,10 L13,10 L13,2 Z"/> diff --git a/app/src/main/res/drawable/ic_scene.xml b/app/src/main/res/drawable/ic_scene.xml index d4c209f..53d7e8c 100644 --- a/app/src/main/res/drawable/ic_scene.xml +++ b/app/src/main/res/drawable/ic_scene.xml @@ -4,28 +4,24 @@ android:viewportWidth="24" android:viewportHeight="24"> - + + + android:pathData="M4,4 L10,4 C10.55,4 11,4.45 11,5 L11,10 C11,10.55 10.55,11 10,11 L4,11 C3.45,11 3,10.55 3,10 L3,5 C3,4.45 3.45,4 4,4 Z"/> + + android:pathData="M14,4 L20,4 C20.55,4 21,4.45 21,5 L21,10 C21,10.55 20.55,11 20,11 L14,11 C13.45,11 13,10.55 13,10 L13,5 C13,4.45 13.45,4 14,4 Z"/> + + android:pathData="M4,13 L10,13 C10.55,13 11,13.45 11,14 L11,19 C11,19.55 10.55,20 10,20 L4,20 C3.45,20 3,19.55 3,19 L3,14 C3,13.45 3.45,13 4,13 Z"/> - + - - + android:pathData="M14,13 L20,13 C20.55,13 21,13.45 21,14 L21,19 C21,19.55 20.55,20 20,20 L14,20 C13.45,20 13,19.55 13,19 L13,14 C13,13.45 13.45,13 14,13 Z"/> diff --git a/app/src/main/res/drawable/ic_security.xml b/app/src/main/res/drawable/ic_security.xml index 6ada778..bdfaf4d 100644 --- a/app/src/main/res/drawable/ic_security.xml +++ b/app/src/main/res/drawable/ic_security.xml @@ -1,3 +1,11 @@ - - + + + + diff --git a/app/src/main/res/drawable/ic_statistics.xml b/app/src/main/res/drawable/ic_statistics.xml index b94951b..56851eb 100644 --- a/app/src/main/res/drawable/ic_statistics.xml +++ b/app/src/main/res/drawable/ic_statistics.xml @@ -4,29 +4,19 @@ android:viewportWidth="24" android:viewportHeight="24"> - + + + android:pathData="M12,2 C6.48,2 2,6.48 2,12 C2,17.52 6.48,22 12,22 C17.52,22 22,17.52 22,12 C22,6.48 17.52,2 12,2 Z M12,20 C7.59,20 4,16.41 4,12 C4,7.59 7.59,4 12,4 C16.41,4 20,7.59 20,12 C20,16.41 16.41,20 12,20 Z"/> + + android:pathData="M12,12 L12,4 C16.41,4 20,7.59 20,12 L12,12 Z"/> + - - - - - - + android:pathData="M12,12 L20,12 C20,14.5 19,16.8 17.3,18.3 L12,12 Z"/>