给 Obsidian 移动端伴侣加图片查看器:手势与翻页共存、AI 品牌焕新与一个 CJK 环境坑
导读
给自己的开源小项目 Obsidian Mobile Companion(Android 端,GitHub 同步 Markdown 仓库)做了一天的体验升级:图片全屏查看器(双指缩放 + 双击放大 + 同目录滑动切换)、AI 生成的整套品牌素材、插画空态、LazyColumn 性能收尾。真正的硬骨头有两个:手势缩放与 Pager 翻页的事件争夺,以及一个中文用户名引发的 Gradle 灵异 bug。
前言
这是一个 Kotlin + Jetpack Compose 的个人项目,通过 GitHub REST API 同步 Obsidian 仓库。9 月 16 日一天产出四个 commit:图片查看器、品牌素材重制、插画空态 + 纸纹背景、LazyColumn 化。功能不大,但每一处都踩到了值得记录的点儿。
一、图片查看器:手势与翻页如何共存
需求三入口全要:文件浏览页点开图片、Reader 内嵌图片点击放大、查看器内同目录滑动切换;缩放用双指捏合 + 双击放大/还原;格式上不加新依赖——Coil 2.7 自带的解码能力覆盖 PNG/JPG/WebP/BMP/AVIF,GIF 在 API 28+ 自动动图,SVG 落到”暂不支持”占位。
核心难点是手势冲突:HorizontalPager 要单指横向拖动来翻页,而缩放手势也要拖动来平移。Compose 自带的 detectTransformGestures 会吞掉一切拖动事件,Pager 直接翻不了页。解法是自定义手势检测器,只在”该消费的时候才消费”:

图 1:双指归查看器、1x 单指归 Pager——事件消费权按状态切换
缩放数学抽成了纯函数 ZoomTransform(不碰 Compose 状态,好测):围绕触点缩放保持不动点、边缘钳制不露空白、双击在 1x↔3x 间切换。写单测时还修了自己一处错误预期——双击靠近容器边缘的点时,钳制会牺牲不动点(offset 超出平移边界被截断),把用例改成”近中心保持不动点 + 近边缘命中钳制”两个断言,反而把行为边界钉得更清楚。最终 JVM 单测 170 个全过(新增 17:缩放数学 9 + 同目录列表 5 + findEntry 3)。
二、品牌焕新:AI 生成 + PIL 后处理的流水线
旧封面(橙色长阴影书)观感不行,以一张暖色扁平插画为参考风格(羊皮纸底、赭石橘/芥末黄/橄榄绿几何元素、翻开的书与便签)整套重制。素材全部由 MiniMax image-01 多轮生成,PIL 做后处理:
| 素材 | 用途 |
|---|---|
cover.jpg |
新封面:书 → 便签飞出 → 手机的同步叙事构图 |
icon-master.jpg |
启动图标:满幅 terracotta 圆角方块 + 奶油色翻开的书 |
hero-welcome.jpg |
Onboarding 欢迎页主视觉(190dp 圆角卡片) |
spot-empty/search/offline.jpg |
三张空态插画 |
bg-welcome.jpg |
首尾两屏的纸纹背景 |
两个后处理细节值得记。其一是图标四角:AI 生成的圆角方块四角透出浅色,第一版修角脚本用”比 tile 红 25”的阈值判断,写出来发现条件恒假(tile R=249,上限 255),静默失败一张没修;改成采样角点颜色算距离(阈值 55²)限定角落区域填充才修干净。其二是五档 mipmap:从 1024 母图程序化生 48~192 五档密度,方形满幅 + 圆形 alpha 蒙版两套,模拟器实装确认闪屏圆形图标正确。

图 2:mmx 生成 → PIL 程序化修角/切密度 → 落进 drawable 与 mipmap
插画空态的接入方式也有讲究:EmptyState 新增插画版重载(@DrawableRes illustration 参数,132dp 圆角卡片),icon 版签名不动——存量 8 个调用点零改动,只把文件页”无索引/空文件夹”、搜索无结果、阅读器离线四处换成插画。
三、收尾:LazyColumn 化与一次”白排查”的仓库清理
文件列表从 Column + forEach 改成 LazyColumn(大文件夹不再一次性组合所有行),path 做稳定 key,分隔线与留白行为保持不变。
仓库清理的结论有点反直觉:排查发现 build5.log 命中 *.log 忽略规则、Web-Prototype.zip 在 .git/info/exclude 里——两个文件本就不被 Git 跟踪,仓库无需任何操作。核对 zip 与对应目录内容完全一致后,只做了磁盘侧删除。教训:清理任务先验证”它真的在仓库里吗”,再动手。
四、环境坑:中文用户名引发的 Gradle 灵异事件
当天最灵异的一幕:testDebugUnitTest 突然报 ClassNotFoundException: GradleWorkerMain——测试 worker 起不来,daemon 重启、重装分发版都没用。--info 日志暴露了蛛丝马迹:worker 的 classpath 参数文件路径是 @C:\Users\张\.gradle\...。
用最小 Java 实验复现确认(UTF-8 argfile + CJK 路径在 GBK launcher 下必炸)后才动的手。这类”环境层”bug 的排查要点:先怀疑编码和路径,再怀疑工具本身——Gradle 没坏,是用户名里的汉字和 JDK 的默认_charset_ 撞了车。
经验总结
- 手势共存的关键是消费权的精确分配:双指/放大态归变换手势,1x 单指归 Pager,两边用同一个 scale 状态做开关;
- 缩放数学抽纯函数,单测先写;钳制与不动点的冲突用测试钉死边界;
- AI 素材流水线里,后处理脚本要防静默失败——阈值判断写反了不会报错,只是什么都不修;
- 给存量组件加能力用重载/新参数,8 个调用点零改动;
- 清理仓库前先验证目标是否真的被 Git 跟踪;
- Windows + 中文用户名 + JDK 17:Gradle 构建带上
GRADLE_USER_HOME指到纯 ASCII 路径。
结语
个人项目的好处是可以在一天之内把”想要”变成”有了”:查看器、品牌、空态、性能,四个 commit 全部推送。但当天最有长期价值的产出反而是那个 CJK 环境坑的解法——它已经进了长期记忆,下次换机器重装环境时,能省回一整天的排查。