谁悲失路之人
拾遗 PickUp——一个 Vibe Coding 作品的诞生与进化

一句话

拾遗 PickUp 是一款帮助用户快速清理手机照片的 App——像刷短视频一样上滑删除、下滑保留,每 10 张一组批量确认,让整理相册不再是一种负担。

当前版本 v2.0.0,已产出可安装的 Android APK。


缘起

手机相册里积压了成千上万张照片——截图、废片、重复拍摄、已经用不上的记录。一张一张点进去再点删除太慢,批量全选又怕误删重要照片。市面上已有的清理工具要么太复杂,要么不够优雅。

于是有了 拾遗 PickUp:用短视频式的滑动交互,把「整理照片」这件事变得轻快、可控、甚至有点上瘾。


核心体验

滑动即决策

打开 App,照片一张张全屏展示。你的手指就是决策工具:

方向 操作 视觉反馈
↑ 上滑 标记删除 红色蒙层 + “删除” + 触觉反馈
↓ 下滑 标记保留 绿色圆点 + “保留”
← 左滑 跳过,下一张 卡片飞出 + “跳过”
→ 右滑 返回上一张 “上一张”

每滑一张都有细腻的触觉反馈(expo-haptics)和回弹动画(react-native-reanimated 4),底部圆点进度条实时反映当前组的标记状态:黄色 = 已标记删除,绿色 = 已保留,灰色 = 待决定。

产品界面

以下是 PickUp 的实际运行截图——深色主题、原比例照片卡片、圆角大图展示:

PickUp 主浏览界面 - 照片卡片与手势操作

滑动交互

上滑删除、下滑保留、左滑跳过、右滑返回——四向手势配合触觉反馈和回弹动画,让整理照片像刷短视频一样顺畅:

PickUp 滑动交互 - 四向手势标记

批量确认,安全可控

滑完一组 10 张后,进入全屏删除确认页——以第一张待删除照片为模糊背景,展示待删除照片堆叠预览:

PickUp 删除确认与待删除列表

你可以点击照片再次确认、点击 Delete 触发系统删除,或选择稍后删除。途中右上角浮动胶囊按钮随时批量删除。

智能分组,不重复不遗漏

照片分组使用 Fisher-Yates 洗牌算法从候选池中随机抽取 10 张,已浏览的不再出现。所有照片看完一圈后,从最早浏览的开始回填,保证不重复、不遗漏。

支持四种排序模式:

  • 🎲 随机乱序
  • 📐 面积从大到小(优先处理占空间的大图)
  • 🕐 最新优先
  • 🕐 最早优先

照片信息展示

每张照片卡片上展示:

  • 相对日期:年月日 · X 天前 / 昨天 / 今天
  • 地理位置:自动读取 EXIF GPS 坐标,显示拍摄城市(如 📍 上海市)
  • LIVE 标识:Live Photo 显示专属徽章

统计与成就

App 内建统计面板,持续追踪:

  • 累计浏览 / 累计删除 / 连续使用天数 / 释放空间
  • 日使用量跨天自动重置
  • 每周清理回顾 + 7 日柱状图
  • 成就系统:首次清理、删除入门、空间管理师、连续 3 天、相册守护者等徽章

统计面板

App 内建了丰富的统计面板——累计浏览、删除、连续天数、释放空间一目了然:

PickUp 统计面板与更多功能

月份清理闭环(v2.0)

「更多功能」页的月份柱状图可点击进入指定月份的专属清理流——只整理那个月的照片,清完后弹出完成提示。这让「按月份整理」成为一个完整闭环,而不是只看不动的统计图。


会员与限制

用户类型 限制
免费用户 每日 3 组(30 张)
Pro 订阅 无限使用

支付通过 RevenueCat 管理,支持周/月/年/永久四种订阅方案。开发阶段内置「开发者模式」——设置页连续点击底部文字 5 次即可绕过所有限制。


技术栈

类别 技术选型
框架 Expo SDK 54 + React Native 0.81.5(New Architecture)
路由 expo-router 6(file-based routing,Tab 导航)
手势 & 动画 react-native-gesture-handler + react-native-reanimated 4
触觉 expo-haptics
媒体访问 expo-media-library(相册读取 + 原生删除)
文件系统 expo-file-system(文件大小计算)
支付 RevenueCat(react-native-purchases)
持久化 @react-native-async-storage/async-storage
图标 @expo/vector-icons(MaterialCommunityIcons)
测试 Jest + ts-jest + @testing-library/react-native

架构设计

Provider 嵌套层级

Provider 嵌套架构图

1
2
3
4
5
6
7
GestureHandlerRootView
└─ ErrorBoundary
└─ SubscriptionProvider (RevenueCat + 日用量)
└─ StatsProvider (使用统计)
└─ PhotoProvider (核心照片引擎)
└─ SessionProvider (交互日志)
└─ Tab Navigator

核心数据流

  1. 权限检查(静默查询,不弹系统对话框)
  2. 照片加载(MediaLibrary.getAssetsAsync 分页拉取,500 张/页)
  3. 分组生成(Fisher-Yates 从未浏览照片中随机选 10 张)
  4. 浏览追踪(已浏览 ID 集合 + 顺序数组持久化)
  5. 日用量控制({ date, count } 结构,跨天自动清零)
  6. 手势交互(PanResponder → SessionContext → PhotoContext 标记集合)

核心数据流

渲染优先级

主浏览页按严格顺序判断渲染:

1
权限被拒 → 加载中 → 达到日限制 → 相册为空 → 正常浏览

这个顺序经历了多次 Bug 修复才固定下来——错误的顺序曾导致闪屏、死循环、黑屏等问题。


开发历程

拾遗 PickUp 是一个典型的 Vibe Coding 作品——从第一行代码到 v2.0.0,全程由人类描述意图、AI 辅助实现。

开发时间线

时间线

时间 里程碑
2026-05-21 Phase 1 MVP:核心滑动交互 + 照片加载 + 标记删除/保留
2026-05-23 v1.0.1:修复滑动抽搐、黑屏、无限循环等关键 Bug,动态包名配置
2026-05-24 v1.4-v1.6:首页重构、Hub 月份柱状图、相册选择、品牌改名 PickUp
2026-05-25 v1.2:释放空间统计、排序功能、最近删除页
2026-05-27 v1.7:左右滑动动画增强、庆祝动画、新手引导重做
2026-05-31 v1.3:全屏删除确认页、每周回顾、成就系统
2026-06-01 v1.3.0:删除确认链路完善、待删除列表、照片双指缩放
2026-06-03 v1.3.4:开屏动画优化、更新日志弹框、关于页
2026-06-04 v1.3.5:体验修复、图标优化、本地 release 构建
2026-06-05 v2.0.0:全局 UI 升级、月份清理闭环、黑底高对比 iOS 风格

构建配置

项目通过 app.config.js 动态配置 + Gradle applicationIdSuffix 实现三套包名并存:

Build Type 应用名 包名 体积
debug 拾忆 com.zackf.pickup.dev 192 MB
release PickUp com.zackf.pickup.preview 96 MB
production 拾遗 com.zackf.pickup

本地 Gradle 构建(assembleDebug / assembleRelease)替代了 EAS 云构建,启用 R8 代码混淆 + 资源压缩 + ABI 过滤。

完整开发日志

以下是从项目第一天到 v2.0.0 的完整开发日志——以杂志风格的 HTML 页面呈现,记录每一个里程碑、Bug 修复和关键决策:

📖 这份日志以杂志排版呈现——Playfair Display 标题字体、时间线式条目布局、按标签分类(Bug/Feature/Design/Build)、关键文件引用和下一步计划。可直接滚动浏览整个开发历程。


设计语言(v2.0)

v2.0 进行了一次全局视觉升级,参考 iOS Human Interface Guidelines 和 Dribbble 移动端设计趋势:

  • 暗色基底:大面积的深黑背景
  • 高对比卡片:大圆角深灰模块,精致而不抢眼
  • **品牌主色 #A8D46F**:柔和的黄绿色,用于状态强调和关键动作
  • 超粗字体:数字和标题使用 bold/heavy 字重建立层级
  • 克制的动画:弹框采用毛玻璃 + 淡入淡出,sheet 用滑入动画

底部 Tab 栏设计经历了多次迭代——从默认 Tab → 毛玻璃胶囊 → 纯黑底半透明胶囊,最终落地为一个简洁的图标导航条。

品牌色探索

在 v2.0 视觉升级中,我们通过 HTML 原型对比了 5 种品牌主色在真实界面中的表现——从柱状图到统计卡片到 Tab 图标,全部实时渲染。下面这个页面是当时的色彩对比工具,可以直接操作查看效果:

💡 上方的色彩预览工具是一个完整的纯 HTML/CSS 页面——5 列手机框内嵌真实 UI 组件(月份柱状图、统计卡、Tab 栏),切换 CSS 变量即可全局替换品牌色。最终选定 #A8D46F 作为 v2.0 主色。



有意思的技术细节

绕过系统删除弹框的尝试

Android 系统删除媒体文件时一定会弹出系统确认框(Scoped Storage 保护)。团队尝试了 MANAGE_EXTERNAL_STORAGE 权限、FileSystem 直接删除等方案,历经多轮探索后结论是不可行——Android 11+ 对共享存储的文件删除有 OS 级保护,换 SDK/框架/语言都无法绕过。

最终方案:Android 走 MediaLibrary.deleteAssetsAsync(系统弹框确认一次),iOS 先弹自定义确认页再调删除(iOS 无系统弹框)。

expo-font 版本不匹配引发原生崩溃

一个印象深刻的教训:用 npm install 而非 npx expo install 添加 expo-font,导致安装了一个 SDK 56+ 的版本(56.0.5),其原生代码调用的方法在 SDK 54 的 expo-modules-core 中不存在,引发 getDirectConverter 崩溃。教训:Expo 生态中必须用 npx expo install 管理包版本,否则版本不匹配会引发难以调试的原生崩溃。

滑动抽搐的数学根因

早期版本滑动卡片出现明显的「抽搐」——手指松开后卡片来回振荡。根因是使用了 withSpring 动画配合默认的欠阻尼参数。改为 withTiming + Easing.out(Easing.cubic) 后动画顺滑自然。


文件结构一览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
app/                          # 页面 (expo-router)
index.tsx # 主浏览页
review.tsx # 待删除列表
settings.tsx # 个人中心
paywall.tsx # Pro 订阅
recent-deletes.tsx # 最近删除
albums.tsx # 相册选择
hub.tsx # 更多功能
about.tsx # 关于页

src/
components/
gesture/ # 手势交互(SwipeableCard, ActionIndicator 等)
delete-review/ # 删除确认体验
photo-card/ # 照片卡片与进度
settings/ # 设置页组件
hub/ # 更多功能组件
ui/ # 通用 UI(Toast, Modal, Celebration 等)
contexts/ # React Context(Photo, Session, Subscription, Stats)
hooks/ # usePhotoEngine, useHaptics
services/ # 照片分组、删除、订阅、统计
types/ # TypeScript 类型定义
utils/ # 工具函数(Fisher-Yates, 日期, 成就, 删除确认)
design-tokens.ts # 全局设计令牌

写在最后

拾遗 PickUp 从 5 月 21 日的第一行代码到 6 月 5 日的 v2.0.0,两周多的时间里经历了十几个版本的迭代。它不是一个「完美的产品」,但它是一个完整的、可用的、有灵魂的作品——有自己的设计语言、有经过反复打磨的交互细节、有从真实使用中长出来的功能。

如果你也对手机相册的混乱感到焦虑,欢迎试试看。

拾遗,取自「拾起遗落的记忆」——记忆由你选择。


作者:Zack Feng(Fueen)
代码仓库github.com/fueen/pickup
联系邮箱1179722988@qq.com

React Native Expo Vibe Coding 独立开发 照片清理
AI基础知识扫盲:从大模型到Skill与MCP
© 2019-2026 Fueen