Compare commits
No commits in common. "bb096bb5b1733ef33042eac4d5c9ad5756568e93" and "8c21431bcc9d298d3781da1e3f3a33b1a0b8f97f" have entirely different histories.
bb096bb5b1
...
8c21431bcc
@ -1,17 +1,16 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("org.jetbrains.kotlin.plugin.compose")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.example.smarthome"
|
||||
compileSdk = 36
|
||||
compileSdk = 35
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.example.smarthome"
|
||||
minSdk = 33 // Android 13+
|
||||
targetSdk = 36
|
||||
minSdk = 24
|
||||
targetSdk = 35
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
vectorDrawables {
|
||||
@ -37,6 +36,9 @@ android {
|
||||
compose = true
|
||||
viewBinding = true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.5.14"
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
@ -65,16 +67,13 @@ dependencies {
|
||||
implementation("androidx.compose.material3:material3")
|
||||
implementation("androidx.compose.material3:material3-window-size-class:1.3.0")
|
||||
|
||||
// Haze - 现代毛玻璃效果库
|
||||
implementation("dev.chrisbanes.haze:haze:1.7.1")
|
||||
|
||||
implementation("com.github.Dimezis:BlurView:version-3.2.0")
|
||||
implementation("com.google.code.gson:gson:2.10.1")
|
||||
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
||||
implementation("com.google.android.gms:play-services-location:21.0.1")
|
||||
|
||||
// ZXing 二维码生成
|
||||
implementation("com.google.zxing:core:3.5.2")
|
||||
|
||||
|
||||
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12")
|
||||
|
||||
|
||||
@ -16,8 +16,5 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".MainActivityHaze" />
|
||||
<activity android:name=".MainActivityCompose" />
|
||||
<activity android:name=".MainActivityAlternative" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
package com.example.smarthome
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import com.example.smarthome.ui.theme.SmartHomeTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.LocalOverscrollConfiguration
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||
import eightbitlab.com.blurview.BlurTarget
|
||||
import eightbitlab.com.blurview.BlurView
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.ui.Modifier
|
||||
@ -31,7 +31,6 @@ import androidx.compose.runtime.collectAsState
|
||||
import com.example.smarthome.data.BackgroundManager
|
||||
import com.example.smarthome.data.LanguageManager
|
||||
import com.example.smarthome.data.UserManager
|
||||
import com.example.smarthome.ui.SmartHomeScreen
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@ -54,48 +53,65 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
val decorView = window.decorView
|
||||
val blurTarget = findViewById<BlurTarget>(R.id.blurTarget)
|
||||
val blurView = findViewById<BlurView>(R.id.blurView)
|
||||
val windowBackground: Drawable? = decorView.background
|
||||
blurView.setupWith(blurTarget)
|
||||
.setFrameClearDrawable(windowBackground)
|
||||
.setBlurRadius(20f)
|
||||
|
||||
}
|
||||
private fun hideStatusBar() {
|
||||
// 让内容延伸到系统栏区域
|
||||
// 设置全屏模式,让内容延伸到系统栏区域
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
// 使用现代 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
|
||||
}
|
||||
|
||||
|
||||
// 设置全屏标志
|
||||
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
|
||||
)
|
||||
|
||||
// 支持刘海屏设备
|
||||
val lp = window.attributes
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
|
||||
window.attributes = window.attributes.also {
|
||||
it.layoutInDisplayCutoutMode = android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||
}
|
||||
lp.layoutInDisplayCutoutMode = android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||
}
|
||||
window.attributes = lp
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun AppRoot() {
|
||||
val context = androidx.compose.ui.platform.LocalContext.current
|
||||
|
||||
|
||||
// 监听登录状态
|
||||
val isLoggedIn by UserManager.isLoggedIn.collectAsState()
|
||||
|
||||
// 禁用上下滑到边界时的拉伸回弹效果,降低滑动重绘开销
|
||||
CompositionLocalProvider(LocalOverscrollConfiguration provides null) {
|
||||
if (!isLoggedIn) {
|
||||
// 未登录,显示登录页面
|
||||
LoginScreen(
|
||||
onLoginSuccess = {
|
||||
// 登录成功后会自动更新isLoggedIn状态
|
||||
}
|
||||
)
|
||||
} else {
|
||||
// 已登录,显示主界面
|
||||
MainContent()
|
||||
}
|
||||
|
||||
if (!isLoggedIn) {
|
||||
// 未登录,显示登录页面
|
||||
LoginScreen(
|
||||
onLoginSuccess = {
|
||||
// 登录成功后会自动更新isLoggedIn状态
|
||||
}
|
||||
)
|
||||
} else {
|
||||
// 已登录,显示主界面
|
||||
MainContent()
|
||||
}
|
||||
}
|
||||
|
||||
@ -118,7 +134,7 @@ fun MainContent() {
|
||||
}
|
||||
|
||||
var selectedRoom by remember { mutableStateOf(0) }
|
||||
var selectedNavItem by remember { mutableStateOf(0) }
|
||||
var selectedNavItem by remember { mutableStateOf(0) } // 0=控制台, 5=设置
|
||||
var rooms by remember { mutableStateOf(savedRooms) }
|
||||
|
||||
// 保存房间列表到SharedPreferences
|
||||
@ -127,8 +143,10 @@ fun MainContent() {
|
||||
rooms = roomList
|
||||
}
|
||||
|
||||
// 初始化背景管理器并获取当前选中的背景(同步初始化,避免首帧闪烁默认背景)
|
||||
remember { BackgroundManager.init(context) }
|
||||
// 初始化背景管理器并获取当前选中的背景
|
||||
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
|
||||
@ -152,32 +170,26 @@ fun MainContent() {
|
||||
.background(Color(overlayAlpha shl 24 or 0x121212))
|
||||
)
|
||||
|
||||
// 主内容 - 全屏显示
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
// 主内容 - 添加系统栏内边距
|
||||
Box(modifier = Modifier.fillMaxSize().systemBarsPadding()) {
|
||||
MainScaffold(
|
||||
selectedRoom = selectedRoom,
|
||||
onRoomSelect = { roomIndex ->
|
||||
selectedRoom = roomIndex
|
||||
selectedNavItem = 0 // 切换到控制台页面
|
||||
},
|
||||
selectedNavItem = selectedNavItem,
|
||||
onNavItemSelect = { selectedNavItem = it },
|
||||
rooms = rooms,
|
||||
onAddRoom = { newRoomName ->
|
||||
saveRooms(rooms + newRoomName)
|
||||
},
|
||||
onDeleteRoom = { index ->
|
||||
if (rooms.size > 1) { // 至少保留一个房间
|
||||
saveRooms(rooms.filterIndexed { i, _ -> i != index })
|
||||
if (selectedRoom >= rooms.size - 1) {
|
||||
selectedRoom = (rooms.size - 2).coerceAtLeast(0)
|
||||
}
|
||||
selectedRoom = selectedRoom,
|
||||
onRoomSelect = { selectedRoom = it },
|
||||
selectedNavItem = selectedNavItem,
|
||||
onNavItemSelect = { selectedNavItem = it },
|
||||
rooms = rooms,
|
||||
onAddRoom = { newRoomName ->
|
||||
saveRooms(rooms + newRoomName)
|
||||
},
|
||||
onDeleteRoom = { index ->
|
||||
if (rooms.size > 1) { // 至少保留一个房间
|
||||
saveRooms(rooms.filterIndexed { i, _ -> i != index })
|
||||
if (selectedRoom >= rooms.size - 1) {
|
||||
selectedRoom = (rooms.size - 2).coerceAtLeast(0)
|
||||
}
|
||||
},
|
||||
onRenameRoom = { index, newName ->
|
||||
saveRooms(rooms.mapIndexed { i, name -> if (i == index) newName else name })
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
package com.example.smarthome
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
||||
class MainActivityAlternative : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
// 简单的空 Activity
|
||||
}
|
||||
}
|
||||
@ -1,176 +0,0 @@
|
||||
package com.example.smarthome
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.blur
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.smarthome.ui.theme.SmartHomeTheme
|
||||
|
||||
class MainActivityCompose : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
SmartHomeTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = Color(0xFF1A1A2E)
|
||||
) {
|
||||
SmartHomeScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SmartHomeScreen() {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
Color(0xFF1A1A2E),
|
||||
Color(0xFF16213E),
|
||||
Color(0xFF0F3460)
|
||||
)
|
||||
)
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
// 左侧毛玻璃边栏
|
||||
GlassSidebar(
|
||||
modifier = Modifier
|
||||
.width(260.dp)
|
||||
.padding(24.dp)
|
||||
)
|
||||
|
||||
// 主内容区域
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(24.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
GlassCard()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GlassSidebar(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxHeight()
|
||||
.clip(RoundedCornerShape(32.dp))
|
||||
.background(
|
||||
Brush.linearGradient(
|
||||
colors = listOf(
|
||||
Color.White.copy(alpha = 0.15f),
|
||||
Color.White.copy(alpha = 0.08f)
|
||||
)
|
||||
)
|
||||
)
|
||||
.blur(20.dp)
|
||||
.padding(32.dp)
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Lumina",
|
||||
color = Color.White,
|
||||
fontSize = 32.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Smart Home",
|
||||
color = Color.White.copy(alpha = 0.6f),
|
||||
fontSize = 14.sp
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
listOf("🏠 首页", "🌡️ 温控", "💡 照明", "🔒 安全").forEach { item ->
|
||||
Text(
|
||||
text = item,
|
||||
color = if (item == "🏠 首页") Color.White else Color.White.copy(alpha = 0.6f),
|
||||
fontSize = 16.sp,
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GlassCard() {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(400.dp)
|
||||
.height(250.dp)
|
||||
.clip(RoundedCornerShape(24.dp))
|
||||
.background(
|
||||
Brush.linearGradient(
|
||||
colors = listOf(
|
||||
Color.White.copy(alpha = 0.20f),
|
||||
Color.White.copy(alpha = 0.12f),
|
||||
Color.White.copy(alpha = 0.06f)
|
||||
)
|
||||
)
|
||||
)
|
||||
.blur(25.dp)
|
||||
.padding(32.dp),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "智能家居控制中心",
|
||||
color = Color.White,
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "当前温度: 26°C",
|
||||
color = Color.White.copy(alpha = 0.8f),
|
||||
fontSize = 16.sp
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "湿度: 65%",
|
||||
color = Color.White.copy(alpha = 0.8f),
|
||||
fontSize = 16.sp
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "空气质量: 优",
|
||||
color = Color.White.copy(alpha = 0.8f),
|
||||
fontSize = 16.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,215 +0,0 @@
|
||||
package com.example.smarthome
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
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.hazeEffect
|
||||
import dev.chrisbanes.haze.hazeSource
|
||||
import com.example.smarthome.ui.theme.SmartHomeTheme
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
|
||||
class MainActivityHaze : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
SmartHomeTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = Color(0xFF1A1A2E)
|
||||
) {
|
||||
SmartHomeScreenWithHaze()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SmartHomeScreenWithHaze() {
|
||||
val hazeState = HazeState()
|
||||
val density = LocalDensity.current
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
Color(0xFF1A1A2E),
|
||||
Color(0xFF16213E),
|
||||
Color(0xFF0F3460)
|
||||
)
|
||||
)
|
||||
)
|
||||
.hazeSource(state = hazeState)
|
||||
) {
|
||||
// 背景装饰内容
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.SpaceEvenly,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
repeat(5) { index ->
|
||||
with(density) {
|
||||
val sizeDp = (100 + index * 20).dp
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(sizeDp)
|
||||
.clip(RoundedCornerShape(50))
|
||||
.background(
|
||||
Brush.radialGradient(
|
||||
colors = listOf(
|
||||
Color(0xFF6C5CE7),
|
||||
Color(0xFFA29BFE),
|
||||
Color.Transparent
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
// 左侧毛玻璃边栏
|
||||
HazeSidebar(
|
||||
modifier = Modifier
|
||||
.width(260.dp)
|
||||
.padding(24.dp),
|
||||
hazeState = hazeState
|
||||
)
|
||||
|
||||
// 主内容区域
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(24.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
HazeCard(hazeState = hazeState)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HazeSidebar(
|
||||
modifier: Modifier = Modifier,
|
||||
hazeState: HazeState
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxHeight()
|
||||
.clip(RoundedCornerShape(32.dp))
|
||||
.hazeEffect(
|
||||
state = hazeState,
|
||||
style = HazeStyle(
|
||||
blurRadius = 20.dp,
|
||||
tint = HazeTint(Color.White.copy(alpha = 0.15f)),
|
||||
noiseFactor = 0.0f
|
||||
)
|
||||
)
|
||||
.padding(32.dp)
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Lumina",
|
||||
color = Color.White,
|
||||
fontSize = 32.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Smart Home",
|
||||
color = Color.White.copy(alpha = 0.6f),
|
||||
fontSize = 14.sp
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
listOf("🏠 首页", "🌡️ 温控", "💡 照明", "🔒 安全").forEach { item ->
|
||||
Text(
|
||||
text = item,
|
||||
color = if (item == "🏠 首页") Color.White else Color.White.copy(alpha = 0.6f),
|
||||
fontSize = 16.sp,
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HazeCard(
|
||||
hazeState: HazeState
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(400.dp)
|
||||
.height(250.dp)
|
||||
.clip(RoundedCornerShape(24.dp))
|
||||
.hazeEffect(
|
||||
state = hazeState,
|
||||
style = HazeStyle(
|
||||
blurRadius = 25.dp,
|
||||
tint = HazeTint(Color.White.copy(alpha = 0.2f)),
|
||||
noiseFactor = 0.0f
|
||||
)
|
||||
)
|
||||
.padding(32.dp),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "智能家居控制中心",
|
||||
color = Color.White,
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "当前温度: 26°C",
|
||||
color = Color.White.copy(alpha = 0.8f),
|
||||
fontSize = 16.sp
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "湿度: 65%",
|
||||
color = Color.White.copy(alpha = 0.8f),
|
||||
fontSize = 16.sp
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "空气质量: 优",
|
||||
color = Color.White.copy(alpha = 0.8f),
|
||||
fontSize = 16.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,583 +0,0 @@
|
||||
package com.example.smarthome.ui
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
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.R
|
||||
import kotlinx.coroutines.delay
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
|
||||
// ==================== 配色 ====================
|
||||
private val BgDeep = Color(0xFF08090D)
|
||||
private val CardBg = Color(0xFF0F1116)
|
||||
private val CardBorder = Color(0xFF1E2030)
|
||||
private val TextWhite = Color(0xFFE8EAF0)
|
||||
private val TextGray = Color(0xFF6B7080)
|
||||
private val TextSub = Color(0xFF9DA5B4)
|
||||
private val AccentBlue = Color(0xFF3D7EFF)
|
||||
private val AccentCyan = Color(0xFF00C8C8)
|
||||
private val AccentGreen = Color(0xFF36D68A)
|
||||
private val AccentAmber = Color(0xFFF5A623)
|
||||
private val AccentPurple = Color(0xFF9B6FFF)
|
||||
|
||||
// ==================== 主界面 ====================
|
||||
@Composable
|
||||
fun HomeScreen() {
|
||||
var selectedRoom by remember { mutableStateOf("客厅") }
|
||||
var currentTime by remember { mutableStateOf("") }
|
||||
LaunchedEffect(Unit) {
|
||||
while (true) {
|
||||
currentTime = SimpleDateFormat("HH:mm", Locale.CHINA).format(Date())
|
||||
delay(30_000)
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
// 背景
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
drawRect(color = BgDeep)
|
||||
drawCircle(
|
||||
brush = Brush.radialGradient(
|
||||
colors = listOf(
|
||||
Color(0xFF1A2744).copy(alpha = 0.6f),
|
||||
Color.Transparent
|
||||
),
|
||||
center = Offset(size.width * 0.78f, size.height * 0.22f),
|
||||
radius = size.width * 0.55f
|
||||
),
|
||||
radius = size.width * 0.55f,
|
||||
center = Offset(size.width * 0.78f, size.height * 0.22f)
|
||||
)
|
||||
drawCircle(
|
||||
brush = Brush.radialGradient(
|
||||
colors = listOf(
|
||||
Color(0xFF0D3030).copy(alpha = 0.45f),
|
||||
Color.Transparent
|
||||
),
|
||||
center = Offset(size.width * 0.15f, size.height * 0.75f),
|
||||
radius = size.width * 0.4f
|
||||
),
|
||||
radius = size.width * 0.4f,
|
||||
center = Offset(size.width * 0.15f, size.height * 0.75f)
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 20.dp, vertical = 18.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// 顶部标题栏
|
||||
TopBar(currentTime = currentTime)
|
||||
|
||||
RoomGrid(
|
||||
selectedRoom = selectedRoom,
|
||||
onRoomSelected = { selectedRoom = it }
|
||||
)
|
||||
|
||||
FunctionGrid(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 顶部栏 ====================
|
||||
@Composable
|
||||
private fun TopBar(currentTime: String) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
text = "ZenHome",
|
||||
color = TextWhite,
|
||||
fontSize = 22.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
Text(
|
||||
text = "智能家居控制中心",
|
||||
color = TextGray,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
// 时间徽章
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(0.dp))
|
||||
.background(CardBg)
|
||||
.border(1.dp, CardBorder, RoundedCornerShape(0.dp))
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = if (currentTime.isEmpty()) "--:--" else currentTime,
|
||||
color = TextWhite,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class RoomItem(
|
||||
val name: String,
|
||||
val icon: String,
|
||||
val deviceCount: String,
|
||||
val accent: Color
|
||||
)
|
||||
|
||||
private val rooms = listOf(
|
||||
RoomItem("客厅", "🛋", "6 台设备", AccentBlue),
|
||||
RoomItem("厨房", "🍳", "4 台设备", AccentAmber),
|
||||
RoomItem("卧室", "🛏", "5 台设备", AccentPurple),
|
||||
RoomItem("影音室", "🎬", "3 台设备", AccentCyan),
|
||||
RoomItem("游戏房", "🎮", "2 台设备", AccentGreen)
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun RoomGrid(
|
||||
selectedRoom: String,
|
||||
onRoomSelected: (String) -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(96.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
rooms.forEach { room ->
|
||||
val selected = room.name == selectedRoom
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
.clip(RoundedCornerShape(0.dp))
|
||||
.background(
|
||||
if (selected) {
|
||||
Brush.verticalGradient(
|
||||
listOf(room.accent.copy(alpha = 0.28f), room.accent.copy(alpha = 0.08f))
|
||||
)
|
||||
} else {
|
||||
Brush.verticalGradient(listOf(CardBg, CardBg))
|
||||
}
|
||||
)
|
||||
.border(
|
||||
1.dp,
|
||||
if (selected) room.accent.copy(alpha = 0.55f) else CardBorder,
|
||||
RoundedCornerShape(0.dp)
|
||||
)
|
||||
.clickable { onRoomSelected(room.name) }
|
||||
.padding(12.dp)
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.SpaceBetween) {
|
||||
Text(room.icon, fontSize = 20.sp)
|
||||
Column {
|
||||
Text(
|
||||
text = room.name,
|
||||
color = if (selected) room.accent else TextWhite,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Text(room.deviceCount, color = TextGray, fontSize = 11.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 左侧模式列 ====================
|
||||
private data class ModeItem(
|
||||
val label: String,
|
||||
val icon: String,
|
||||
val accent: Color,
|
||||
val desc: String
|
||||
)
|
||||
|
||||
private val modes = listOf(
|
||||
ModeItem("在家", "🏠", AccentBlue, "全功能"),
|
||||
ModeItem("离家", "🚶", AccentAmber, "节能待机"),
|
||||
ModeItem("舒适", "🛋", AccentPurple, "舒适模式"),
|
||||
ModeItem("节能", "🌿", AccentGreen, "低耗模式")
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun ModeColumn(
|
||||
selectedMode: String,
|
||||
onModeSelected: (String) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
modes.forEach { mode ->
|
||||
ModeCard(
|
||||
item = mode,
|
||||
selected = selectedMode == mode.label,
|
||||
onClick = { onModeSelected(mode.label) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ModeCard(
|
||||
item: ModeItem,
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
if (item.label == "离家" || item.label == "舒适") {
|
||||
val resId = if (item.label == "离家") R.drawable.lijia else R.drawable.shushi
|
||||
val borderColor = if (selected) item.accent.copy(alpha = 0.55f) else CardBorder
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(0.dp))
|
||||
.border(1.dp, borderColor, RoundedCornerShape(0.dp))
|
||||
.clickable { onClick() }
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = resId),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier.matchParentSize()
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val bgBrush = if (selected) {
|
||||
Brush.verticalGradient(
|
||||
listOf(item.accent.copy(alpha = 0.28f), item.accent.copy(alpha = 0.10f))
|
||||
)
|
||||
} else {
|
||||
Brush.verticalGradient(listOf(CardBg, CardBg))
|
||||
}
|
||||
val borderColor = if (selected) item.accent.copy(alpha = 0.55f) else CardBorder
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(0.dp))
|
||||
.background(bgBrush)
|
||||
.border(1.dp, borderColor, RoundedCornerShape(0.dp))
|
||||
.clickable { onClick() }
|
||||
.padding(horizontal = 10.dp, vertical = 12.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(item.icon, fontSize = 22.sp)
|
||||
Spacer(Modifier.height(6.dp))
|
||||
Text(
|
||||
text = item.label,
|
||||
color = if (selected) item.accent else TextWhite,
|
||||
fontSize = 13.sp,
|
||||
fontWeight = if (selected) FontWeight.Bold else FontWeight.Normal
|
||||
)
|
||||
Spacer(Modifier.height(3.dp))
|
||||
Text(
|
||||
text = item.desc,
|
||||
color = TextGray,
|
||||
fontSize = 10.sp
|
||||
)
|
||||
if (selected) {
|
||||
Spacer(Modifier.height(6.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(6.dp)
|
||||
.clip(CircleShape)
|
||||
.background(item.accent)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 右侧 2×2 功能卡片 ====================
|
||||
@Composable
|
||||
private fun FunctionGrid(modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
OutdoorCard(modifier = Modifier.weight(1f).fillMaxHeight())
|
||||
IndoorCard(modifier = Modifier.weight(1f).fillMaxHeight())
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
LightingCard(modifier = Modifier.weight(1f).fillMaxHeight())
|
||||
EnergyCard(modifier = Modifier.weight(1f).fillMaxHeight())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 室外环境卡片 ====================
|
||||
@Composable
|
||||
private fun OutdoorCard(modifier: Modifier = Modifier) {
|
||||
FuncCard(
|
||||
modifier = modifier,
|
||||
accentColor = AccentCyan
|
||||
) {
|
||||
CardHeader(icon = "🌤", title = "室外环境", accent = AccentCyan)
|
||||
Spacer(Modifier.height(10.dp))
|
||||
BigValueRow(value = "26", unit = "°C", label = "气温", accent = AccentCyan)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
EnvRow(items = listOf("湿度" to "62%", "PM2.5" to "34", "风速" to "3级"))
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 室内环境卡片 ====================
|
||||
@Composable
|
||||
private fun IndoorCard(modifier: Modifier = Modifier) {
|
||||
FuncCard(
|
||||
modifier = modifier,
|
||||
accentColor = AccentBlue
|
||||
) {
|
||||
CardHeader(icon = "🏡", title = "室内环境", accent = AccentBlue)
|
||||
Spacer(Modifier.height(10.dp))
|
||||
BigValueRow(value = "24", unit = "°C", label = "室温", accent = AccentBlue)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
EnvRow(items = listOf("湿度" to "55%", "CO₂" to "412", "噪音" to "38dB"))
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 全屋照明卡片 ====================
|
||||
@Composable
|
||||
private fun LightingCard(modifier: Modifier = Modifier) {
|
||||
FuncCard(
|
||||
modifier = modifier,
|
||||
accentColor = AccentAmber
|
||||
) {
|
||||
CardHeader(icon = "💡", title = "全屋照明", accent = AccentAmber)
|
||||
Spacer(Modifier.height(10.dp))
|
||||
BigValueRow(value = "8", unit = "/12", label = "灯具开启", accent = AccentAmber)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
LightBrightnessBar(brightness = 0.67f, accent = AccentAmber)
|
||||
Spacer(Modifier.height(6.dp))
|
||||
EnvRow(items = listOf("客厅" to "开", "卧室" to "关", "厨房" to "开"))
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 能耗统计卡片 ====================
|
||||
@Composable
|
||||
private fun EnergyCard(modifier: Modifier = Modifier) {
|
||||
FuncCard(
|
||||
modifier = modifier,
|
||||
accentColor = AccentGreen
|
||||
) {
|
||||
CardHeader(icon = "⚡", title = "能耗统计", accent = AccentGreen)
|
||||
Spacer(Modifier.height(10.dp))
|
||||
BigValueRow(value = "4.7", unit = "kWh", label = "今日用电", accent = AccentGreen)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
EnergyMiniChart(accent = AccentGreen)
|
||||
Spacer(Modifier.height(6.dp))
|
||||
EnvRow(items = listOf("空调" to "1.8", "照明" to "0.6", "其他" to "2.3"))
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 通用卡片容器 ====================
|
||||
@Composable
|
||||
private fun FuncCard(
|
||||
modifier: Modifier = Modifier,
|
||||
accentColor: Color,
|
||||
content: @Composable ColumnScope.() -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(0.dp))
|
||||
.background(CardBg)
|
||||
.border(1.dp, accentColor.copy(alpha = 0.18f), RoundedCornerShape(0.dp))
|
||||
.padding(14.dp),
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
// ==================== 卡片头部 ====================
|
||||
@Composable
|
||||
private fun CardHeader(icon: String, title: String, accent: Color) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.clip(RoundedCornerShape(0.dp))
|
||||
.background(accent.copy(alpha = 0.15f))
|
||||
.border(1.dp, accent.copy(alpha = 0.3f), RoundedCornerShape(0.dp)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(icon, fontSize = 16.sp)
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(title, color = TextWhite, fontSize = 14.sp, fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 大数值行 ====================
|
||||
@Composable
|
||||
private fun BigValueRow(value: String, unit: String, label: String, accent: Color) {
|
||||
Row(verticalAlignment = Alignment.Bottom) {
|
||||
Text(
|
||||
text = value,
|
||||
color = accent,
|
||||
fontSize = 36.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Column {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(unit, color = TextSub, fontSize = 14.sp, fontWeight = FontWeight.Medium)
|
||||
Text(label, color = TextGray, fontSize = 10.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 环境数据行 ====================
|
||||
@Composable
|
||||
private fun EnvRow(items: List<Pair<String, String>>) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
items.forEach { (k, v) ->
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(v, color = TextWhite, fontSize = 12.sp, fontWeight = FontWeight.Medium)
|
||||
Text(k, color = TextGray, fontSize = 10.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 亮度进度条 ====================
|
||||
@Composable
|
||||
private fun LightBrightnessBar(brightness: Float, accent: Color) {
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text("亮度", color = TextGray, fontSize = 10.sp)
|
||||
Text("${(brightness * 100).toInt()}%", color = accent, fontSize = 10.sp)
|
||||
}
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(4.dp)
|
||||
.clip(RoundedCornerShape(2.dp))
|
||||
.background(CardBorder)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(brightness)
|
||||
.height(4.dp)
|
||||
.clip(RoundedCornerShape(2.dp))
|
||||
.background(
|
||||
Brush.horizontalGradient(
|
||||
listOf(accent.copy(alpha = 0.6f), accent)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 能耗迷你折线图 ====================
|
||||
@Composable
|
||||
private fun EnergyMiniChart(accent: Color) {
|
||||
val points = listOf(1.2f, 2.1f, 1.8f, 3.2f, 2.7f, 4.1f, 4.7f)
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(36.dp)
|
||||
) {
|
||||
val w = size.width
|
||||
val h = size.height
|
||||
val maxVal = points.max()
|
||||
val minVal = points.min()
|
||||
val range = (maxVal - minVal).coerceAtLeast(0.1f)
|
||||
val step = w / (points.size - 1)
|
||||
|
||||
val path = androidx.compose.ui.graphics.Path()
|
||||
points.forEachIndexed { i, v ->
|
||||
val x = i * step
|
||||
val y = h - ((v - minVal) / range) * h * 0.85f - h * 0.07f
|
||||
if (i == 0) path.moveTo(x, y) else path.lineTo(x, y)
|
||||
}
|
||||
drawPath(
|
||||
path = path,
|
||||
color = accent,
|
||||
style = Stroke(width = 2.dp.toPx(), cap = StrokeCap.Round)
|
||||
)
|
||||
// 最后一点高亮圆点
|
||||
val lastX = (points.size - 1) * step
|
||||
val lastY = h - ((points.last() - minVal) / range) * h * 0.85f - h * 0.07f
|
||||
drawCircle(color = accent, radius = 3.dp.toPx(), center = Offset(lastX, lastY))
|
||||
drawCircle(
|
||||
color = accent.copy(alpha = 0.3f),
|
||||
radius = 6.dp.toPx(),
|
||||
center = Offset(lastX, lastY)
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -9,8 +9,6 @@ import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
@ -74,7 +72,7 @@ fun LoginScreen(
|
||||
val context = LocalContext.current
|
||||
|
||||
// 登录方式:phone=手机号登录,wechat=微信扫码登录
|
||||
var loginMethod by remember { mutableStateOf("wechat") }
|
||||
var loginMethod by remember { mutableStateOf("phone") }
|
||||
|
||||
// 扫码状态(微信登录用)
|
||||
var scanStatus by remember { mutableStateOf(ScanStatus.WAITING) }
|
||||
@ -337,8 +335,7 @@ fun WeChatLoginCard(
|
||||
),
|
||||
shape = RoundedCornerShape(28.dp)
|
||||
)
|
||||
.padding(24.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(32.dp)
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
@ -357,12 +354,12 @@ fun WeChatLoginCard(
|
||||
modifier = Modifier.padding(top = 4.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// 二维码区域
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(160.dp)
|
||||
.size(200.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(Color.White)
|
||||
.padding(12.dp),
|
||||
@ -461,15 +458,15 @@ fun WeChatLoginCard(
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// 模拟登录按钮(仅在模拟模式下显示)- 放在显眼位置
|
||||
// 模拟登录按钮(仅在模拟模式下显示)
|
||||
if (LoginConfig.USE_MOCK) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(52.dp)
|
||||
.clip(RoundedCornerShape(14.dp))
|
||||
.height(48.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(
|
||||
Brush.horizontalGradient(
|
||||
listOf(Color(0xFF07C160), Color(0xFF06AD56))
|
||||
@ -482,17 +479,17 @@ fun WeChatLoginCard(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(text = "<EFBFBD>", fontSize = 20.sp)
|
||||
Text(text = "💬", fontSize = 18.sp)
|
||||
Text(
|
||||
text = "点击模拟扫码登录",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
text = tr("login_simulate"),
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
// 刷新二维码
|
||||
|
||||
@ -1,613 +0,0 @@
|
||||
package com.example.smarthome.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
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 com.example.smarthome.R
|
||||
import com.example.smarthome.data.tr
|
||||
|
||||
@Composable
|
||||
fun EditRoomsDialog(
|
||||
rooms: List<String>,
|
||||
onDismiss: () -> Unit,
|
||||
onRenameRoom: (Int, String) -> Unit,
|
||||
onDeleteRoom: (Int) -> Unit,
|
||||
onAddRoom: () -> Unit = {}
|
||||
) {
|
||||
// 当前编辑的房间索引
|
||||
var editingIndex by remember { mutableStateOf(-1) }
|
||||
var editingName by remember { mutableStateOf("") }
|
||||
|
||||
// 背景遮罩
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { androidx.compose.foundation.interaction.MutableInteractionSource() }
|
||||
) { onDismiss() },
|
||||
contentAlignment = Alignment.TopCenter
|
||||
) {
|
||||
// 对话框主体 - 往上挪,使用透明卡片风格
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(top = 60.dp, bottom = 60.dp)
|
||||
.widthIn(max = 420.dp)
|
||||
.fillMaxWidth(0.9f)
|
||||
.clip(RoundedCornerShape(28.dp))
|
||||
.background(Color(0xFF1A1A1A).copy(alpha = 0.65f))
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = Color.White.copy(alpha = 0.15f),
|
||||
shape = RoundedCornerShape(28.dp)
|
||||
)
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { androidx.compose.foundation.interaction.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)
|
||||
}
|
||||
}
|
||||
|
||||
// 房间列表 - 使用 weight 占据剩余空间,确保底部按钮始终可见
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier
|
||||
.weight(1f, fill = false)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
rooms.forEachIndexed { index, name ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(14.dp))
|
||||
.background(Color.White.copy(alpha = 0.1f))
|
||||
.padding(12.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (editingIndex == index) {
|
||||
// 编辑模式 - 使用 BasicTextField 去掉内层框
|
||||
BasicTextField(
|
||||
value = editingName,
|
||||
onValueChange = { editingName = it },
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 12.dp),
|
||||
textStyle = androidx.compose.ui.text.TextStyle(
|
||||
fontSize = 15.sp,
|
||||
color = Color.White
|
||||
),
|
||||
singleLine = true,
|
||||
cursorBrush = SolidColor(Color(0xFF00D1FF))
|
||||
)
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
// 确认按钮
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(Color(0xFF00D1FF).copy(alpha = 0.2f))
|
||||
.clickable(enabled = editingName.isNotBlank()) {
|
||||
if (editingName.isNotBlank()) {
|
||||
onRenameRoom(index, editingName.trim())
|
||||
editingIndex = -1
|
||||
editingName = ""
|
||||
}
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "✓",
|
||||
color = if (editingName.isNotBlank()) Color(0xFF00D1FF) else Color(0xFF6A6A7A),
|
||||
fontSize = 16.sp
|
||||
)
|
||||
}
|
||||
|
||||
// 取消按钮
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(Color(0xFF3A3A4E))
|
||||
.clickable {
|
||||
editingIndex = -1
|
||||
editingName = ""
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "✕",
|
||||
color = Color(0xFF9AA0A6),
|
||||
fontSize = 14.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(32.dp)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(Color(0xFF00D1FF).copy(alpha = 0.2f))
|
||||
.clickable {
|
||||
editingIndex = index
|
||||
editingName = name
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "✏️",
|
||||
color = Color(0xFF00D1FF),
|
||||
fontSize = 14.sp
|
||||
)
|
||||
}
|
||||
|
||||
// 删除按钮(总览不能删除)
|
||||
if (index != 0) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(Color(0xFFFF5252).copy(alpha = 0.2f))
|
||||
.clickable { onDeleteRoom(index) },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "🗑️",
|
||||
color = Color(0xFFFF5252),
|
||||
fontSize = 14.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加新房间按钮
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(52.dp)
|
||||
.clip(RoundedCornerShape(14.dp))
|
||||
.background(Color(0xFF1A1A2E))
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = Color(0xFF3A3A4E),
|
||||
shape = RoundedCornerShape(14.dp)
|
||||
)
|
||||
.clickable { onAddRoom() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "+",
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
Text(
|
||||
text = "添加房间",
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 15.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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AddRoomDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: (String) -> Unit
|
||||
) {
|
||||
var roomName by remember { mutableStateOf("") }
|
||||
var selectedIcon by remember { mutableStateOf(0) }
|
||||
|
||||
// 房间建议
|
||||
val roomSuggestions = listOf("书房", "餐厅", "阳台", "卫生间", "储物间", "健身房", "茶室", "儿童房")
|
||||
|
||||
// 房间图标
|
||||
val roomIcons = listOf("🏠", "🛋️", "🍳", "🛏️", "🎬", "🎮", "📚", "🍽️", "🌿", "🏋️", "🍵", "🧸")
|
||||
|
||||
// 背景遮罩(点击关闭)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { androidx.compose.foundation.interaction.MutableInteractionSource() }
|
||||
) { onDismiss() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
// 对话框主体
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(400.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 { androidx.compose.foundation.interaction.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)
|
||||
)
|
||||
}
|
||||
|
||||
// 快速选择
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -46,18 +46,10 @@ fun SettingsScreen(onBack: () -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsContentList(
|
||||
rooms: List<String> = listOf("总览", "客厅", "厨房", "卧室", "影音室", "游戏房"),
|
||||
onAddRoom: (String) -> Unit = {},
|
||||
onDeleteRoom: (Int) -> Unit = {},
|
||||
onRenameRoom: (Int, String) -> Unit = { _, _ -> },
|
||||
onShowEditRoomsDialog: () -> Unit = {},
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
fun SettingsContentList() {
|
||||
val context = LocalContext.current
|
||||
val currentLang by LanguageManager.currentLanguage.collectAsState()
|
||||
var showLanguageDialog by remember { mutableStateOf(false) }
|
||||
var showBackgroundDialog by remember { mutableStateOf(false) }
|
||||
|
||||
// 语言选择对话框
|
||||
if (showLanguageDialog) {
|
||||
@ -71,12 +63,8 @@ fun SettingsContentList(
|
||||
)
|
||||
}
|
||||
|
||||
if (showBackgroundDialog) {
|
||||
BackgroundSelectorDialog(onDismiss = { showBackgroundDialog = false })
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
item { SettingsSection(title = tr("settings_account")) {
|
||||
@ -93,12 +81,6 @@ fun SettingsContentList(
|
||||
}}
|
||||
|
||||
item { SettingsSection(title = tr("settings_device")) {
|
||||
SettingsItem(
|
||||
title = "房间管理",
|
||||
subtitle = "添加、删除或重命名房间",
|
||||
onClick = { onShowEditRoomsDialog() }
|
||||
)
|
||||
|
||||
var autoConnect by remember { mutableStateOf(true) }
|
||||
SettingsSwitchItem(
|
||||
title = tr("settings_auto_connect"),
|
||||
@ -117,11 +99,7 @@ fun SettingsContentList(
|
||||
}}
|
||||
|
||||
item { SettingsSection(title = tr("settings_display")) {
|
||||
SettingsItem(
|
||||
title = "背景壁纸",
|
||||
subtitle = "选择您喜欢的背景图片",
|
||||
onClick = { showBackgroundDialog = true }
|
||||
)
|
||||
BackgroundSelector()
|
||||
|
||||
val selectedBg by BackgroundManager.selectedBackground.collectAsState()
|
||||
// 深色模式根据当前背景是否为暗色来判断
|
||||
@ -488,86 +466,28 @@ fun LogoutButton(onClick: () -> Unit) {
|
||||
|
||||
|
||||
@Composable
|
||||
fun SettingsContent(
|
||||
rooms: List<String> = listOf("总览", "客厅", "厨房", "卧室", "影音室", "游戏房"),
|
||||
onAddRoom: (String) -> Unit = {},
|
||||
onDeleteRoom: (Int) -> Unit = {},
|
||||
onRenameRoom: (Int, String) -> Unit = { _, _ -> },
|
||||
onBack: () -> Unit = {}
|
||||
) {
|
||||
var showEditRoomsDialog by remember { mutableStateOf(false) }
|
||||
var showAddRoomDialog by remember { mutableStateOf(false) }
|
||||
|
||||
fun SettingsContent() {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
// 顶部标题
|
||||
Row(
|
||||
modifier = Modifier.padding(bottom = 24.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(Color.White.copy(alpha = 0.12f))
|
||||
.clickable { onBack() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "←",
|
||||
color = Color.White,
|
||||
fontSize = 22.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = "设置",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = "设置",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.White,
|
||||
modifier = Modifier.padding(bottom = 24.dp)
|
||||
)
|
||||
|
||||
// 设置内容
|
||||
SettingsContentList(
|
||||
rooms = rooms,
|
||||
onAddRoom = onAddRoom,
|
||||
onDeleteRoom = onDeleteRoom,
|
||||
onRenameRoom = onRenameRoom,
|
||||
onShowEditRoomsDialog = { showEditRoomsDialog = true },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
// 编辑房间对话框
|
||||
if (showEditRoomsDialog) {
|
||||
EditRoomsDialog(
|
||||
rooms = rooms,
|
||||
onDismiss = { showEditRoomsDialog = false },
|
||||
onRenameRoom = onRenameRoom,
|
||||
onDeleteRoom = onDeleteRoom,
|
||||
onAddRoom = { showAddRoomDialog = true }
|
||||
)
|
||||
}
|
||||
|
||||
// 添加房间对话框
|
||||
if (showAddRoomDialog) {
|
||||
AddRoomDialog(
|
||||
onDismiss = { showAddRoomDialog = false },
|
||||
onConfirm = { roomName ->
|
||||
onAddRoom(roomName)
|
||||
showAddRoomDialog = false
|
||||
}
|
||||
)
|
||||
SettingsContentList()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BackgroundSelector(onBackgroundSelected: () -> Unit = {}) {
|
||||
fun BackgroundSelector() {
|
||||
val context = LocalContext.current
|
||||
val selectedBackground by BackgroundManager.selectedBackground.collectAsState()
|
||||
|
||||
@ -630,7 +550,6 @@ fun BackgroundSelector(onBackgroundSelected: () -> Unit = {}) {
|
||||
interactionSource = interactionSources[index]
|
||||
) {
|
||||
BackgroundManager.setBackground(context, bg.id)
|
||||
onBackgroundSelected()
|
||||
}
|
||||
) {
|
||||
Image(
|
||||
@ -680,18 +599,3 @@ fun BackgroundSelector(onBackgroundSelected: () -> Unit = {}) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BackgroundSelectorDialog(onDismiss: () -> Unit) {
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(24.dp))
|
||||
.background(Color(0xFF1E1E2E))
|
||||
.padding(20.dp)
|
||||
) {
|
||||
BackgroundSelector(onBackgroundSelected = onDismiss)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,345 +0,0 @@
|
||||
package com.example.smarthome.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
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.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
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.hazeEffect
|
||||
import dev.chrisbanes.haze.hazeSource
|
||||
|
||||
@Composable
|
||||
fun SmartHomeScreen() {
|
||||
val hazeState = HazeState()
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
Color(0xFF1A1A2E),
|
||||
Color(0xFF16213E),
|
||||
Color(0xFF0F3460)
|
||||
)
|
||||
)
|
||||
)
|
||||
.hazeSource(state = hazeState)
|
||||
) {
|
||||
// 背景装饰
|
||||
BackgroundDecoration()
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
// 左侧毛玻璃边栏
|
||||
GlassSidebarWithHaze(
|
||||
hazeState = hazeState,
|
||||
modifier = Modifier
|
||||
.width(260.dp)
|
||||
.padding(start = 24.dp, top = 24.dp, bottom = 24.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(24.dp))
|
||||
|
||||
// 主内容区域
|
||||
MainContentArea(
|
||||
hazeState = hazeState,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(end = 24.dp, top = 24.dp, bottom = 24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BackgroundDecoration() {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.SpaceEvenly,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
repeat(5) { index ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size((100 + index * 20).dp)
|
||||
.clip(RoundedCornerShape(50))
|
||||
.background(
|
||||
Brush.radialGradient(
|
||||
colors = listOf(
|
||||
Color(0xFF6C5CE7),
|
||||
Color(0xFFA29BFE),
|
||||
Color.Transparent
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GlassSidebarWithHaze(
|
||||
hazeState: HazeState,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxHeight()
|
||||
.clip(RoundedCornerShape(32.dp))
|
||||
.hazeEffect(
|
||||
state = hazeState,
|
||||
style = HazeStyle(
|
||||
blurRadius = 20.dp,
|
||||
tint = HazeTint(Color.White.copy(alpha = 0.15f)),
|
||||
noiseFactor = 0.0f
|
||||
)
|
||||
)
|
||||
.padding(32.dp)
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
text = "Lumina",
|
||||
color = Color.White,
|
||||
fontSize = 32.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Smart Home",
|
||||
color = Color.White.copy(alpha = 0.6f),
|
||||
fontSize = 14.sp
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
|
||||
listOf(
|
||||
"🏠 首页" to true,
|
||||
"🌡️ 温控" to false,
|
||||
"💡 照明" to false,
|
||||
"🔒 安全" to false,
|
||||
"🎬 场景" to false,
|
||||
"⚙️ 自动化" to false
|
||||
).forEach { (item, isSelected) ->
|
||||
Text(
|
||||
text = item,
|
||||
color = if (isSelected) Color.White else Color.White.copy(alpha = 0.6f),
|
||||
fontSize = 16.sp,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 底部用户信息
|
||||
Column {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.clip(RoundedCornerShape(20))
|
||||
.background(
|
||||
Brush.linearGradient(
|
||||
colors = listOf(
|
||||
Color(0xFF6C5CE7),
|
||||
Color(0xFFA29BFE)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
Column {
|
||||
Text(
|
||||
text = "管理员",
|
||||
color = Color.White,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
Text(
|
||||
text = "在线",
|
||||
color = Color.White.copy(alpha = 0.6f),
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MainContentArea(
|
||||
hazeState: HazeState,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp)
|
||||
) {
|
||||
// 顶部状态卡片
|
||||
StatusCard(hazeState = hazeState)
|
||||
|
||||
// 设备网格
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(2),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
items(6) { index ->
|
||||
DeviceCard(
|
||||
index = index,
|
||||
hazeState = hazeState
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatusCard(hazeState: HazeState) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(200.dp)
|
||||
.clip(RoundedCornerShape(24.dp))
|
||||
.hazeEffect(
|
||||
state = hazeState,
|
||||
style = HazeStyle(
|
||||
blurRadius = 25.dp,
|
||||
tint = HazeTint(Color.White.copy(alpha = 0.2f)),
|
||||
noiseFactor = 0.0f
|
||||
)
|
||||
)
|
||||
.padding(32.dp)
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
text = "智能家居控制中心",
|
||||
color = Color.White,
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
StatusItem("温度", "26°C")
|
||||
StatusItem("湿度", "65%")
|
||||
StatusItem("空气质量", "优")
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "6 个设备在线",
|
||||
color = Color.White.copy(alpha = 0.8f),
|
||||
fontSize = 14.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatusItem(label: String, value: String) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = value,
|
||||
color = Color.White,
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
text = label,
|
||||
color = Color.White.copy(alpha = 0.6f),
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DeviceCard(
|
||||
index: Int,
|
||||
hazeState: HazeState
|
||||
) {
|
||||
val devices = listOf(
|
||||
"客厅灯" to "💡",
|
||||
"空调" to "❄️",
|
||||
"电视" to "📺",
|
||||
"音响" to "🔊",
|
||||
"摄像头" to "📹",
|
||||
"门锁" to "🔒"
|
||||
)
|
||||
|
||||
val (name, icon) = devices[index]
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(150.dp)
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.hazeEffect(
|
||||
state = hazeState,
|
||||
style = HazeStyle(
|
||||
blurRadius = 20.dp,
|
||||
tint = HazeTint(Color.White.copy(alpha = 0.15f)),
|
||||
noiseFactor = 0.0f
|
||||
)
|
||||
)
|
||||
.padding(20.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = icon,
|
||||
fontSize = 40.sp
|
||||
)
|
||||
Text(
|
||||
text = name,
|
||||
color = Color.White,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
Text(
|
||||
text = "开启",
|
||||
color = Color.White.copy(alpha = 0.8f),
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
||||
</vector>
|
||||
@ -1,14 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<!-- 水滴 -->
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M12,2.69l5.66,5.66a8,8 0,1 1,-11.31 0z"/>
|
||||
</vector>
|
||||
@ -1,27 +1,4 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<!-- 左天线 -->
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M8,8 L5,3"/>
|
||||
<!-- 右天线 -->
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M16,8 L19,3"/>
|
||||
<!-- 电视机身 -->
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineJoin="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M4,8 L20,8 Q21,8 21,9 L21,19 Q21,20 20,20 L4,20 Q3,20 3,19 L3,9 Q3,8 4,8 Z"/>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||
<path android:strokeColor="#FFFFFFFF" android:strokeWidth="2" android:fillColor="#00000000" android:pathData="M4,6 L20,6 L20,18 L4,18 Z"/>
|
||||
<path android:strokeColor="#FFFFFFFF" android:strokeWidth="2" android:fillColor="#00000000" android:pathData="M10,10 L14,12 L10,14 Z"/>
|
||||
</vector>
|
||||
|
||||
@ -1,20 +1,25 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<!-- 锁扣(上方弧形) -->
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="48"
|
||||
android:viewportHeight="48">
|
||||
|
||||
<!-- 锁的主体 -->
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M7,10 L7,7 C7,4.24 9.24,2 12,2 C14.76,2 17,4.24 17,7 L17,10"/>
|
||||
<!-- 锁身(圆角矩形) -->
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M32,20 L30,20 L30,16 C30,12.69 27.31,10 24,10 C20.69,10 18,12.69 18,16 L18,20 L16,20 C14.9,20 14,20.9 14,22 L14,36 C14,37.1 14.9,38 16,38 L32,38 C33.1,38 34,37.1 34,36 L34,22 C34,20.9 33.1,20 32,20 Z M24,32 C22.9,32 22,31.1 22,30 C22,28.9 22.9,28 24,28 C25.1,28 26,28.9 26,30 C26,31.1 25.1,32 24,32 Z M28,20 L20,20 L20,16 C20,13.79 21.79,12 24,12 C26.21,12 28,13.79 28,16 Z"/>
|
||||
|
||||
<!-- 锁孔高光 -->
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillAlpha="0.4"
|
||||
android:pathData="M24,28 C25.1,28 26,28.9 26,30 C26,31.1 25.1,32 24,32 C22.9,32 22,31.1 22,30 C22,28.9 22.9,28 24,28 Z"/>
|
||||
|
||||
<!-- 装饰性光晕 -->
|
||||
<path
|
||||
android:strokeColor="#FFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeAlpha="0.3"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M5,10 L19,10 Q20,10 20,11 L20,21 Q20,22 19,22 L5,22 Q4,22 4,21 L4,11 Q4,10 5,10 Z"/>
|
||||
android:pathData="M24,6 C13.51,6 5,14.51 5,25 C5,35.49 13.51,44 24,44 C34.49,44 43,35.49 43,25 C43,14.51 34.49,6 24,6 Z"/>
|
||||
</vector>
|
||||
|
||||
@ -1,42 +1,35 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<!-- 游戏手柄主体 -->
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M6,11 L6,9 C6,6.79 7.79,5 10,5 L14,5 C16.21,5 18,6.79 18,9 L18,11"/>
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M4,11 L20,11 C21.1,11 22,11.9 22,13 L22,15 C22,17.21 20.21,19 18,19 L17,19 L16,21 L8,21 L7,19 L6,19 C3.79,19 2,17.21 2,15 L2,13 C2,11.9 2.9,11 4,11 Z"/>
|
||||
<!-- 左侧十字键 - 横 -->
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="1.5"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M6,15 L10,15"/>
|
||||
<!-- 左侧十字键 - 竖 -->
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="1.5"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M8,13 L8,17"/>
|
||||
<!-- 右侧按钮 -->
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="48"
|
||||
android:viewportHeight="48">
|
||||
|
||||
<!-- 游戏手柄完整主体 -->
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M16,14 C16,14.55 15.55,15 15,15 C14.45,15 14,14.55 14,14 C14,13.45 14.45,13 15,13 C15.55,13 16,13.45 16,14 Z"/>
|
||||
android:pathData="M32,14 L16,14 C11.58,14 8,17.58 8,22 L8,30 C8,33 10,36 12,38 C13,37 14,35 14,33 L14,26 C14,28.21 15.79,30 18,30 L30,30 C32.21,30 34,28.21 34,26 L34,33 C34,35 35,37 36,38 C38,36 40,33 40,30 L40,22 C40,17.58 36.42,14 32,14 Z"/>
|
||||
|
||||
<!-- 左侧十字键 -->
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M19,14 C19,14.55 18.55,15 18,15 C17.45,15 17,14.55 17,14 C17,13.45 17.45,13 18,13 C18.55,13 19,13.45 19,14 Z"/>
|
||||
android:fillColor="#000000"
|
||||
android:fillAlpha="0.3"
|
||||
android:pathData="M18,17 L20,17 L20,19 L22,19 L22,21 L20,21 L20,23 L18,23 L18,21 L16,21 L16,19 L18,19 Z"/>
|
||||
|
||||
<!-- 右侧按钮 A -->
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:fillAlpha="0.3"
|
||||
android:pathData="M32,18 C32.83,18 33.5,18.67 33.5,19.5 C33.5,20.33 32.83,21 32,21 C31.17,21 30.5,20.33 30.5,19.5 C30.5,18.67 31.17,18 32,18 Z"/>
|
||||
|
||||
<!-- 右侧按钮 B -->
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:fillAlpha="0.3"
|
||||
android:pathData="M28,18 C28.83,18 29.5,18.67 29.5,19.5 C29.5,20.33 28.83,21 28,21 C27.17,21 26.5,20.33 26.5,19.5 C26.5,18.67 27.17,18 28,18 Z"/>
|
||||
|
||||
<!-- 装饰星星 -->
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillAlpha="0.6"
|
||||
android:pathData="M24,8 L25,11 L28,11 L25.5,13 L26.5,16 L24,14 L21.5,16 L22.5,13 L20,11 L23,11 Z"/>
|
||||
</vector>
|
||||
|
||||
@ -1,28 +1,32 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<!-- 房顶 -->
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M3,10 L12,3 L21,10"/>
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="48"
|
||||
android:viewportHeight="48">
|
||||
|
||||
<!-- 房子主体 -->
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineJoin="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M5,10 L5,20 Q5,21 6,21 L18,21 Q19,21 19,20 L19,10"/>
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M24,8 L10,18 L10,38 C10,39.1 10.9,40 12,40 L20,40 L20,30 L28,30 L28,40 L36,40 C37.1,40 38,39.1 38,38 L38,18 Z"/>
|
||||
|
||||
<!-- 房顶 -->
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M24,4 L4,20 L8,20 L24,8 L40,20 L44,20 Z"/>
|
||||
|
||||
<!-- 门 -->
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineJoin="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,21 L9,15 Q9,14 10,14 L14,14 Q15,14 15,15 L15,21"/>
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillAlpha="0.3"
|
||||
android:pathData="M20,30 L28,30 L28,40 L20,40 Z"/>
|
||||
|
||||
<!-- 窗户 -->
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillAlpha="0.3"
|
||||
android:pathData="M14,22 L18,22 L18,26 L14,26 Z"/>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillAlpha="0.3"
|
||||
android:pathData="M30,22 L34,22 L34,26 L30,26 Z"/>
|
||||
</vector>
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<!-- 云 -->
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M19.35,10.04 C18.67,6.59 15.64,4 12,4 C9.11,4 6.6,5.64 5.35,8.04 C2.34,8.36 0,10.91 0,14 C0,17.31 2.69,20 6,20 L19,20 C21.76,20 24,17.76 24,15 C24,12.36 21.95,10.22 19.35,10.04 Z"/>
|
||||
</vector>
|
||||
@ -1,39 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<!-- 太阳(部分) -->
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M2,12 L4,12"/>
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M4.93,4.93 L6.34,6.34"/>
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M12,2 L12,4"/>
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19.07,4.93 L17.66,6.34"/>
|
||||
<!-- 云 -->
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M17.5,19H9a7,7 0,1 1,6.71 -9h1.79a4.5,4.5 0,1 1,0 9Z"/>
|
||||
</vector>
|
||||
@ -1,25 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<!-- 雾气线条 -->
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M3,8 L21,8"/>
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M5,12 L19,12"/>
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M3,16 L21,16"/>
|
||||
</vector>
|
||||
@ -1,29 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<!-- 云 -->
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M19,12 C19,9.24 16.76,7 14,7 C13.35,7 12.73,7.12 12.15,7.34 C11.13,5.36 9.04,4 6.67,4 C3.46,4 0.86,6.6 0.86,9.81 C0.86,9.94 0.87,10.07 0.88,10.2 C0.34,10.72 0,11.45 0,12.27 C0,13.78 1.22,15 2.73,15 L17,15 C18.1,15 19,14.1 19,13 L19,12 Z"/>
|
||||
<!-- 雨滴 -->
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M7,17 L7,21"/>
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M12,17 L12,21"/>
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M17,17 L17,21"/>
|
||||
</vector>
|
||||
@ -1,56 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<!-- 雪花 -->
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M12,2 L12,22"/>
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M2,12 L22,12"/>
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M4.93,4.93 L19.07,19.07"/>
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M4.93,19.07 L19.07,4.93"/>
|
||||
<!-- 小装饰 -->
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="1.5"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M12,6 L10,4"/>
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="1.5"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M12,6 L14,4"/>
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="1.5"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M12,18 L10,20"/>
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="1.5"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M12,18 L14,20"/>
|
||||
</vector>
|
||||
@ -1,59 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<!-- 太阳中心 -->
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M12,12m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0"/>
|
||||
<!-- 光芒 -->
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M12,2 L12,4"/>
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M12,20 L12,22"/>
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M4,12 L2,12"/>
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M22,12 L20,12"/>
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M5.64,5.64 L4.22,4.22"/>
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19.78,19.78 L18.36,18.36"/>
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M5.64,18.36 L4.22,19.78"/>
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19.78,4.22 L18.36,5.64"/>
|
||||
</vector>
|
||||
@ -1,28 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<!-- 风 -->
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M17.7,7.7A2.5,2.5 0,1 1,19 12H2"/>
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9.6,4.6A2,2 0,1 1,11 8H2"/>
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M12.6,19.4A2,2 0,1 0,14 16H2"/>
|
||||
</vector>
|
||||
|
Before Width: | Height: | Size: 5.9 MiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 392 KiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.3 MiB |
@ -1,13 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@android:color/transparent"
|
||||
android:fitsSystemWindows="false">
|
||||
|
||||
<eightbitlab.com.blurview.BlurTarget
|
||||
android:id="@+id/blurTarget"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
android:id="@+id/composeView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
</eightbitlab.com.blurview.BlurTarget>
|
||||
|
||||
<eightbitlab.com.blurview.BlurView
|
||||
android:id="@+id/blurView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="top"
|
||||
app:blurOverlayColor="@color/glassOverlay" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
@ -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_v3" android:inset="17%" />
|
||||
<inset android:drawable="@drawable/taozhi_icon_v2" android:inset="10%" />
|
||||
</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_v3" android:inset="17%" />
|
||||
<inset android:drawable="@drawable/taozhi_icon_v2" android:inset="10%" />
|
||||
</foreground>
|
||||
</adaptive-icon>
|
||||
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 96 KiB |
1
blurview/.gitignore
vendored
@ -1 +0,0 @@
|
||||
/build
|
||||
@ -1,28 +0,0 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "eightbitlab.com.blurview"
|
||||
compileSdk = 35
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 18
|
||||
targetSdk = 35
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.annotation:annotation:1.9.1")
|
||||
}
|
||||
17
blurview/proguard-rules.pro
vendored
@ -1,17 +0,0 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in C:\Java\sdk/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
@ -1,5 +0,0 @@
|
||||
<manifest package="eightbitlab.com.blurview">
|
||||
|
||||
<application/>
|
||||
|
||||
</manifest>
|
||||
@ -1,41 +0,0 @@
|
||||
package eightbitlab.com.blurview;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public interface BlurAlgorithm {
|
||||
/**
|
||||
* @param bitmap bitmap to be blurred
|
||||
* @param blurRadius blur radius
|
||||
* @return blurred bitmap
|
||||
*/
|
||||
Bitmap blur(@NonNull Bitmap bitmap, float blurRadius);
|
||||
|
||||
/**
|
||||
* Frees allocated resources
|
||||
*/
|
||||
void destroy();
|
||||
|
||||
/**
|
||||
* @return true if this algorithm returns the same instance of bitmap as it accepted
|
||||
* false if it creates a new instance.
|
||||
* <p>
|
||||
* If you return false from this method, you'll be responsible to swap bitmaps in your
|
||||
* {@link BlurAlgorithm#blur(Bitmap, float)} implementation
|
||||
* (assign input bitmap to your field and return the instance algorithm just blurred).
|
||||
*/
|
||||
boolean canModifyBitmap();
|
||||
|
||||
/**
|
||||
* Retrieve the {@link android.graphics.Bitmap.Config} on which the {@link BlurAlgorithm}
|
||||
* can actually work.
|
||||
*
|
||||
* @return bitmap config supported by the given blur algorithm.
|
||||
*/
|
||||
@NonNull
|
||||
Bitmap.Config getSupportedBitmapConfig();
|
||||
|
||||
void render(@NonNull Canvas canvas, @NonNull Bitmap bitmap);
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
package eightbitlab.com.blurview;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
|
||||
public interface BlurController extends BlurViewFacade {
|
||||
|
||||
float DEFAULT_SCALE_FACTOR = 4f;
|
||||
float DEFAULT_BLUR_RADIUS = 16f;
|
||||
|
||||
/**
|
||||
* Draws blurred content on given canvas
|
||||
*
|
||||
* @return true if BlurView should proceed with drawing itself and its children
|
||||
*/
|
||||
boolean draw(Canvas canvas);
|
||||
|
||||
/**
|
||||
* Must be used to notify Controller when BlurView's size has changed
|
||||
*/
|
||||
void updateBlurViewSize();
|
||||
|
||||
/**
|
||||
* Frees allocated resources
|
||||
*/
|
||||
void destroy();
|
||||
}
|
||||
@ -1,60 +0,0 @@
|
||||
package eightbitlab.com.blurview;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.RecordingCanvas;
|
||||
import android.graphics.RenderNode;
|
||||
import android.os.Build;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
/**
|
||||
* A FrameLayout that records a snapshot of its children on a RenderNode.
|
||||
* This snapshot is used by the BlurView to apply blur effect.
|
||||
*/
|
||||
public class BlurTarget extends FrameLayout {
|
||||
// Need both RenderNode (API 29) and RenderEffect (API 31) to be available for a full hardware rendering pipeline
|
||||
static final boolean canUseHardwareRendering = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S;
|
||||
|
||||
RenderNode renderNode;
|
||||
|
||||
{
|
||||
if (canUseHardwareRendering) {
|
||||
renderNode = new RenderNode("BlurViewHost node");
|
||||
}
|
||||
}
|
||||
|
||||
public BlurTarget(@NonNull Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public BlurTarget(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public BlurTarget(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
public BlurTarget(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void dispatchDraw(@NonNull Canvas canvas) {
|
||||
if (canUseHardwareRendering && canvas.isHardwareAccelerated()) {
|
||||
renderNode.setPosition(0, 0, getWidth(), getHeight());
|
||||
RecordingCanvas recordingCanvas = renderNode.beginRecording();
|
||||
super.dispatchDraw(recordingCanvas);
|
||||
renderNode.endRecording();
|
||||
canvas.drawRenderNode(renderNode);
|
||||
} else {
|
||||
super.dispatchDraw(canvas);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,198 +0,0 @@
|
||||
package eightbitlab.com.blurview;
|
||||
|
||||
import static eightbitlab.com.blurview.BlurController.DEFAULT_SCALE_FACTOR;
|
||||
import static eightbitlab.com.blurview.PreDrawBlurController.TRANSPARENT;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import eightbitlab.com.blurview.R;
|
||||
|
||||
/**
|
||||
* FrameLayout that blurs its underlying content.
|
||||
* Can have children and draw them over blurred background.
|
||||
*/
|
||||
public class BlurView extends FrameLayout {
|
||||
|
||||
BlurController blurController = new NoOpController();
|
||||
|
||||
@ColorInt
|
||||
private int overlayColor;
|
||||
private boolean blurAutoUpdate = true;
|
||||
|
||||
public BlurView(Context context) {
|
||||
super(context);
|
||||
init(null, 0);
|
||||
}
|
||||
|
||||
public BlurView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(attrs, 0);
|
||||
}
|
||||
|
||||
public BlurView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init(attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
private void init(AttributeSet attrs, int defStyleAttr) {
|
||||
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.BlurView, defStyleAttr, 0);
|
||||
overlayColor = a.getColor(R.styleable.BlurView_blurOverlayColor, TRANSPARENT);
|
||||
a.recycle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas) {
|
||||
boolean shouldDraw = blurController.draw(canvas);
|
||||
if (shouldDraw) {
|
||||
super.draw(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
blurController.updateBlurViewSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
blurController.setBlurAutoUpdate(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
if (!isHardwareAccelerated()) {
|
||||
Log.e("BlurView", "BlurView can't be used in not hardware-accelerated window!");
|
||||
} else {
|
||||
blurController.setBlurAutoUpdate(this.blurAutoUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param target the root to start blur from.
|
||||
* @param algorithm sets the blur algorithm. Ignored on API >= 31 where efficient hardware rendering pipeline is used.
|
||||
* @param scaleFactor a scale factor to downscale the view snapshot before blurring.
|
||||
* Helps achieving stronger blur and potentially better performance at the expense of blur precision.
|
||||
* The blur radius is essentially the radius * scaleFactor.
|
||||
* @param applyNoise optional blue noise texture over the blurred content to make it look more natural. True by default.
|
||||
* @return {@link BlurView} to setup needed params.
|
||||
*/
|
||||
public BlurViewFacade setupWith(@NonNull BlurTarget target, BlurAlgorithm algorithm, float scaleFactor, boolean applyNoise) {
|
||||
blurController.destroy();
|
||||
if (BlurTarget.canUseHardwareRendering) {
|
||||
// Ignores the blur algorithm, always uses RenderEffect
|
||||
blurController = new RenderNodeBlurController(this, target, overlayColor, scaleFactor, applyNoise);
|
||||
} else {
|
||||
blurController = new PreDrawBlurController(this, target, overlayColor, algorithm, scaleFactor, applyNoise);
|
||||
}
|
||||
|
||||
return blurController;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param rootView the root to start blur from.
|
||||
* BlurAlgorithm is automatically picked based on the API version.
|
||||
* It uses RenderEffect on API 31+, and RenderScriptBlur on older versions.
|
||||
* @param scaleFactor a scale factor to downscale the view snapshot before blurring.
|
||||
* Helps achieving stronger blur and potentially better performance at the expense of blur precision.
|
||||
* The blur radius is essentially the radius * scaleFactor.
|
||||
* @param applyNoise optional blue noise texture over the blurred content to make it look more natural. True by default.
|
||||
* @return {@link BlurView} to setup needed params.
|
||||
*/
|
||||
public BlurViewFacade setupWith(@NonNull BlurTarget rootView, float scaleFactor, boolean applyNoise) {
|
||||
BlurAlgorithm algorithm;
|
||||
if (BlurTarget.canUseHardwareRendering) {
|
||||
// Ignores the blur algorithm, always uses RenderNodeBlurController and RenderEffect
|
||||
algorithm = null;
|
||||
} else {
|
||||
algorithm = new RenderScriptBlur(getContext());
|
||||
}
|
||||
return setupWith(rootView, algorithm, scaleFactor, applyNoise);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param rootView root to start blur from.
|
||||
* BlurAlgorithm is automatically picked based on the API version.
|
||||
* It uses RenderEffect on API 31+, and RenderScriptBlur on older versions.
|
||||
* The {@link DEFAULT_SCALE_FACTOR} scale factor for view snapshot is used.
|
||||
* Blue noise texture is applied by default.
|
||||
* @return {@link BlurView} to setup needed params.
|
||||
*/
|
||||
public BlurViewFacade setupWith(@NonNull BlurTarget rootView) {
|
||||
return setupWith(rootView, DEFAULT_SCALE_FACTOR, true);
|
||||
}
|
||||
|
||||
// Setters duplicated to be able to conveniently change these settings outside of setupWith chain
|
||||
|
||||
/**
|
||||
* @see BlurViewFacade#setBlurRadius(float)
|
||||
*/
|
||||
public BlurViewFacade setBlurRadius(float radius) {
|
||||
return blurController.setBlurRadius(radius);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see BlurViewFacade#setOverlayColor(int)
|
||||
*/
|
||||
public BlurViewFacade setOverlayColor(@ColorInt int overlayColor) {
|
||||
this.overlayColor = overlayColor;
|
||||
return blurController.setOverlayColor(overlayColor);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see BlurViewFacade#setBlurAutoUpdate(boolean)
|
||||
*/
|
||||
public BlurViewFacade setBlurAutoUpdate(boolean enabled) {
|
||||
this.blurAutoUpdate = enabled;
|
||||
return blurController.setBlurAutoUpdate(enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see BlurViewFacade#setBlurEnabled(boolean)
|
||||
*/
|
||||
public BlurViewFacade setBlurEnabled(boolean enabled) {
|
||||
return blurController.setBlurEnabled(enabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRotation(float rotation) {
|
||||
super.setRotation(rotation);
|
||||
notifyRotationChanged(rotation);
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
public void notifyRotationChanged(float rotation) {
|
||||
if (usingRenderNode()) {
|
||||
((RenderNodeBlurController) blurController).updateRotation(rotation);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
public void notifyScaleXChanged(float scaleX) {
|
||||
if (usingRenderNode()) {
|
||||
((RenderNodeBlurController) blurController).updateScaleX(scaleX);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
public void notifyScaleYChanged(float scaleY) {
|
||||
if (usingRenderNode()) {
|
||||
((RenderNodeBlurController) blurController).updateScaleY(scaleY);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean usingRenderNode() {
|
||||
return blurController instanceof RenderNodeBlurController;
|
||||
}
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
package eightbitlab.com.blurview;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
// Serves purely as a marker of a Canvas used in BlurView
|
||||
// to skip drawing itself and other BlurViews on the View hierarchy snapshot
|
||||
public class BlurViewCanvas extends Canvas {
|
||||
public BlurViewCanvas(@NonNull Bitmap bitmap) {
|
||||
super(bitmap);
|
||||
}
|
||||
}
|
||||
@ -1,48 +0,0 @@
|
||||
package eightbitlab.com.blurview;
|
||||
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public interface BlurViewFacade {
|
||||
|
||||
/**
|
||||
* Enables/disables the blur. Enabled by default
|
||||
*
|
||||
* @param enabled true to enable, false otherwise
|
||||
* @return {@link BlurViewFacade}
|
||||
*/
|
||||
BlurViewFacade setBlurEnabled(boolean enabled);
|
||||
|
||||
/**
|
||||
* Can be used to stop blur auto update or resume if it was stopped before.
|
||||
* Enabled by default.
|
||||
*
|
||||
* @return {@link BlurViewFacade}
|
||||
*/
|
||||
BlurViewFacade setBlurAutoUpdate(boolean enabled);
|
||||
|
||||
/**
|
||||
* @param frameClearDrawable sets the drawable to draw before view hierarchy.
|
||||
* Can be used to draw Activity's window background if your root layout doesn't provide any background
|
||||
* Optional, by default frame is cleared with a transparent color.
|
||||
* @return {@link BlurViewFacade}
|
||||
*/
|
||||
BlurViewFacade setFrameClearDrawable(@Nullable Drawable frameClearDrawable);
|
||||
|
||||
/**
|
||||
* @param radius sets the blur radius. The real blur radius is radius * scaleFactor.
|
||||
* Default value is {@link BlurController#DEFAULT_BLUR_RADIUS}
|
||||
* @return {@link BlurViewFacade}
|
||||
*/
|
||||
BlurViewFacade setBlurRadius(float radius);
|
||||
|
||||
/**
|
||||
* Sets the color overlay to be drawn on top of blurred content
|
||||
*
|
||||
* @param overlayColor int color
|
||||
* @return {@link BlurViewFacade}
|
||||
*/
|
||||
BlurViewFacade setOverlayColor(@ColorInt int overlayColor);
|
||||
}
|
||||
@ -1,47 +0,0 @@
|
||||
package eightbitlab.com.blurview;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
// Used in edit mode and in case if no BlurController was set
|
||||
public class NoOpController implements BlurController {
|
||||
@Override
|
||||
public boolean draw(Canvas canvas) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBlurViewSize() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlurViewFacade setBlurRadius(float radius) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlurViewFacade setOverlayColor(int overlayColor) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlurViewFacade setFrameClearDrawable(@Nullable Drawable windowBackground) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlurViewFacade setBlurEnabled(boolean enabled) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlurViewFacade setBlurAutoUpdate(boolean enabled) {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
package eightbitlab.com.blurview;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.BitmapShader;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffXfermode;
|
||||
import android.graphics.Shader;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
class Noise {
|
||||
private static Paint noisePaint;
|
||||
|
||||
static void apply(Canvas canvas, Context context, int width, int height) {
|
||||
initPaint(context);
|
||||
canvas.drawRect(0, 0, width, height, noisePaint);
|
||||
}
|
||||
|
||||
private static void initPaint(Context context) {
|
||||
if (noisePaint == null) {
|
||||
Bitmap alphaBitmap = getNoiseBitmap(context);
|
||||
noisePaint = new Paint();
|
||||
noisePaint.setAntiAlias(true);
|
||||
noisePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP));
|
||||
noisePaint.setShader(new BitmapShader(alphaBitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT));
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static Bitmap getNoiseBitmap(Context context) {
|
||||
Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.blue_noise);
|
||||
Bitmap alphaBitmap = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(alphaBitmap);
|
||||
Paint paint = new Paint();
|
||||
paint.setAlpha(38); // 15% opacity
|
||||
canvas.drawBitmap(bitmap, 0, 0, paint);
|
||||
return alphaBitmap;
|
||||
}
|
||||
}
|
||||
@ -1,258 +0,0 @@
|
||||
package eightbitlab.com.blurview;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Blur Controller that handles all blur logic for the attached View.
|
||||
* It honors View size changes, View animation and Visibility changes.
|
||||
* <p>
|
||||
* The basic idea is to draw the view hierarchy on a bitmap, excluding the attached View,
|
||||
* then blur and draw it on the system Canvas.
|
||||
* <p>
|
||||
* It uses {@link ViewTreeObserver.OnPreDrawListener} to detect when
|
||||
* blur should be updated.
|
||||
* <p>
|
||||
*/
|
||||
public final class PreDrawBlurController implements BlurController {
|
||||
|
||||
@ColorInt
|
||||
public static final int TRANSPARENT = 0;
|
||||
|
||||
private float blurRadius = DEFAULT_BLUR_RADIUS;
|
||||
|
||||
private final BlurAlgorithm blurAlgorithm;
|
||||
private final float scaleFactor;
|
||||
private final boolean applyNoise;
|
||||
private BlurViewCanvas internalCanvas;
|
||||
private Bitmap internalBitmap;
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
final View blurView;
|
||||
private int overlayColor;
|
||||
private final ViewGroup rootView;
|
||||
private final int[] rootLocation = new int[2];
|
||||
private final int[] blurViewLocation = new int[2];
|
||||
|
||||
private final ViewTreeObserver.OnPreDrawListener drawListener = new ViewTreeObserver.OnPreDrawListener() {
|
||||
@Override
|
||||
public boolean onPreDraw() {
|
||||
// Not invalidating a View here, just updating the Bitmap.
|
||||
// This relies on the HW accelerated bitmap drawing behavior in Android
|
||||
// If the bitmap was drawn on HW accelerated canvas, it holds a reference to it and on next
|
||||
// drawing pass the updated content of the bitmap will be rendered on the screen
|
||||
updateBlur();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
private boolean blurEnabled = true;
|
||||
private boolean initialized;
|
||||
|
||||
@Nullable
|
||||
private Drawable frameClearDrawable;
|
||||
|
||||
/**
|
||||
* @param blurView View which will draw it's blurred underlying content
|
||||
* @param rootView Root View where blurView's underlying content starts drawing.
|
||||
* Can be Activity's root content layout (android.R.id.content)
|
||||
* @param algorithm sets the blur algorithm
|
||||
* @param scaleFactor a scale factor to downscale the view snapshot before blurring.
|
||||
* Helps achieving stronger blur and potentially better performance at the expense of blur precision.
|
||||
* @param applyNoise optional blue noise texture over the blurred content to make it look more natural. True by default.
|
||||
*/
|
||||
public PreDrawBlurController(@NonNull View blurView,
|
||||
@NonNull ViewGroup rootView,
|
||||
@ColorInt int overlayColor,
|
||||
BlurAlgorithm algorithm,
|
||||
float scaleFactor,
|
||||
boolean applyNoise) {
|
||||
this.rootView = rootView;
|
||||
this.blurView = blurView;
|
||||
this.overlayColor = overlayColor;
|
||||
this.blurAlgorithm = algorithm;
|
||||
this.scaleFactor = scaleFactor;
|
||||
this.applyNoise = applyNoise;
|
||||
|
||||
int measuredWidth = blurView.getMeasuredWidth();
|
||||
int measuredHeight = blurView.getMeasuredHeight();
|
||||
|
||||
init(measuredWidth, measuredHeight);
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
void init(int measuredWidth, int measuredHeight) {
|
||||
setBlurAutoUpdate(true);
|
||||
SizeScaler sizeScaler = new SizeScaler(scaleFactor);
|
||||
if (sizeScaler.isZeroSized(measuredWidth, measuredHeight)) {
|
||||
// Will be initialized later when the View reports a size change
|
||||
blurView.setWillNotDraw(true);
|
||||
return;
|
||||
}
|
||||
|
||||
blurView.setWillNotDraw(false);
|
||||
SizeScaler.Size bitmapSize = sizeScaler.scale(measuredWidth, measuredHeight);
|
||||
internalBitmap = Bitmap.createBitmap(bitmapSize.width, bitmapSize.height, blurAlgorithm.getSupportedBitmapConfig());
|
||||
internalCanvas = new BlurViewCanvas(internalBitmap);
|
||||
initialized = true;
|
||||
// Usually it's not needed, because `onPreDraw` updates the blur anyway.
|
||||
// But it handles cases when the PreDraw listener is attached to a different Window, for example
|
||||
// when the BlurView is in a Dialog window, but the root is in the Activity.
|
||||
// Previously it was done in `draw`, but it was causing potential side effects and Jetpack Compose crashes
|
||||
updateBlur();
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
void updateBlur() {
|
||||
if (!blurEnabled || !initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (frameClearDrawable == null) {
|
||||
internalBitmap.eraseColor(Color.TRANSPARENT);
|
||||
} else {
|
||||
frameClearDrawable.draw(internalCanvas);
|
||||
}
|
||||
|
||||
internalCanvas.save();
|
||||
setupInternalCanvasMatrix();
|
||||
try {
|
||||
rootView.draw(internalCanvas);
|
||||
} catch (Exception e) {
|
||||
// Can potentially fail on rendering Hardware Bitmaps or something like that
|
||||
Log.e("BlurView", "Error during snapshot capturing", e);
|
||||
}
|
||||
internalCanvas.restore();
|
||||
|
||||
blurAndSave();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up matrix to draw starting from blurView's position
|
||||
*/
|
||||
private void setupInternalCanvasMatrix() {
|
||||
rootView.getLocationOnScreen(rootLocation);
|
||||
blurView.getLocationOnScreen(blurViewLocation);
|
||||
|
||||
int left = blurViewLocation[0] - rootLocation[0];
|
||||
int top = blurViewLocation[1] - rootLocation[1];
|
||||
|
||||
// https://github.com/Dimezis/BlurView/issues/128
|
||||
float scaleFactorH = (float) blurView.getHeight() / internalBitmap.getHeight();
|
||||
float scaleFactorW = (float) blurView.getWidth() / internalBitmap.getWidth();
|
||||
|
||||
float scaledLeftPosition = -left / scaleFactorW;
|
||||
float scaledTopPosition = -top / scaleFactorH;
|
||||
|
||||
internalCanvas.translate(scaledLeftPosition, scaledTopPosition);
|
||||
internalCanvas.scale(1 / scaleFactorW, 1 / scaleFactorH);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean draw(Canvas canvas) {
|
||||
if (!blurEnabled || !initialized) {
|
||||
return true;
|
||||
}
|
||||
// Not blurring itself or other BlurViews to not cause recursive draw calls
|
||||
// Related: https://github.com/Dimezis/BlurView/issues/110
|
||||
if (canvas instanceof BlurViewCanvas) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// https://github.com/Dimezis/BlurView/issues/128
|
||||
float scaleFactorH = (float) blurView.getHeight() / internalBitmap.getHeight();
|
||||
float scaleFactorW = (float) blurView.getWidth() / internalBitmap.getWidth();
|
||||
|
||||
canvas.save();
|
||||
// Don't draw outside of the BlurView bounds if parent has clipChildren = false
|
||||
canvas.clipRect(0f, 0f, blurView.getWidth(), blurView.getHeight());
|
||||
canvas.save();
|
||||
canvas.scale(scaleFactorW, scaleFactorH);
|
||||
blurAlgorithm.render(canvas, internalBitmap);
|
||||
// restore scale so we don't upscale the noise texture
|
||||
canvas.restore();
|
||||
if (applyNoise) {
|
||||
Noise.apply(canvas, blurView.getContext(), blurView.getWidth(), blurView.getHeight());
|
||||
}
|
||||
if (overlayColor != TRANSPARENT) {
|
||||
canvas.drawColor(overlayColor);
|
||||
}
|
||||
// restore clip rect
|
||||
canvas.restore();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void blurAndSave() {
|
||||
internalBitmap = blurAlgorithm.blur(internalBitmap, blurRadius);
|
||||
if (!blurAlgorithm.canModifyBitmap()) {
|
||||
internalCanvas.setBitmap(internalBitmap);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBlurViewSize() {
|
||||
int measuredWidth = blurView.getMeasuredWidth();
|
||||
int measuredHeight = blurView.getMeasuredHeight();
|
||||
|
||||
init(measuredWidth, measuredHeight);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
setBlurAutoUpdate(false);
|
||||
blurAlgorithm.destroy();
|
||||
initialized = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlurViewFacade setBlurRadius(float radius) {
|
||||
this.blurRadius = radius;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlurViewFacade setFrameClearDrawable(@Nullable Drawable frameClearDrawable) {
|
||||
this.frameClearDrawable = frameClearDrawable;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlurViewFacade setBlurEnabled(boolean enabled) {
|
||||
this.blurEnabled = enabled;
|
||||
setBlurAutoUpdate(enabled);
|
||||
blurView.invalidate();
|
||||
return this;
|
||||
}
|
||||
|
||||
public BlurViewFacade setBlurAutoUpdate(final boolean enabled) {
|
||||
rootView.getViewTreeObserver().removeOnPreDrawListener(drawListener);
|
||||
blurView.getViewTreeObserver().removeOnPreDrawListener(drawListener);
|
||||
if (enabled) {
|
||||
rootView.getViewTreeObserver().addOnPreDrawListener(drawListener);
|
||||
// Track changes in the blurView window too, for example if it's in a bottom sheet dialog
|
||||
if (rootView.getWindowId() != blurView.getWindowId()) {
|
||||
blurView.getViewTreeObserver().addOnPreDrawListener(drawListener);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlurViewFacade setOverlayColor(int overlayColor) {
|
||||
if (this.overlayColor != overlayColor) {
|
||||
this.overlayColor = overlayColor;
|
||||
blurView.invalidate();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@ -1,270 +0,0 @@
|
||||
package eightbitlab.com.blurview;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.RecordingCanvas;
|
||||
import android.graphics.RenderEffect;
|
||||
import android.graphics.RenderNode;
|
||||
import android.graphics.Shader;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import android.view.ViewTreeObserver;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import eightbitlab.com.blurview.SizeScaler.Size;
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.S)
|
||||
public class RenderNodeBlurController implements BlurController {
|
||||
private final int[] targetLocation = new int[2];
|
||||
private final int[] blurViewLocation = new int[2];
|
||||
|
||||
private final BlurView blurView;
|
||||
private final BlurTarget target;
|
||||
private final RenderNode blurNode = new RenderNode("BlurView node");
|
||||
private final float scaleFactor;
|
||||
private final boolean applyNoise;
|
||||
|
||||
private Drawable frameClearDrawable;
|
||||
private int overlayColor;
|
||||
private float blurRadius = 1f;
|
||||
private boolean enabled = true;
|
||||
|
||||
// Potentially cached stuff from the slow software path
|
||||
@Nullable
|
||||
private Bitmap cachedBitmap;
|
||||
@Nullable
|
||||
private RenderScriptBlur fallbackBlur;
|
||||
|
||||
// This tracks BlurView location in scrollable containers, during animations, etc.
|
||||
private final ViewTreeObserver.OnPreDrawListener drawListener = () -> {
|
||||
saveOnScreenLocation();
|
||||
updateRenderNodeProperties();
|
||||
return true;
|
||||
};
|
||||
|
||||
public RenderNodeBlurController(@NonNull BlurView blurView, @NonNull BlurTarget target, int overlayColor, float scaleFactor, boolean applyNoise) {
|
||||
this.blurView = blurView;
|
||||
this.overlayColor = overlayColor;
|
||||
this.target = target;
|
||||
this.scaleFactor = scaleFactor;
|
||||
this.applyNoise = applyNoise;
|
||||
blurView.setWillNotDraw(false);
|
||||
blurView.getViewTreeObserver().addOnPreDrawListener(drawListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean draw(Canvas canvas) {
|
||||
if (!enabled) {
|
||||
return true;
|
||||
}
|
||||
saveOnScreenLocation();
|
||||
|
||||
if (canvas.isHardwareAccelerated()) {
|
||||
hardwarePath(canvas);
|
||||
} else {
|
||||
// Rendering on a software canvas.
|
||||
// Presumably this is something taking a programmatic screenshot,
|
||||
// or maybe a software-based View/Fragment transition.
|
||||
// This is slow and shouldn't be a common case for this controller.
|
||||
softwarePath(canvas);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Not doing any scaleFactor-related manipulations here, because RenderEffect blur internally
|
||||
// already scales down the snapshot depending on the blur radius.
|
||||
// https://cs.android.com/android/platform/superproject/main/+/main:external/skia/src/core/SkImageFilterTypes.cpp;drc=61197364367c9e404c7da6900658f1b16c42d0da;l=2103
|
||||
// https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/libs/hwui/jni/RenderEffect.cpp;l=39;drc=61197364367c9e404c7da6900658f1b16c42d0da?q=nativeCreateBlurEffect&ss=android%2Fplatform%2Fsuperproject%2Fmain
|
||||
private void hardwarePath(Canvas canvas) {
|
||||
// TODO would be good to keep it the size of the BlurView instead of the target, but then the animation
|
||||
// like translation and rotation would go out of bounds. Not sure if there's a good fix for this
|
||||
blurNode.setPosition(0, 0, target.getWidth(), target.getHeight());
|
||||
updateRenderNodeProperties();
|
||||
|
||||
drawSnapshot();
|
||||
|
||||
canvas.save();
|
||||
// Don't draw outside of the BlurView bounds if parent has clipChildren = false
|
||||
canvas.clipRect(0f, 0f, blurView.getWidth(), blurView.getHeight());
|
||||
// Draw on the system canvas
|
||||
canvas.drawRenderNode(blurNode);
|
||||
if (applyNoise) {
|
||||
Noise.apply(canvas, blurView.getContext(), blurView.getWidth(), blurView.getHeight());
|
||||
}
|
||||
if (overlayColor != Color.TRANSPARENT) {
|
||||
canvas.drawColor(overlayColor);
|
||||
}
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
private void updateRenderNodeProperties() {
|
||||
float layoutTranslationX = -getLeft();
|
||||
float layoutTranslationY = -getTop();
|
||||
|
||||
// Pivot point for the rotation and scale (in case it's applied)
|
||||
blurNode.setPivotX(blurView.getWidth() / 2f - layoutTranslationX);
|
||||
blurNode.setPivotY(blurView.getHeight() / 2f - layoutTranslationY);
|
||||
blurNode.setTranslationX(layoutTranslationX);
|
||||
blurNode.setTranslationY(layoutTranslationY);
|
||||
|
||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.S) {
|
||||
// There's a bug on API 31 - blurNode doesn't get re-rendered on setting new translation/scale/rotation,
|
||||
// so we need to re-apply the blur effect to trigger a redraw.
|
||||
applyBlur();
|
||||
}
|
||||
}
|
||||
|
||||
private void drawSnapshot() {
|
||||
RecordingCanvas recordingCanvas = blurNode.beginRecording();
|
||||
if (frameClearDrawable != null) {
|
||||
frameClearDrawable.draw(recordingCanvas);
|
||||
}
|
||||
recordingCanvas.drawRenderNode(target.renderNode);
|
||||
// Looks like the order of this doesn't matter
|
||||
applyBlur();
|
||||
blurNode.endRecording();
|
||||
}
|
||||
|
||||
private void softwarePath(Canvas canvas) {
|
||||
SizeScaler sizeScaler = new SizeScaler(scaleFactor);
|
||||
Size original = new Size(blurView.getWidth(), blurView.getHeight());
|
||||
Size scaled = sizeScaler.scale(original);
|
||||
if (cachedBitmap == null || cachedBitmap.getWidth() != scaled.width || cachedBitmap.getHeight() != scaled.height) {
|
||||
cachedBitmap = Bitmap.createBitmap(scaled.width, scaled.height, Bitmap.Config.ARGB_8888);
|
||||
}
|
||||
Canvas softwareCanvas = new Canvas(cachedBitmap);
|
||||
|
||||
softwareCanvas.save();
|
||||
setupCanvasMatrix(softwareCanvas, original, scaled);
|
||||
if (frameClearDrawable != null) {
|
||||
frameClearDrawable.draw(canvas);
|
||||
}
|
||||
try {
|
||||
target.draw(softwareCanvas);
|
||||
} catch (Exception e) {
|
||||
// Can potentially fail on rendering Hardware Bitmaps or something like that
|
||||
Log.e("BlurView", "Error during snapshot capturing", e);
|
||||
}
|
||||
softwareCanvas.restore();
|
||||
|
||||
if (fallbackBlur == null) {
|
||||
fallbackBlur = new RenderScriptBlur(blurView.getContext());
|
||||
}
|
||||
fallbackBlur.blur(cachedBitmap, blurRadius);
|
||||
canvas.save();
|
||||
canvas.scale((float) original.width / scaled.width, (float) original.height / scaled.height);
|
||||
fallbackBlur.render(canvas, cachedBitmap);
|
||||
canvas.restore();
|
||||
if (applyNoise) {
|
||||
Noise.apply(canvas, blurView.getContext(), blurView.getWidth(), blurView.getHeight());
|
||||
}
|
||||
if (overlayColor != Color.TRANSPARENT) {
|
||||
canvas.drawColor(overlayColor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up matrix to draw starting from blurView's position
|
||||
*/
|
||||
private void setupCanvasMatrix(Canvas canvas, Size targetSize, Size scaledSize) {
|
||||
// https://github.com/Dimezis/BlurView/issues/128
|
||||
float scaleFactorH = (float) targetSize.height / scaledSize.height;
|
||||
float scaleFactorW = (float) targetSize.width / scaledSize.width;
|
||||
|
||||
float scaledLeftPosition = -getLeft() / scaleFactorW;
|
||||
float scaledTopPosition = -getTop() / scaleFactorH;
|
||||
|
||||
canvas.translate(scaledLeftPosition, scaledTopPosition);
|
||||
canvas.scale(1 / scaleFactorW, 1 / scaleFactorH);
|
||||
}
|
||||
|
||||
private int getTop() {
|
||||
return blurViewLocation[1] - targetLocation[1];
|
||||
}
|
||||
|
||||
private int getLeft() {
|
||||
return blurViewLocation[0] - targetLocation[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBlurViewSize() {
|
||||
// No-op, the size is updated in draw method, it's cheap and not called frequently
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
blurNode.discardDisplayList();
|
||||
if (fallbackBlur != null) {
|
||||
fallbackBlur.destroy();
|
||||
fallbackBlur = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlurViewFacade setBlurEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
blurView.invalidate();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlurViewFacade setBlurAutoUpdate(boolean enabled) {
|
||||
blurView.getViewTreeObserver().removeOnPreDrawListener(drawListener);
|
||||
if (enabled) {
|
||||
blurView.getViewTreeObserver().addOnPreDrawListener(drawListener);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlurViewFacade setFrameClearDrawable(@Nullable Drawable frameClearDrawable) {
|
||||
this.frameClearDrawable = frameClearDrawable;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlurViewFacade setBlurRadius(float radius) {
|
||||
this.blurRadius = radius;
|
||||
applyBlur();
|
||||
return this;
|
||||
}
|
||||
|
||||
private void applyBlur() {
|
||||
// scaleFactor is only used to increase the blur radius
|
||||
// because RenderEffect already scales down the snapshot when needed.
|
||||
float realBlurRadius = blurRadius * scaleFactor;
|
||||
RenderEffect blur = RenderEffect.createBlurEffect(realBlurRadius, realBlurRadius, Shader.TileMode.CLAMP);
|
||||
blurNode.setRenderEffect(blur);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlurViewFacade setOverlayColor(int overlayColor) {
|
||||
if (this.overlayColor != overlayColor) {
|
||||
this.overlayColor = overlayColor;
|
||||
blurView.invalidate();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
void updateRotation(float rotation) {
|
||||
blurNode.setRotationZ(-rotation);
|
||||
}
|
||||
|
||||
public void updateScaleX(float scaleX) {
|
||||
blurNode.setScaleX(1 / scaleX);
|
||||
}
|
||||
|
||||
public void updateScaleY(float scaleY) {
|
||||
blurNode.setScaleY(1 / scaleY);
|
||||
}
|
||||
|
||||
private void saveOnScreenLocation() {
|
||||
target.getLocationOnScreen(targetLocation);
|
||||
blurView.getLocationOnScreen(blurViewLocation);
|
||||
}
|
||||
}
|
||||
@ -1,105 +0,0 @@
|
||||
package eightbitlab.com.blurview;
|
||||
|
||||
import static java.lang.Math.min;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.renderscript.Allocation;
|
||||
import android.renderscript.Element;
|
||||
import android.renderscript.RenderScript;
|
||||
import android.renderscript.ScriptIntrinsicBlur;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* Blur using RenderScript, processed on GPU when device drivers support it.
|
||||
* Requires API 17+
|
||||
*
|
||||
* @deprecated because RenderScript is deprecated and its hardware acceleration is not guaranteed.
|
||||
* On API 31+ an alternative hardware accelerated blur implementation is automatically used.
|
||||
*/
|
||||
@Deprecated
|
||||
public class RenderScriptBlur implements BlurAlgorithm {
|
||||
private final Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG);
|
||||
private final RenderScript renderScript;
|
||||
private final ScriptIntrinsicBlur blurScript;
|
||||
private Allocation outAllocation;
|
||||
|
||||
private int lastBitmapWidth = -1;
|
||||
private int lastBitmapHeight = -1;
|
||||
|
||||
/**
|
||||
* @param context Context to create the {@link RenderScript}
|
||||
*/
|
||||
public RenderScriptBlur(@NonNull Context context) {
|
||||
renderScript = RenderScript.create(context);
|
||||
blurScript = ScriptIntrinsicBlur.create(renderScript, Element.U8_4(renderScript));
|
||||
}
|
||||
|
||||
private boolean canReuseAllocation(@NonNull Bitmap bitmap) {
|
||||
return bitmap.getHeight() == lastBitmapHeight && bitmap.getWidth() == lastBitmapWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bitmap bitmap to blur
|
||||
* @param blurRadius blur radius (1..25)
|
||||
* @return blurred bitmap
|
||||
*/
|
||||
@Override
|
||||
public Bitmap blur(@NonNull Bitmap bitmap, float blurRadius) {
|
||||
try {
|
||||
//Allocation will use the same backing array of pixels as bitmap if created with USAGE_SHARED flag
|
||||
Allocation inAllocation = Allocation.createFromBitmap(renderScript, bitmap);
|
||||
|
||||
if (!canReuseAllocation(bitmap)) {
|
||||
if (outAllocation != null) {
|
||||
outAllocation.destroy();
|
||||
}
|
||||
outAllocation = Allocation.createTyped(renderScript, inAllocation.getType());
|
||||
lastBitmapWidth = bitmap.getWidth();
|
||||
lastBitmapHeight = bitmap.getHeight();
|
||||
}
|
||||
|
||||
blurScript.setRadius(min(blurRadius, 25f));
|
||||
blurScript.setInput(inAllocation);
|
||||
//do not use inAllocation in forEach. it will cause visual artifacts on blurred Bitmap
|
||||
blurScript.forEach(outAllocation);
|
||||
outAllocation.copyTo(bitmap);
|
||||
|
||||
inAllocation.destroy();
|
||||
} catch (Exception e) {
|
||||
// Can potentially crash because RenderScript context was released by someone else via RenderScript.releaseAllContexts()
|
||||
// Some Glide transformations can cause this.
|
||||
Log.e("BlurView", "RenderScript blur failed. Rendering unblurred snapshot", e);
|
||||
}
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void destroy() {
|
||||
blurScript.destroy();
|
||||
renderScript.destroy();
|
||||
if (outAllocation != null) {
|
||||
outAllocation.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canModifyBitmap() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Bitmap.Config getSupportedBitmapConfig() {
|
||||
return Bitmap.Config.ARGB_8888;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(@NonNull Canvas canvas, @NonNull Bitmap bitmap) {
|
||||
canvas.drawBitmap(bitmap, 0f, 0f, paint);
|
||||
}
|
||||
}
|
||||
@ -1,93 +0,0 @@
|
||||
package eightbitlab.com.blurview;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Scales width and height by [scaleFactor],
|
||||
* and then rounds the size proportionally so the width is divisible by [ROUNDING_VALUE]
|
||||
*/
|
||||
public class SizeScaler {
|
||||
|
||||
// Bitmap size should be divisible by ROUNDING_VALUE to meet stride requirement.
|
||||
// This will help avoiding an extra bitmap allocation when passing the bitmap to RenderScript for blur.
|
||||
// Usually it's 16, but on Samsung devices it's 64 for some reason.
|
||||
private static final int ROUNDING_VALUE = 64;
|
||||
private final float scaleFactor;
|
||||
private final boolean noStrideAlignment;
|
||||
|
||||
public SizeScaler(float scaleFactor) {
|
||||
this(scaleFactor, false);
|
||||
}
|
||||
|
||||
public SizeScaler(float scaleFactor, boolean noStrideAlignment) {
|
||||
this.scaleFactor = scaleFactor;
|
||||
this.noStrideAlignment = noStrideAlignment;
|
||||
}
|
||||
|
||||
Size scale(int width, int height) {
|
||||
int nonRoundedScaledWidth = downscaleSize(width);
|
||||
int scaledWidth = roundSize(nonRoundedScaledWidth);
|
||||
//Only width has to be aligned to ROUNDING_VALUE
|
||||
float roundingScaleFactor = (float) width / scaledWidth;
|
||||
//Ceiling because rounding or flooring might leave empty space on the View's bottom
|
||||
int scaledHeight = (int) Math.ceil(height / roundingScaleFactor);
|
||||
|
||||
return new Size(scaledWidth, scaledHeight);
|
||||
}
|
||||
|
||||
Size scale(Size size) {
|
||||
return scale(size.width, size.height);
|
||||
}
|
||||
|
||||
boolean isZeroSized(int measuredWidth, int measuredHeight) {
|
||||
return downscaleSize(measuredHeight) == 0 || downscaleSize(measuredWidth) == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rounds a value to the nearest divisible by {@link #ROUNDING_VALUE} to meet stride requirement
|
||||
*/
|
||||
private int roundSize(int value) {
|
||||
if (noStrideAlignment) {
|
||||
return value;
|
||||
}
|
||||
if (value % ROUNDING_VALUE == 0) {
|
||||
return value;
|
||||
}
|
||||
return value - (value % ROUNDING_VALUE) + ROUNDING_VALUE;
|
||||
}
|
||||
|
||||
private int downscaleSize(float value) {
|
||||
return (int) Math.ceil(value / scaleFactor);
|
||||
}
|
||||
|
||||
static class Size {
|
||||
|
||||
final int width;
|
||||
final int height;
|
||||
|
||||
Size(int width, int height) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Size size = (Size) o;
|
||||
return width == size.width && height == size.height;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Size{" +
|
||||
"width=" + width +
|
||||
", height=" + height +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 6.2 KiB |
@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<declare-styleable name="BlurView">
|
||||
<attr name="blurOverlayColor" format="color"/>
|
||||
</declare-styleable>
|
||||
</resources>
|
||||
@ -1,61 +0,0 @@
|
||||
package eightbitlab.com.blurview;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.params.provider.Arguments.of;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import eightbitlab.com.blurview.SizeScaler.Size;
|
||||
|
||||
class SizeScalerTest {
|
||||
private static final float scalingFactor = 8f;
|
||||
|
||||
private final SizeScaler scaler = new SizeScaler(scalingFactor);
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("scalingResults")
|
||||
void scales_and_returns_proper_size_and_scale_factor(int x, int y, Size expected) {
|
||||
Size result = scaler.scale(x, y);
|
||||
assertEquals(expected, result);
|
||||
}
|
||||
|
||||
// In case if rounding mode for downscaleSize() will be changed
|
||||
@ParameterizedTest
|
||||
@CsvSource({"0,0,true", "1,1,false", "8,8,false", "0,100,true"})
|
||||
void isZeroSized(int x, int y, boolean isZeroSized) {
|
||||
assertEquals(isZeroSized, scaler.isZeroSized(x, y));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static Stream<Arguments> scalingResults() {
|
||||
return Stream.of(
|
||||
of(64, 64, size(64, 64)),
|
||||
// min size is 64
|
||||
of(7, 7, size(64, 64)),
|
||||
of(128, 128, size(64, 64)),
|
||||
of(1024, 1024, size(128, 128)),
|
||||
// if Y is not divisible by 64 but X is, don't align Y
|
||||
of(1024, 256, size(128, 32)),
|
||||
of(1000, 256, size(128, 33)),
|
||||
// scale Y by the same factor as X
|
||||
of(900, 256, size(128, 37)),
|
||||
of(900, 200, size(128, 29)),
|
||||
of(907, 203, size(128, 29)),
|
||||
of(1080, 104, size(192, 19)),
|
||||
of(1080, 192, size(192, 35)),
|
||||
of(1080, 1149, size(192, 205))
|
||||
);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static Size size(int x, int y) {
|
||||
return new Size(x, y);
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,6 @@
|
||||
plugins {
|
||||
id("com.android.application") version "8.9.1" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
|
||||
id("org.jetbrains.kotlin.plugin.compose") version "2.1.0" apply false
|
||||
id("com.android.application") version "8.6.0" apply false
|
||||
id("org.jetbrains.kotlin.android") version "1.9.24" apply false
|
||||
}
|
||||
|
||||
tasks.register("clean", Delete::class) {
|
||||
|
||||
@ -1,108 +0,0 @@
|
||||
# Android项目全局上下文文档
|
||||
## 1. 项目概况
|
||||
语言:Kotlin、少量 Java 本地库代码
|
||||
架构:Jetpack Compose 单 Activity UI,未形成标准 MVVM;当前主要通过 Composable 局部状态、StateFlow 全局 Manager、SharedPreferences 持久化驱动界面
|
||||
技术栈:AndroidX、Jetpack Compose、Material3、Haze 毛玻璃、OkHttp、Gson、ZXing、Google Play Services Location、LeakCanary、Android Instrumentation Test
|
||||
Gradle/SDK版本:Android Gradle Plugin 8.9.1,Kotlin 2.1.0,Compose BOM 2024.10.00,compileSdk 36,targetSdk 36,minSdk 33,Java/Kotlin JVM 1.8
|
||||
项目作用:智能家居控制面板应用,提供登录、房间管理、设备控制展示、场景、自动化、设置、背景壁纸、多语言、天气展示,并预留 CAN/MQTT 设备柜通信能力
|
||||
模块:settings.gradle.kts 当前仅 include :app;仓库内存在 blurview Android Library 源码和 test/BlurView-master 示例/测试目录,但未接入当前主工程构建
|
||||
|
||||
## 2. 包结构与模块职责
|
||||
com.example.smarthome:应用入口层
|
||||
包含 SmartHomeApp、MainActivity、MainActivityCompose、MainActivityHaze、MainActivityAlternative。生产入口是 MainActivity,负责初始化语言/用户管理器、隐藏系统栏、挂载 activity_main.xml 中的 ComposeView 并渲染 AppRoot。
|
||||
|
||||
com.example.smarthome.data:数据、状态和服务层
|
||||
UserManager 管理登录状态、用户信息、手机号验证码模拟登录和用户资料持久化。
|
||||
LanguageManager 管理多语言枚举、翻译字典、语言持久化和切换语言后的应用重启。
|
||||
BackgroundManager 管理背景壁纸列表、当前背景 StateFlow 和持久化。
|
||||
WeatherService 使用定位、HTTP 天气接口、缓存和模拟兜底获取天气。
|
||||
DeviceManager 聚合 MQTT 与 CAN 服务,向 UI 暴露设备控制统一入口。
|
||||
MqttService.kt 定义 MQTT 常量、实体、接口和模拟实现。
|
||||
CanService.kt 定义 CAN 常量、命令、实体、接口和模拟实现。
|
||||
|
||||
com.example.smarthome.ui:Compose UI 层
|
||||
MainScaffold 是主界面核心,承载控制台、房间内容、设备卡片、空调/灯光/图表、顶部栏、导航相关组件。
|
||||
LoginScreen、PhoneLoginScreen 提供微信扫码模拟登录和手机号验证码登录。
|
||||
SettingsScreen 提供设置、语言、背景、房间管理、退出登录。
|
||||
RoomDialog 提供添加/编辑/删除房间弹窗。
|
||||
SceneScreen 管理场景开关。
|
||||
AutomationScreen 管理自动化列表增删改状态。
|
||||
SecurityScreen 管理安防状态展示与本地开关。
|
||||
StatisticsScreen 展示静态能耗统计。
|
||||
SmartHomeScreen、HomeScreen、Neumorph*、LiquidIndicator 是备用/实验性界面或通用视觉组件。
|
||||
Previews 提供多尺寸 Compose 预览。
|
||||
|
||||
com.example.smarthome.ui.theme:主题层
|
||||
SmartHomeTheme 使用 Material3 darkColorScheme,当前为固定深色主题。
|
||||
|
||||
eightbitlab.com.blurview:本地 BlurView 库源码
|
||||
包含 BlurView、BlurController、PreDrawBlurController、RenderScriptBlur、RenderNodeBlurController 等传统 View 毛玻璃实现;当前未被 :app 依赖。
|
||||
|
||||
## 3. 分层与数据流
|
||||
启动流程:MainActivity.onCreate -> LanguageManager.init(context) -> UserManager.init(context) -> 隐藏状态栏/导航栏 -> ComposeView.setContent -> SmartHomeTheme -> AppRoot。
|
||||
|
||||
登录数据流:AppRoot collect UserManager.isLoggedIn。未登录显示 LoginScreen;微信模拟登录调用 UserManager.login,手机号登录调用 UserManager.sendVerifyCode、verifyCode、loginWithPhone;登录成功后 UserManager.isLoggedIn 变为 true,自动切换到 MainContent。
|
||||
|
||||
主界面数据流:MainContent 从 smart_home_prefs.rooms 加载房间列表,使用 Compose remember/mutableStateOf 保存 selectedRoom、selectedNavItem、rooms,通过回调传给 MainScaffold。房间增删改由 saveRooms 写入 SharedPreferences 后更新 Compose 状态。
|
||||
|
||||
导航数据流:MainScaffold 根据 selectedNavItem 分发页面:0 控制台、1 场景、2 自动化、3 设置。SecurityScreen、StatisticsScreen 和部分导航组件已存在,但当前主 switch 未接入对应页面。
|
||||
|
||||
控制台数据流:DashboardContent -> RoomSelector -> RoomContent。总览房间显示环境、安全健康、模式、灯光、所有设备;具体房间显示空调卡、状态图、灯光行、设备网格。多数设备/图表数据为 UI 局部状态或静态模拟数据。
|
||||
|
||||
设置数据流:SettingsContent -> SettingsContentList。语言通过 LanguageManager.currentLanguage collect,切换语言写入 language_prefs 并重启应用;背景通过 BackgroundManager.selectedBackground collect,切换背景写入 background_prefs;退出登录调用 UserManager.logout。
|
||||
|
||||
场景/自动化/安防数据流:SceneScreen 使用 SharedPreferences("scenes") 保存场景激活状态;AutomationScreen 使用 SharedPreferences("automations").automation_list 和 Gson 保存自动化列表;SecurityScreen 使用 smart_home_prefs.security_armed 保存安防开关。
|
||||
|
||||
设备通信数据流:UI 可通过 rememberDeviceManager() 获取 DeviceManager。DeviceManager.initialize 打开 CAN 串口,并在 MQTT 参数完整时初始化/连接 MQTT;业务方法调用 ICanService 发送开门、上电、LED、查询等命令;CAN 回调进入 handleCanResponse,必要时触发后续 CAN 查询或 MQTT 上报。当前 CAN/MQTT 均是模拟实现,真实客户端代码以 TODO 注释保留。
|
||||
|
||||
线程规则:Compose 状态更新主要在主线程组合环境内完成;网络天气请求在 WeatherService.getWeather 中通过 withContext(Dispatchers.IO) 执行;DeviceManager 使用 CoroutineScope(SupervisorJob() + Dispatchers.IO) 初始化 CAN/MQTT;CAN/MQTT 的 MutableStateFlow 可跨线程更新,但新增真实硬件/网络回调时需注意线程切换和生命周期释放。
|
||||
|
||||
## 4. 核心基类
|
||||
当前项目没有自定义 BaseActivity、BaseFragment、BaseViewModel、Repository 基类。
|
||||
Activity 基类使用系统/AndroidX 类:MainActivity、MainActivityCompose、MainActivityHaze 继承 ComponentActivity;MainActivityAlternative 继承 AppCompatActivity。
|
||||
全局状态封装以 singleton object/class manager 为主:UserManager、LanguageManager、BackgroundManager、WeatherService、DeviceManager、MqttServiceImpl、CanServiceImpl。
|
||||
|
||||
## 5. 现有通用封装
|
||||
网络:WeatherService 使用 OkHttp 访问 7timer、wttr.in、ip-api;LoginScreen 使用 HttpURLConnection 预留微信扫码登录接口;MqttServiceImpl 预留真实 MQTT 客户端接入点但当前只模拟连接和发布日志。
|
||||
本地存储:广泛使用 SharedPreferences,包括 user_prefs、users_database、language_prefs、background_prefs、smart_home_prefs、scenes、automations、mqtt_prefs。
|
||||
状态封装:使用 MutableStateFlow/StateFlow 暴露登录、用户、语言、背景、MQTT 连接、CAN 连接、最后 CAN 响应、当前设备操作等状态。
|
||||
工具方法:tr(key) 多语言快捷函数;rememberSelectedBackground()、rememberDeviceManager()、rememberMqttConnectionState()、rememberCanConnectionState() Compose 便捷函数;Context.getMqttService()、Context.getCanService() 扩展;generateQRCode 使用 ZXing 生成二维码;Context.findActivity() 用于从 Context 查找 Activity。
|
||||
UI 通用组件:BackgroundSelector、LanguageSelectDialog、EditRoomsDialog、AddRoomDialog、CustomSwitch、GradientButton、CustomSlider、LightSlider、BlurGlassCard、FloatingBottomNav、SideNavRail、NeumorphButton、LiquidIndicator 等。
|
||||
|
||||
## 6. 开发约束
|
||||
- 最小改动,禁止随意重构稳定代码
|
||||
- 遵守分层架构,不跨层调用
|
||||
- 优先复用现有代码,不重复造轮子
|
||||
- 沿用项目现有第三方方案
|
||||
- 当前没有标准 ViewModel/Repository 层;新增功能应优先沿用现有 Manager + StateFlow + Compose 状态方式,除非明确要求架构升级
|
||||
- 修改房间、登录、语言、背景等功能时,应复用现有 SharedPreferences key 和 Manager,不要引入新的重复存储
|
||||
- 修改设备通信时,应通过 DeviceManager、IMqttService、ICanService 接口扩展,不要让 UI 直接拼 CAN 帧或 MQTT topic
|
||||
- 修改 UI 时优先复用 MainScaffold.kt、SettingsScreen.kt、RoomDialog.kt 中已有组件和视觉风格
|
||||
- 注意 SecurityScreen、StatisticsScreen 当前未接入生产导航 switch,接入前需确认导航索引设计
|
||||
- 注意 UiAdaptationTest 引用了当前源码中不存在的 ControlPanel,测试维护前需先确认目标组件
|
||||
|
||||
## 7. 核心实体与全局常量
|
||||
核心实体:
|
||||
UserManager.UserInfo:用户 ID、手机号、昵称、头像、注册/登录时间、性别、生日、邮箱、地址。
|
||||
BackgroundOption:背景 id、名称、资源、缩略图、描述、是否深色。
|
||||
WeatherInfo:温度、天气、湿度、空气质量、AQI、城市。
|
||||
DoorInfo:格口/柜子/设备状态、接口、电压、SN、PVD、PRO、MFG、电量、维修状态。
|
||||
DeviceEvent:借还/丢失等设备事件上报字段。
|
||||
CanMessage、CanResponse:CAN 命令和响应数据。
|
||||
Scene:场景 id、名称、描述、图标、渐变、激活状态。
|
||||
Automation:自动化 id、名称、触发条件、动作、启用状态。
|
||||
SecurityEvent:安全事件标题、时间、类型、描述。
|
||||
Device:UI 设备卡片名称、副标题、图标、开关状态。
|
||||
ScanStatusResult、ScanStatus:扫码登录状态。
|
||||
|
||||
全局常量:
|
||||
LoginConfig:BASE_URL、GET_QR_CODE、CHECK_STATUS、POLL_INTERVAL、QR_EXPIRE_TIME、USE_MOCK。
|
||||
MqttConstants:MQTT host、API host、端口、发布/订阅 topic 前后缀、QoS。
|
||||
CanConstants:默认串口 /dev/ttyS4、波特率 115200、CAN 帧头/长度/帧尾、门开关状态。
|
||||
CanCommands:LON、LOF、LST、PON、POF、RON、GON、AOF、PVD、MFG、PRO、DSN、CON、1VER、1LEN、1MD5、1MDE。
|
||||
OperationType:借、还、维修、批量出入柜、格口上断电、灯光、设备信息、固件版本、开门、异常检测等操作码。
|
||||
LanguageManager.Language:中文、英文、韩文、日文、俄文。
|
||||
Mode:HOME、AWAY、FUN、MOVIE。
|
||||
|
||||
## 8. 协作约定
|
||||
生成本文档后,后续我只提供待修改局部代码,不再发送完整工程。所有修改基于这份上下文;信息不足、架构冲突请主动提问,不要自行猜测。回复仅输出改动代码+简短说明。
|
||||
|
Before Width: | Height: | Size: 757 KiB |
@ -16,5 +16,5 @@ dependencyResolutionManagement {
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "智能家居"
|
||||
rootProject.name = "SmartHome"
|
||||
include(":app")
|
||||
|
||||
BIN
shushi.png
|
Before Width: | Height: | Size: 1.4 MiB |
@ -1,13 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"skills": {
|
||||
"luckincoffee-mycoffeeskill": {
|
||||
"name": "luckincoffee-mycoffeeskill",
|
||||
"version": "1.0.0",
|
||||
"source": "redskill",
|
||||
"install_dir": "/Users/zangyimeng/Documents/韬智/智能家居/skills/luckincoffee-mycoffeeskill",
|
||||
"installed_at": "1783930343714",
|
||||
"sha256": "737fd6b36c4d1d31dec2b9da07cd357ef6efe921f92131d59b6231324052c996"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
{
|
||||
"mark_version": 1,
|
||||
"identifier": "luckincoffee-mycoffeeskill",
|
||||
"version": "1.0.0",
|
||||
"sha256": "737fd6b36c4d1d31dec2b9da07cd357ef6efe921f92131d59b6231324052c996",
|
||||
"installed_at": "1783930343713"
|
||||
}
|
||||
@ -1,94 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
## V0.8.0 - 2026-05-29
|
||||
|
||||
- 固化 Skill 强约束
|
||||
|
||||
## V0.7.3 - 2026-05-29
|
||||
|
||||
- 增加LICENSE声明
|
||||
|
||||
## V0.7.2 - 2026-05-28
|
||||
|
||||
- 统一金额展示为带¥格式
|
||||
|
||||
## V0.7.1 - 2026-05-28
|
||||
|
||||
- 明确取餐信息展示条件,收敛 token 提示话术。
|
||||
- 统一下单规则,避免未支付订单暴露取餐信息,减少重复追问。
|
||||
- 查询订单仅在已支付且返回取餐信息时,展示取餐码与预计时间。
|
||||
- 新增 token 保存固定询问话术,先确认保存意愿再推进后续流程。
|
||||
- 统一 MCP 未启用时提示话术,强调未获同意前禁止落盘 token。
|
||||
|
||||
|
||||
## V0.7.0 - 2026-05-28
|
||||
|
||||
- 完善 token 持久化约束并补充支付后引导话术
|
||||
|
||||
## V0.6.5 - 2026-05-26
|
||||
|
||||
- 明确支付二维码同步展示支付链接
|
||||
|
||||
## V0.6.4 - 2026-05-25
|
||||
|
||||
- 明确咖啡下单支付与 token 使用规则
|
||||
|
||||
## V0.6.3 - 2026-05-24
|
||||
|
||||
- 明确支付二维码按渠道图片能力展示
|
||||
|
||||
## V0.6.2 - 2026-05-24
|
||||
|
||||
- 更新包README为最新信息
|
||||
|
||||
## V0.6.1 - 2026-05-24
|
||||
|
||||
- 更新构 建脚本
|
||||
|
||||
## V0.6.0 - 2026-05-24
|
||||
|
||||
- 补充 my-coffee MCP HTTP 兜底调用说明
|
||||
|
||||
## V0.5.1 - 2026-05-24
|
||||
|
||||
- 更新README
|
||||
|
||||
## V0.5.0 - 2026-05-24
|
||||
|
||||
- 规范咖啡点单定位策略与下单校验逻辑
|
||||
|
||||
## V0.4.0 - 2026-05-21
|
||||
|
||||
- 明确点单门店确认与预览后建单规则
|
||||
|
||||
## V0.3.1 - 2026-05-20
|
||||
|
||||
- 补充商品属性识别规则
|
||||
|
||||
## V0.3.0 - 2026-05-17
|
||||
|
||||
- 明确下单前确认、预览订单和优惠券传递要求,降低按原价下单风险
|
||||
- 增加基于网络出口 IP 获取近似经纬度的定位策略
|
||||
- 要求创建订单前展示门店、商品和预览价格并等待用户确认
|
||||
- 要求 previewOrder 返回 couponCodeList 时 createOrder 必须透传
|
||||
- 规范订单创建后展示原价、减免金额、应付金额和二维码图片
|
||||
|
||||
## V0.2.0 - 2026-05-14
|
||||
|
||||
- 切换下单助手 MCP 生产配置
|
||||
|
||||
## V0.1.0 - 2026-05-13
|
||||
|
||||
- 明确咖啡下单技能预览与下单约束
|
||||
|
||||
## V0.0.3 - 2026-04-22
|
||||
|
||||
- 验证 npm script 参数透传
|
||||
|
||||
## V0.0.2 - 2026-04-22
|
||||
|
||||
- 补充自动发版流程
|
||||
|
||||
## V0.0.1 - 2026-04-22
|
||||
|
||||
- Initial release
|
||||
@ -1,327 +0,0 @@
|
||||
---
|
||||
name: my-coffee
|
||||
description: Use when users ask to order Luckin Coffee, search Luckin stores/products, query pickup code/order status, cancel a Luckin order, or mention 瑞幸、luckin、咖啡、点单、下单、门店、取餐码.
|
||||
keywords:
|
||||
- 瑞幸
|
||||
- luckin
|
||||
- 咖啡
|
||||
- 点单
|
||||
- 下单
|
||||
- 门店
|
||||
- 取餐码
|
||||
- 订单状态
|
||||
- 取消订单
|
||||
packageType: instruction-skill
|
||||
instructionOnly: true
|
||||
metadata:
|
||||
version: 0.8.0
|
||||
openclaw:
|
||||
requiredMcp:
|
||||
- my-coffee
|
||||
requiresNetwork: true
|
||||
dataClassification: payment-order
|
||||
---
|
||||
|
||||
# My Coffee 瑞幸咖啡下单助手
|
||||
|
||||
## 前置条件
|
||||
|
||||
**必需 MCP Server**: `my-coffee`
|
||||
|
||||
优先使用名为 `my-coffee` 的 MCP server;若当前智能体暴露的是同一瑞幸订单 MCP 的其它别名,以实际可用 server 名为准。
|
||||
|
||||
**MCP 配置**:
|
||||
|
||||
```json
|
||||
{
|
||||
"my-coffee": {
|
||||
"type": "streamableHttp",
|
||||
"url": "https://gwmcp.lkcoffee.com/order/user/mcp",
|
||||
"headers": {
|
||||
"Authorization": "Bearer ${LUCKIN_MCP_TOKEN}"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**安全说明**:
|
||||
|
||||
- `LUCKIN_MCP_TOKEN` 读取优先级:环境变量 `LUCKIN_MCP_TOKEN` > 当前对话用户明确提供的 token > 本地文件 `~/.my-coffee/LUCKIN_MCP_TOKEN`(仅在用户明确同意记录后可使用)。
|
||||
- 如果用户在当前或历史消息里发过完整 token,应先尝试该 token,不要直接让用户重新登录平台获取。
|
||||
- 用户发送 token 时必须先询问是否记录到 `~/.my-coffee/LUCKIN_MCP_TOKEN` 供后续对话复用;只有用户明确同意才可写入,禁止静默保存。
|
||||
- 写入 token 前确保目录存在(`mkdir -p ~/.my-coffee`);写入后建议限制权限(如 `chmod 600 ~/.my-coffee/LUCKIN_MCP_TOKEN`)。
|
||||
- 用户要求撤销保存时,删除本地 token 文件 `~/.my-coffee/LUCKIN_MCP_TOKEN`,并明确告知“后续将不再从本地文件复用 token”。
|
||||
- 除非用户明确要求 MCP 配置,否则不要输出 Authorization token。
|
||||
- 真实 MCP 请求必须使用完整 token:优先 `Authorization: Bearer ${LUCKIN_MCP_TOKEN}`;若用户已提供 token,则使用用户提供的完整原文。
|
||||
- 禁止执行 `Bearer ***`、`Bearer xxx…yyy`、`Bearer <token>` 等占位 Authorization;如果没有环境变量且用户也没提供 token,只提示用户配置或提供 `LUCKIN_MCP_TOKEN` 后重试。
|
||||
- 首次调用某 MCP 工具前,或不确定参数时,先读取对应工具 descriptor/schema;本文参数只是快速参考,实际以 schema 为准。
|
||||
- 创建订单可能生成真实支付二维码,回复只保留必要的订单和支付信息。
|
||||
|
||||
## MCP 调用模式
|
||||
|
||||
优先调用当前智能体已配置的 `my-coffee` MCP 工具。若当前智能体没有配置 MCP server,但可拿到有效 token(环境变量 / 用户在对话中提供 / 本地文件 `~/.my-coffee/LUCKIN_MCP_TOKEN`),使用 `curl` 调用 MCP HTTP 接口。
|
||||
|
||||
建议先组装 token:
|
||||
|
||||
```bash
|
||||
TOKEN="${LUCKIN_MCP_TOKEN:-}"
|
||||
if [ -z "$TOKEN" ] && [ -f ~/.my-coffee/LUCKIN_MCP_TOKEN ]; then
|
||||
TOKEN="$(cat ~/.my-coffee/LUCKIN_MCP_TOKEN)"
|
||||
fi
|
||||
```
|
||||
|
||||
查看工具列表和 schema:
|
||||
|
||||
```bash
|
||||
curl -s -N "${LUCKIN_MCP_URL:-https://gwmcp.lkcoffee.com/order/user/mcp}" \
|
||||
-H "Authorization: Bearer ${TOKEN:-$LUCKIN_MCP_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Accept: application/json, text/event-stream" \
|
||||
-d '{"jsonrpc":"2.0","method":"tools/list","params":{},"id":1}'
|
||||
```
|
||||
|
||||
调用工具:
|
||||
|
||||
```bash
|
||||
curl -s -N "${LUCKIN_MCP_URL:-https://gwmcp.lkcoffee.com/order/user/mcp}" \
|
||||
-H "Authorization: Bearer ${TOKEN:-$LUCKIN_MCP_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Accept: application/json, text/event-stream" \
|
||||
-d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"TOOL_NAME","arguments":{}},"id":1}'
|
||||
```
|
||||
|
||||
将 `TOOL_NAME` 替换为实际工具名,将 `arguments` 替换为工具 schema 要求的参数。从 `result.content[0].text`、`result.structuredContent` 或 SSE `data:` 事件中解析返回结果。
|
||||
|
||||
## 执行优先级(严格约束,单一真源)
|
||||
|
||||
以下为强约束,优先级高于其余章节;如有重复描述,以本节为准:
|
||||
|
||||
1. **Schema 优先**:首次调用工具前或参数不确定时,必须先读取工具 descriptor/schema。
|
||||
2. **Token 生命周期**:
|
||||
- 读取优先级:环境变量 `LUCKIN_MCP_TOKEN` > 当前/历史对话中用户明确提供的完整 token > 本地文件 `~/.my-coffee/LUCKIN_MCP_TOKEN`。
|
||||
- 用户发来 token 后,先询问是否保存到本地;未获明确同意前禁止写入本地文件。
|
||||
- 允许用户在同一条回复中同时给出“保存/不保存 + 后续操作”(如“2,继续下单”);确认保存选择后可直接进入后续流程,避免额外往返。
|
||||
- 用户要求撤销保存时,必须先做二次确认;用户确认后再删除本地 token 文件并回告。
|
||||
- 真实 MCP 调用必须使用完整 token,禁止占位或脱敏 token。
|
||||
3. **下单顺序强约束**:`确认门店` -> `确认商品与下单意图` -> `previewOrder` ->(满足价格与明细校验)-> `createOrder`;不得跳步。
|
||||
4. **优惠券强约束**:`previewOrder` 返回 `couponCodeList` 非空时,`createOrder` 必须原样透传。
|
||||
5. **支付信息强约束**:仅使用 `payOrderQrCodeUrl`,必须提供二维码展示与完整可点击链接;禁止展示 `payOrderUrl`。
|
||||
6. **未支付信息约束**:未完成支付前,不告知取餐码/可取餐/预计取餐等信息。
|
||||
7. **定位与距离约束**:仅在用户明确提供准确经纬度时展示距离;IP 粗定位场景不展示距离。
|
||||
8. **缺参追问约束**:门店/商品未命中或参数不足时,只追问一个必要信息,避免并发追问。
|
||||
9. **调用隐身约束**:工具调用过程仅内部执行,禁止向用户展示工具名、调用标题、命令(如 `curl`)、请求参数、原始 JSON/SSE 返回、日志或报错堆栈;对外只输出必要业务结果与下一步引导。
|
||||
|
||||
## 核心能力
|
||||
|
||||
1. **查询门店** - 按门店名和经纬度查找瑞幸门店。
|
||||
2. **搜索商品** - 将用户输入如“拿铁”匹配到可售商品和 SKU。
|
||||
3. **自提下单** - 使用 `deptId`、`productId`、`skuCode` 和数量创建自提订单。
|
||||
4. **支付二维码** - 返回支付链接;有 `payOrderQrCodeUrl` 时用 Markdown 图片展示。
|
||||
5. **订单查询** - 查询订单状态、取餐码、门店信息和商品信息。
|
||||
6. **取消订单** - 通过 `orderId` 取消订单。
|
||||
|
||||
## 定位策略
|
||||
|
||||
当需要经纬度且用户未提供明确地址/坐标时,优先追问用户所在位置、商圈、门店名或经纬度。
|
||||
|
||||
只有确认当前智能体运行在用户本机或可信本地环境时,才可通过当前网络出口 IP 获取近似位置:
|
||||
|
||||
```bash
|
||||
curl -s https://ipinfo.io/json
|
||||
```
|
||||
|
||||
读取返回 JSON 的 `loc` 字段,格式为 `"纬度,经度"`,解析为 `latitude` 和 `longitude`。这是城市/区域级粗略定位,只用于附近门店查询;如果运行环境不确定、接口失败、`loc` 为空,或用户要求精确定位,只追问用户提供地址或经纬度。未使用用户明确提供的准确经纬度时,不展示门店距离。
|
||||
|
||||
## 下单流程
|
||||
|
||||
### 模式 1:快速自提下单
|
||||
|
||||
**触发语句**: “帮我在瑞幸下单”、“在某门店点一杯”、“AI点单专用,拿铁”、“买一杯咖啡”
|
||||
|
||||
**流程**:
|
||||
|
||||
1. **查询门店** - 调用 `queryShopList`。
|
||||
- 必填:`longitude`、`latitude`。
|
||||
- 可选:`deptName`。
|
||||
- 优先使用用户提供的经纬度;未提供时按“定位策略”处理。
|
||||
- 默认列出本次查询返回的前 5 个门店,包含门店名称、地址和营业时间,并说明可继续查看;只有用户明确要求时再展示更多门店;只有用户明确提供准确经纬度时才展示距离。
|
||||
|
||||
2. **确认门店** - 搜索商品前必须先让用户确认自提门店。
|
||||
- 询问用户是否选择列出的某一家门店。
|
||||
- 如果没有用户需要的门店,引导用户说出所处位置、商圈或想去的门店名,再重新调用 `queryShopList`。
|
||||
- 用户确认后保存 `deptId`、门店精确坐标、`deptName` 和地址。
|
||||
|
||||
3. **搜索商品** - 调用 `searchProductForMcp`。
|
||||
- 必填:`deptId`、`query`。
|
||||
- 除非用户要求外送,否则使用 `delivery="pick"`。
|
||||
- 如果用户输入较宽泛,优先选择工具返回中最贴近的商品;如果结果明显歧义且用户没有授权直接选择,先让用户确认。
|
||||
- 识别用户话术里的杯型、温度、糖度、奶基等商品属性;只要用户提出定制项,必须先调用 `queryProductDetailInfo` 查看可选属性,再用 `switchProduct` 切换到目标 SKU,不能仅凭搜索结果猜 SKU。
|
||||
|
||||
4. **确认下单意图** - 创建订单前只做一次用户确认。
|
||||
- 展示门店名称、地址、营业时间、商品名、规格、数量和搜索返回的预估价;只有用户明确提供准确经纬度时才展示距离。
|
||||
- 明确说明:确认后会先调用 `previewOrder` 获取最终价格和优惠;若最终应付金额不高于预估价、商品明细一致且优惠券信息正常,则直接调用 `createOrder` 生成支付二维码。
|
||||
- 只有用户明确回复确认后,才能继续调用 `previewOrder`。
|
||||
|
||||
5. **预览订单** - 用户确认后必须调用 `previewOrder`,不得直接 `createOrder`。
|
||||
- 用于获取原价、减免金额、最终价格和 `couponCodeList`。
|
||||
- 必填:`deptId`、`productList`。
|
||||
- `amount`、`productId`、`skuCode` 来自商品搜索结果。
|
||||
- `totalInitialPrice` 是订单原价,`privilegeMoney` 是减免金额,`discountPrice` 是最终应付金额。
|
||||
- 预览返回 `couponCodeList` 非空时,创建订单必须原样传给 `createOrder`。
|
||||
- 比较价格前先确认工具返回单位;单位不明或字段缺失时停止并向用户确认,不要自行换算。
|
||||
- 如果最终应付金额不高于预估价、商品明细一致且优惠券信息正常,不要再次询问用户,直接进入 `createOrder`。
|
||||
- 只有最终价格高于预估价、商品明细与用户确认的不一致、优惠券信息异常或工具返回不完整时,才停止并再次请用户确认。
|
||||
|
||||
6. **创建订单** - 调用 `createOrder`。
|
||||
- **执行此步骤前必须已完成 `previewOrder`**
|
||||
- 必填:`deptId`、`productList`、`longitude`、`latitude`。
|
||||
- `couponCodeList` 来自 `previewOrder`,有则必传。
|
||||
- 使用 `queryShopList` 返回的门店坐标。
|
||||
- 默认:`delivery="pick"`。
|
||||
- 仅使用 `payOrderQrCodeUrl` 作为支付链接,不展示 `payOrderUrl`。
|
||||
- 文字回复展示:订单号、门店名称、商品名、数量、原价(来自 `previewOrder.totalInitialPrice`,展示为 `¥金额`)、减免金额(来自 `previewOrder.privilegeMoney`,展示为 `¥金额`)、应付金额(优先使用 `createOrder.discountPrice`,缺失时用 `previewOrder.discountPrice`,展示为 `¥金额`)
|
||||
- 未完成支付前,不告知用户取餐码、可取餐、预计取餐等取餐信息。
|
||||
- 支付二维码展示规则:优先使用当前 channel 的原生图片能力发送(如飞书/微信用 `message` 工具的 `media` 参数传入完整的 `payOrderQrCodeUrl`);若当前环境为纯文本界面(如 Cursor、终端、不支持 media 参数的 channel),使用 Markdown 图片语法 ``。同步展示支付链接 `[打开支付二维码](完整payOrderQrCodeUrl)`,保证二维码未展示时用户仍可点击链接支付。URL 必须原样完整保留,不得用 `…` 省略或截断,链接地址部分必须是完整 URL。
|
||||
- 创建订单成功并展示支付信息后,固定追加:`支付完成后告诉我一声,我可以马上帮你查询订单状态和取餐码。` 并提供两个固定回复(带序号):`1. 已支付,帮我查取餐码`、`2. 还没支付,稍后再查`。
|
||||
|
||||
### 模式 2:查询订单
|
||||
|
||||
**触发语句**: “查订单”、“订单状态”、“取餐码”、“做好了吗”
|
||||
|
||||
**流程**:
|
||||
|
||||
1. 优先使用当前对话里最近的 `orderId`,没有则询问用户。
|
||||
2. 调用 `queryOrderDetailInfo`。
|
||||
3. 展示状态、门店、商品和支付金额(金额前加 `¥`);仅当订单已支付且返回取餐码/取餐状态时,再展示取餐码和预计时间。
|
||||
|
||||
### 模式 3:取消订单
|
||||
|
||||
**触发语句**: “取消订单”、“帮我退掉”、“不要了”
|
||||
|
||||
**流程**:
|
||||
|
||||
1. 优先使用当前对话里最近的 `orderId`,没有则询问用户。
|
||||
2. 调用 `cancelOrder`。
|
||||
3. 简短确认取消结果。
|
||||
|
||||
## 工具参考
|
||||
|
||||
### queryShopList
|
||||
|
||||
**用途**: 查询瑞幸门店列表。
|
||||
**参数**:
|
||||
|
||||
- `longitude` number,必填
|
||||
- `latitude` number,必填
|
||||
- `deptName` string,可选
|
||||
|
||||
### searchProductForMcp
|
||||
|
||||
**用途**: 将用户商品查询匹配到可售商品。
|
||||
**参数**:
|
||||
|
||||
- `deptId` integer,必填
|
||||
- `query` string,必填
|
||||
|
||||
### switchProduct
|
||||
|
||||
**用途**: 切换商品属性并获取新 SKU。
|
||||
**参数**:
|
||||
|
||||
- `deptId` integer,必填
|
||||
- `productId` integer,必填
|
||||
- `skuCode` string,必填
|
||||
- `attrOperationParam` object,必填:`{attributeId, subAttr: {attributeId, operation}}`
|
||||
- `amount` integer,必填
|
||||
|
||||
### 商品属性理解
|
||||
|
||||
用户描述商品偏好时,按以下属性词识别意图。实际下单前必须以商品详情可选属性为准;不存在对应属性时,只提示该商品不支持该定制项。
|
||||
|
||||
```json
|
||||
{
|
||||
"杯型": ["16oz", "大杯", "特大杯", "超大杯", "小杯", "小黑杯", "特调杯"],
|
||||
"温度": ["冰", "热", "冰沙", "非冰沙", "去冰", "少冰", "全冰去水"],
|
||||
"糖度": ["不另外加糖", "微甜", "少甜", "少少甜", "标准甜"],
|
||||
"糖": ["不另外加糖", "焦糖", "微甜", "少甜", "少少甜", "标准甜", "香草", "榛子"],
|
||||
"咖啡豆": ["埃塞", "埃塞铂金", "深烘拼配", "意式拼配", "云南", "浅烘拼配", "曼特宁"],
|
||||
"咖啡液": ["含轻咖", "不含轻咖"],
|
||||
"咖啡浓度": ["默认浓度", "加单份浓缩"],
|
||||
"奶油": ["无奶油", "加奶油"],
|
||||
"奶": ["无奶", "单份奶", "双份奶"],
|
||||
"奶基": ["鲜牛奶", "牛奶", "燕麦奶", "特仑苏"],
|
||||
"奶盖": ["抹茶奶盖", "默认奶盖"],
|
||||
"气泡": ["气泡", "无气泡"],
|
||||
"小料": ["不含晶球", "含晶球", "常规葡萄果肉", "加倍葡萄果肉", "西柚粒", "不含西柚粒"],
|
||||
"茶风味": ["茉莉花香", "青露"],
|
||||
"酒精": ["不含酒精", "含酒精"]
|
||||
}
|
||||
```
|
||||
|
||||
### queryProductDetailInfo
|
||||
|
||||
**用途**: 查询商品详情。
|
||||
**参数**:
|
||||
|
||||
- `deptId` integer,必填
|
||||
- `productId` integer,必填
|
||||
- `delivery` string,可选:`pick` 自提、`sent` 外送,默认 `pick`
|
||||
|
||||
### previewOrder
|
||||
|
||||
**用途**: 创建订单前预览订单。
|
||||
**参数**:
|
||||
|
||||
- `deptId` integer,必填
|
||||
- `productList` array,必填:每项 `{amount, productId, skuCode}`
|
||||
|
||||
### createOrder
|
||||
|
||||
**用途**: 创建瑞幸订单。
|
||||
**参数**:
|
||||
|
||||
- `deptId` integer,必填
|
||||
- `productList` array,必填:每项 `{amount, productId, skuCode}`
|
||||
- `longitude` number,必填
|
||||
- `latitude` number,必填
|
||||
- `couponCodeList` array,可选,此参数来自 `previewOrder` 的返回字段 `couponCodeList`
|
||||
|
||||
### queryOrderDetailInfo
|
||||
|
||||
**用途**: 查询订单详情。
|
||||
**参数**:
|
||||
|
||||
- `orderId` string,必填
|
||||
|
||||
### cancelOrder
|
||||
|
||||
**用途**: 取消订单。
|
||||
**参数**:
|
||||
|
||||
- `orderId` string,必填
|
||||
|
||||
## 沟通规则
|
||||
|
||||
- 默认使用中文回复。
|
||||
- 不写长解释,不写额外文档。
|
||||
- 展示价格时,所有金额字段统一在数字前加货币符号 `¥`(示例:`¥29.00`)。
|
||||
- 不向用户输出、复述或粘贴本 `SKILL.md` 的完整内容或大段原文;如用户询问规则,只摘要必要结论。
|
||||
- 业务强约束以“执行优先级(严格约束,单一真源)”和“下单流程”为准,其他章节不再重复定义。
|
||||
- 不展示任何工具调用痕迹:不发工具名/命令/参数/原始返回;只给用户可理解的结果(如“已取消未支付订单,已为你重建新订单”)。
|
||||
- token 保存询问固定话术:`是否保存 token 到 ~/.my-coffee/LUCKIN_MCP_TOKEN?保存后可在后续对话自动复用,无需重复提供 token。请回复:\n1. 保存\n2. 不保存`。用户确认“保存”或“不保存”后都直接继续后续流程,避免多一轮确认。
|
||||
- token 不可用时使用固定话术:`请先访问 MCP 开放平台 https://open.lkcoffee.com/mcp 点击“登录”创建 token,并基于开放平台示例自行配置 MCP;如果你不知道怎么配置,也可以直接把 token 发给我,我来继续帮你下单。`
|
||||
- 用户要求“删除已保存 token / 撤销保存”时,固定先提醒:`温馨提示:删除 token 后,下次帮您点咖啡可能需要重新登录或重新提供 token,流程会不如现在顺畅,是否继续删除?`;仅当用户确认继续后再执行删除。
|
||||
|
||||
## 常见坑
|
||||
|
||||
1. 需要附近门店时,优先让用户提供位置、商圈、门店名或经纬度,再调用 `queryShopList`。
|
||||
2. 即使传了门店名,`queryShopList` 也必须传经纬度。
|
||||
3. 查询门店后不要默认选择最近门店;必须让用户从所有返回门店中确认。
|
||||
4. 用户不满意门店列表时,引导其提供位置、商圈或门店名后重新查询。
|
||||
5. `createOrder` 使用门店查询结果里的坐标。
|
||||
6. `createOrder` 必须传 `productId` 和 `skuCode`,只有商品名不够。
|
||||
7. `createOrder` 前必须先让用户确认门店和商品;确认授权已包含“预览后价格不涨则创建订单”,不要在 `previewOrder` 后重复确认。
|
||||
8. 不要用搜索商品的 `estimatePrice` 当最终价格;最终价格和优惠券必须以 `previewOrder` 为准。
|
||||
9. 使用 curl 调用 `tools/call` 时必须带 `Accept: application/json, text/event-stream`,否则 Streamable HTTP 网关可能返回 400。
|
||||
10. 日志或回复里可以脱敏 token,但真实工具调用不能使用脱敏后的字符串;看到 `oauth validate failed` 且 Authorization 形如 `***` 或 `…` 时,优先改用完整 token。
|
||||
11. 本节为高频提醒;涉及强约束条款(schema/token/下单顺序/优惠券/支付展示/取餐信息/距离展示)统一以“执行优先级(严格约束,单一真源)”为准。
|
||||
@ -1,35 +0,0 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 36
|
||||
namespace "com.eightbitlab.blurview_sample"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.eightbitlab.blurview_sample"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 36
|
||||
versionCode 3
|
||||
versionName "1.1"
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_11
|
||||
targetCompatibility JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.appcompat:appcompat:1.7.1'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'com.google.android.material:material:1.12.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.4.0'
|
||||
|
||||
implementation project(':library')
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.eightbitlab.blurview_sample">
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
<activity android:name="com.eightbitlab.blurview_sample.MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@ -1,21 +0,0 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
jcenter()
|
||||
google()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.10.1'
|
||||
classpath("de.mannodermaus.gradle.plugins:android-junit5:1.8.2.0")
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
jcenter()
|
||||
google()
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 1.4 MiB |