Day 04 討論過設計系統,但一直停在概念階段。
今天要動手了。
「設計系統建立了,但怎麼轉成程式碼?」我看著之前的筆記發愁。
我開始搜尋「desktop app design principles」、「macOS app layout best practices」。
找到幾個重點:
「幫我分析一下 Desktop 和 Mobile 的差異。」我把搜尋結果給 AI。
AI 整理了一個對比:
場景 | Mobile | Desktop |
---|---|---|
螢幕 | 5-7 吋,觸控優先 | 13-32 吋,滑鼠精準 |
操作 | 手指點擊,區域大 | 滑鼠 hover,精準互動 |
使用時長 | 碎片化,快速瀏覽 | 長時間專注工作 |
確實,Desktop 的設計思維完全不同。
載入畫面是用戶第一眼看到的。我想要有魔法感,但不知道怎麼做。
翻了翻 Dribbble,看到一些有趣的載入動畫。大多是旋轉的圓圈,太普通了。
「我想要魔法書的感覺,」我跟 AI 說,「有沒有什麼建議?」
「可以分層設計,」AI 建議:
聽起來不錯。開始寫程式碼:
@Composable
fun LoadingScreen(
loadingState: LoadingState,
onLoadingComplete: () -> Unit,
modifier: Modifier = Modifier
) {
Box(modifier = modifier.fillMaxSize()) {
// 背景圖片
Image(
painter = painterResource(Res.drawable.night_city_skyline_with_shooting_stars),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize(),
alpha = 0.8f
)
// 半透明遮罩
Box(
modifier = Modifier
.fillMaxSize()
.background(GrimoColors.MorningFog.copy(alpha = 0.15f))
)
// 載入內容
Column(
modifier = Modifier.align(Alignment.Center),
horizontalAlignment = Alignment.CenterHorizontally
) {
MagicalLoadingAnimation()
Spacer(modifier = Modifier.height(48.dp))
Text(
text = loadingState.message,
style = MaterialTheme.typography.bodyLarge
)
}
}
}
第一版太單調,只有旋轉。
我想加點特別的——魔法書和羽毛筆。
@Composable
private fun MagicalLoadingAnimation() {
Box(contentAlignment = Alignment.Center) {
val infiniteTransition = rememberInfiniteTransition()
// 外圈旋轉
val rotation by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(2000, easing = LinearEasing)
)
)
// 魔法書圖片
Image(
painter = painterResource(Res.drawable.magic_circle_book),
contentDescription = "魔法陣與書本",
modifier = Modifier.size(300.dp)
)
// 羽毛筆寫字動畫
val writingX by infiniteTransition.animateFloat(
initialValue = -10f,
targetValue = 10f,
animationSpec = infiniteRepeatable(
animation = tween(800),
repeatMode = RepeatMode.Reverse
)
)
Image(
painter = painterResource(Res.drawable.magic_quill),
contentDescription = "羽毛筆",
modifier = Modifier
.size(200.dp)
.offset(x = writingX.dp)
)
}
}
跑起來了!羽毛筆在書上寫字的感覺很棒。
之前的 Liquid Arcana 太理想化,實際寫程式時我改成了更簡潔的命名:
object GrimoColors {
// 主題色 - 冰川青(低飽和度)
val GlacierCyan = Color(0xFF8FBFD0)
// 基底色 - 晨霧
val MorningVeil = Color(0xFFF2F6F9)
val StarlightMist = Color(0xFFD8E0E7)
// 功能色
val CinderRed = Color(0xFFD77C7C) // 錯誤,柔和不刺眼
// 中性色階
val PureWhite = Color(0xFFFFFFFF)
val MorningFog = MorningVeil
val CoolGray = Color(0xFF8A96A3)
val Midnight = Color(0xFF161B21)
// 玻璃材質
val GlassTintBrand = Color(0x148FBFD0) // 8% 透明度的品牌色
val GlassLightBg = Color(0x29FFFFFF) // 16% 白色
}
取名很痛苦。
一開始用 primary、secondary,但太無聊。後來想到用自然意象——冰川、晨霧、星光。既有詩意,又能暗示顏色。
「動畫要多快?」我問 AI。
「看用途,」AI 回答,「hover 效果要快(100-200ms),頁面轉場可以慢一點(300-500ms)。」
整理成常數:
object GrimoMotion {
const val QUICK_DURATION = 150 // hover 反饋
const val STANDARD_DURATION = 300 // 一般過渡
const val GENTLE_DURATION = 500 // 展開收合
const val BREATH_DURATION = 2000 // 呼吸循環
}
看了很多 glassmorphism 的文章。關鍵是:
Compose Desktop 沒有模糊效果,只能用透明度模擬:
@Composable
fun GlassButton(
text: String,
onClick: () -> Unit
) {
var isHovered by remember { mutableStateOf(false) }
val glassAlpha by animateFloatAsState(
targetValue = if (isHovered) 0.18f else 0.12f,
animationSpec = tween(GrimoMotion.QUICK_DURATION)
)
Box(
modifier = Modifier
.clip(RoundedCornerShape(12.dp))
.background(Color.Black.copy(alpha = glassAlpha))
.border(
width = 1.dp,
color = Color.White.copy(alpha = 0.08f),
shape = RoundedCornerShape(12.dp)
)
.clickable { onClick() }
.onHover { isHovered = it }
.padding(horizontal = 24.dp, vertical = 12.dp)
) {
Text(
text = text,
color = GrimoColors.GlacierCyan
)
}
}
按鈕太素了,想加點星光效果。
「怎麼做星光閃爍?」我問 AI。
「在按鈕上隨機生成小光點,」AI 建議,「控制生命週期,讓它們淡入淡出。」
寫了個星光動畫:
@Composable
fun Modifier.starSparkle(
enabled: Boolean = true,
sparkleColor: Color = Color(0xFFFECA57) // 金色
): Modifier {
if (!enabled) return this
var sparkles by remember { mutableStateOf(listOf<Sparkle>()) }
// 生成星光
LaunchedEffect(enabled) {
while (enabled) {
val newSparkle = Sparkle(
x = Random.nextFloat(),
y = Random.nextFloat(),
size = Random.nextFloat() * 5f + 3f,
lifeTime = 0f
)
sparkles = (sparkles + newSparkle).takeLast(6)
delay(Random.nextLong(200, 800))
}
}
return this.drawWithContent {
drawContent() // 先畫按鈕
// 再畫星光
sparkles.forEach { sparkle ->
val alpha = when {
sparkle.lifeTime < 0.15f -> sparkle.lifeTime * 6.67f
sparkle.lifeTime > 0.6f -> (1f - sparkle.lifeTime) * 2.5f
else -> 1f
}
drawStar(
center = Offset(
size.width * sparkle.x,
size.height * sparkle.y
),
size = sparkle.size,
color = sparkleColor.copy(alpha = alpha)
)
}
}
}
效果很夢幻!hover 時星光閃爍,有魔法的感覺。
動畫一多就卡。
「為什麼會卡?」我問 AI。
「可能觸發了重組,」AI 分析,「試試 graphicsLayer。」
// 卡頓版本
Modifier.offset(y = animatedValue.dp)
// 流暢版本
Modifier.graphicsLayer {
translationY = animatedValue
}
改完順多了。原來 offset 會觸發重組,graphicsLayer 只更新渲染層。
大圖片載入很慢,加個淡入效果:
@Composable
fun OptimizedImage(resource: DrawableResource) {
var isLoaded by remember { mutableStateOf(false) }
AnimatedVisibility(
visible = isLoaded,
enter = fadeIn()
) {
Image(
painter = painterResource(resource),
contentDescription = null,
onSuccess = { isLoaded = true }
)
}
}
從最初的 Liquid Arcana 到實際的 GrimoColors,經歷了很多調整。
理想很豐滿,現實很骨感。太複雜的命名反而難用,簡潔清晰最重要。
跟 AI 對話,具體的問題比籠統的需求有用:
❌ 「幫我設計按鈕」
✅ 「怎麼實現 glassmorphism 效果?」
❌ 「動畫怎麼做」
✅ 「星光閃爍的生命週期怎麼控制?」
第一版載入畫面只有旋轉圈。
加了背景圖。
加了魔法書。
加了羽毛筆動畫。
優化了效能。
每次一小步,最後的效果遠超最初的想像。
今天從設計理念到實際程式碼,走了一遍完整的 UI 開發流程。
最大的收穫是理解了 Desktop UI 的特點——不是放大版的 Mobile,而是完全不同的體驗。大螢幕、精準操作、長時間使用,每個特點都影響設計決策。
AI 在這個過程中像個顧問,我找資料、提問題,它幫我分析、給建議。真正的實作和決策,還是要自己來。
明天繼續深入動畫系統,讓介面更有生命力。
「優秀的 UI 不是讓介面漂亮,而是讓使用者舒適。」
關於作者:Sam,一人公司創辦人。正在打造 Grimo,智能任務管理平台。
專案連結:GitHub - grimostudio