SuperAPI AI 工具集

artwork-palette-theming

设计与创作

查看项目

更新于 10-17 16:16

技能介绍

用途

该技能用于解决“如何根据一张图片的调色板来驱动整个屏幕配色”的问题。调色板提取会为每张图片返回若干色板(vibrant、muted、dominant 以及各自的 light/dark 变体),选错色板会让界面看起来像设计过,也可能看起来像坏掉了。正确选择取决于用途:是小面积强调色,还是文字背后的大面积页面背景。

主要能力

强调色与背景色的区分

强调色(区块着色、chip、条纹、阴影颜色)需要鲜艳的色板,并使用优先级回退链,保证总能返回一个值:

// adapted: the real code nests these. `const val NONE = 0` — the "absent" default every getter takes
var argb = palette.getDarkVibrantColor(NONE)
if (argb == NONE) argb = palette.getDarkMutedColor(NONE)
if (argb == NONE) argb = palette.getVibrantColor(NONE)
if (argb == NONE) argb = palette.getMutedColor(NONE)
if (argb == NONE) argb = palette.getLightVibrantColor(NONE)
if (argb == NONE) argb = palette.getLightMutedColor(NONE)

页面背景则相反,应使用主色板(dominant),即按像素面积计算的图片整体色调。鲜艳色板可能只是很小的饱和色块(头发、口红、logo),用它铺满整页就会出现“为什么这张人像的页面是砖红色”的问题。

// adapted
fun Palette.toImmersiveBackground(): Color {
    val rgb = getDominantColor(NONE).takeIf { it != NONE }
        ?: getMutedColor(NONE).takeIf { it != NONE }
        ?: getVibrantColor(NONE).takeIf { it != NONE }
        ?: return Color.Black
    val base = Color(rgb)
    val luminance = 0.299f * base.red + 0.587f * base.green + 0.114f * base.blue
    return lerp(base, Color.Black, 0.35f + 0.45f * luminance)
}

加深处理让方案在任意图片上都能工作,而不只是在你测试过的图片上。两个常量可理解为下限与斜率:每个背景至少加深这么多,浅色背景再额外加深这么多,从而避免浅色图片产生白字消失的页面。应针对两个极端(能找到的最亮与最暗封面)来调整,单一常量无法同时满足两者。

使用方式与陷阱

“无色板”哨兵值

“无色板”哨兵值就是你传入的值,它是一个真实颜色。缺失本身没有独立表示——getDominantColor(NONE) 会把你自己的 NONE 原样返回——因此“无色板”和“颜色 0x00000000”以相同的 32 位到达。零是正确选择,因为色板总是不透明的(androidx palette 的量化器通过 Color.rgb 构建,会 OR 上 0xFF000000),所以不会与真实色板冲突;而看似“更安全”的不透明黑色会与所有真正的黑色冲突。零无法避免的是下面的陷阱:它仍然会绘制。

同一个哨兵值交给颜色构造函数时是透明的,不是黑色。Color(argb: Int) 把整个 32 位值当作 ARGB 打包,自身不提供 alpha,因此 Color(0) 的 alpha 为 0,什么也不画。在全部失败的路径上应显式返回 Color.Black——这是没人测试的那条路径。

提取返回空是正常现象

提取返回空是正常现象,不是错误。近单色图、纯色图和非常小的图片对某个 profile 不会产生色板,这在任何库中都有相当比例——这正是上面回退链存在的原因。不要把它记录为失败。

没有图片时调色板根本不会运行

没有图片时,喂给提取器的成功回调永远不会触发,调色板会一直为 null。从绘制占位符的同一个生成器派生回退值,可让两者保持一致——deterministic-title-placeholder-painter:

// adapted
val background = palette?.toImmersiveBackground() ?: run {
    val titleColors = gradientFromTitle(title)
    val base = if (titleColors.size >= 2) lerp(titleColors[0], titleColors[1], 0.5f)
               else titleColors.firstOrNull() ?: Color.Black
    lerp(base, Color.Black, 0.3f)
}

按图片标识而非位图重新生成

在懒加载列表中,头部滚出、被回收、重新挂载并再次触发回调——会重新运行提取并让页面闪成略微不同的颜色。保留已有的 key,未变化时跳过:

// adapted
var paletteGeneratedFor by remember { mutableStateOf<String?>(null) }
LaunchedEffect(bitmap) {
    val bm = bitmap
    if (bm != null && paletteGeneratedFor != currentImageKey) {
        paletteState.generate(bm)
        paletteGeneratedFor = currentImageKey
    }
}

钳制到 0f..1f,而不是 255

Compose 颜色分量是浮点数,因此 min(red * factor, 255f) 看起来像钳制却永远不会触发——这是整数颜色 API 的遗留。应使用 coerceIn(0f, 1f)。

解析十六进制需要补 alpha 字节

颜色来自远程元数据和用户设置,因此应解析为可空值——六位数字必须 OR 上不透明 alpha,否则会得到不可见颜色;八位数字本身已带 alpha:

// adapted
fun String.hexToColorOrNull(): Color? = runCatching {
    val clean = removePrefix("#")
    val argb = when (clean.length) {
        6 -> 0xFF000000L or clean.toLong(16)
        8 -> clean.toLong(16)
        else -> return null
    }
    Color(argb)
}.getOrNull()

验证方式

检查每个提取调用点,确认各自按图片标识重新生成:

grep -rn --include='*.kt' -A6 "\.generate(" . | grep -v '/build/' | grep -E "generate\(|GeneratedFor|LaunchedEffect"

检查颜色 API 周围的零哨兵值。故意不加锚定,以便捕获字面量 Color(0) 以及任何被赋予零默认值的 …Color(0) getter——每个命中都是“无颜色”与“透明黑”为同一值的地方:

grep -rn --include='*.kt' -E "Color\(0x?0+\)|Color\(0\)" . | grep -v '/build/'

检查浮点颜色代码中针对整数颜色范围的钳制:

grep -rn --include='*.kt' -E "(min|coerceAtMost|coerceIn)\([^)]*25[56]f" . | grep -v '/build/'

用肉眼针对两个极端而非平均封面来验证:一张接近全白的图片和一张接近全黑的图片。叠加你发布的最小正文字号;两者都可读,说明下限与斜率设置正确。

适用场景

适用于 Android/Kotlin 与 Jetpack Compose 中需要根据封面图、头像或内容图片动态生成界面配色的场景,尤其是沉浸式背景、详情页头部、卡片着色等。

限制

资料未提供该技能的具体安装方式、依赖版本、支持的平台范围或收费信息。