用途
该技能面向 Kotlin/Compose 场景,解决“没有封面图的条目”如何显示的问题。相比所有条目共用一个灰色方块,它希望为每个条目生成独一无二的封面:颜色由标题哈希决定,因此同一标题每次渲染结果一致,并且可以同步获得——在网络加载之前、加载过程中,甚至完全替代网络加载。核心做法是把标题哈希成一个渐变色,再用 Painter 让加载器把它当作图片处理。
主要能力
- 确定性配色:通过
gradientFromTitle(title)从标题哈希推导两个色相,生成线性渐变。 - 自定义 Painter:
TitlePlaceholderPainter继承Painter,在onDraw中绘制圆角矩形渐变、测量好的文字以及一个徽章。 - 同步可用:不依赖网络请求,可在加载前、加载中或替代加载时直接绘制。
- 文字测量与布局:在组合阶段用
rememberTextMeasurer测量标题,限制为两行、自动换行、省略号溢出,并按瓦片尺寸减去内边距约束。 - 徽章叠加:在右上角绘制白色圆点并叠加徽章 Painter。
使用方式
技能提供了工厂函数 rememberTitlePlaceholder(title, style, sizeDp),在组合中获取 LocalDensity、创建文字测量器、测量布局、加载徽章资源,并把 Dp 尺寸转换为像素后构造 Painter,最后用 remember 缓存。
关键实现要点:
- 文字测量放在组合阶段,绘制放在
onDraw,避免每帧测量。 - 测量结果按 title、style、size 作为 key 缓存,并传入不可变结果。
- 约束为瓦片尺寸减去内边距,否则文字会按全宽布局并溢出边缘。
- Painter 没有密度概念,需在组合中把 Dp 转成像素后再构造,并把像素尺寸作为
intrinsicSize返回。 - 在
onDraw中,未限定的size解析为DrawScope.size(实际绘制尺寸),而非构造器属性;示例已遵循绘制尺寸,存储尺寸只用于intrinsicSize。 - 三个图片槽位要全部填充:
placeholder表示加载中,error表示请求失败,fallback表示模型为空(即无封面场景)。 - 要缓存 Painter 本身,而不仅是测量结果,否则每次组合都会产生新实例,破坏跳过优化。
- 颜色需要一致的地方要复用同一个生成函数,例如详情页背景着色应与瓦片调用同一函数。
适用场景
适用于 Kotlin/Compose 应用中列表或网格里没有封面图的条目,需要稳定、唯一、同步可用的占位视觉。也适用于需要在加载前、加载中或加载失败时保持视觉一致的场景。
限制与陷阱
- 确定性依赖哈希行为:Kotlin/JVM 上
String.hashCode()由 JDK 规范保证跨运行、跨机器、跨 JDK 版本稳定;但 Kotlin/Native 和 Kotlin/JS 未作保证,非 String 类型也未说明。若增加 Native 或 Web 目标,或需要持久化颜色,应自行对字节做哈希。 - 哈希要掩码而非取余:哈希是有符号的,
hash % 256对约一半输入为负,会产生负色相;hash and 0xFF对负值正确,因为它作用于补码位。相邻字节取出的两个色相可能过于接近而显得扁平,若要保证对比度,可由第一个色相加固定旋转得到第二个。 - 文字测量必须在组合阶段完成,
onDraw每帧执行,在其中测量会每帧分配和布局。 - 没有布局阶段,定位靠算术:需自行计算左上角并加回内边距。
Size.Unspecified会让图片组合无法确定自身尺寸,因此必须报告intrinsicSize。- 混用存储尺寸与绘制尺寸会导致错误:例如按存储尺寸绘制圆角矩形、却按
DrawScope.size居中画圆,当调用方以非固有尺寸绘制时就会错位。
验证方法
技能给出若干 grep 检查:
- 检查每个
AsyncImage(调用点是否命名了三个槽位,用-A16展开上下文,-[0-9]+-锚定上下文行。 - 检查组合中构造但未被 remember 的 Painter。
- 检查有符号哈希取余(负色相 bug),期望无输出。
- 确定性本身无法用 grep 检查:需在同一会话内渲染同一列表两次并跨重启确认颜色一致,再渲染仅差一个字符的标题——忽略部分输入的生成器会通过第一项测试但失败于第二项。