885 字
4 分钟
APNG 阅览器 —— 架构与开发教程
APNG 阅览器(APNG-View-for-Android)是一个纯离线的 APNG 动图查看器:选择或接收 APNG/PNG 文件,逐帧解码、播放、调速、步进,不需要任何网络权限。整个应用基于 Flutter 3.47.2,架构是「单 Activity + 纯 Dart 逻辑层 + 轻量原生桥」,没有任何重型框架依赖。这篇讲清楚它是怎么组织的、APNG 怎么解出来、帧怎么播起来。
总体架构
四层,自上而下依赖,平台桥横向独立:
flowchart TD
subgraph P[Presentation 表现层]
HP[HomePage<br/>首页 / 文件选择]
VP[ViewerPage<br/>查看 / 播放]
FV[ApngFrameView<br/>帧渲染组件]
end
subgraph S[State / Controller 状态层]
APP[ApngViewerApp<br/>ThemeMode 管理]
PL[ApngPlayer<br/>帧调度 / 速度 / 暂停]
end
subgraph D[Data / Service 数据层]
DEC[ApngDecoder<br/>image 包解码 APNG]
SP[SharedPreferences<br/>最近记录 / 主题持久化]
end
subgraph N[Platform 平台桥 MethodChannel]
MA[MainActivity.kt<br/>pickApngFile / getPendingFile]
end
HP -->|push| VP
VP --> FV
HP --> APP
VP --> PL
PL --> DEC
HP --> SP
HP -->|invokeMethod| MA
DEC -->|ApngDecodeResult| PL
PL -->|currentFrameData| FV设计取舍:
- 单 Activity:Flutter 侧只有一个
MainActivity,原生代码只干两件事——拉起 SAF 文件选择器、接收外部ACTION_VIEW/SEND意图。没有插件,没有第三方桥。 - 逻辑全在 Dart:解码、播放、渲染全部纯 Dart 实现,好处是 iOS 上以后可以直接复用同一套逻辑层。
- 依赖只有三个:
image(解码)、photo_view(静态大图缩放)、shared_preferences(持久化)。
APNG 解码:PNG 块级解析
APNG 不是独立格式,就是 PNG 加了三个扩展块。解码器读文件字节流,按 PNG chunk 规范遍历:
flowchart LR
B[文件字节] --> SIG[PNG 签名<br/>89 50 4E 47]
SIG --> IHDR[IHDR<br/>宽高 / 位深]
IHDR --> ACTL{acTL 存在?}
ACTL -->|是| EXT[动画扩展块<br/>acTL fcTL fdAT]
ACTL -->|否| IDAT0[IDAT<br/>单帧静态图]
EXT --> OUT[ApngDecodeResult<br/>frames 宽高 loopCount]
IDAT0 --> OUT| 块 | 作用 |
|---|---|
acTL | 动画控制:总帧数、循环次数 |
fcTL | 每帧的宽高、偏移、混合模式、帧延迟(dec/num 分数秒) |
fdAT | 动画帧的实际像素数据(结构同 IDAT,多 4 字节序号) |
IDAT | 第 0 帧(静态兜底)像素数据 |
解码核心就一段循环:
// lib/apng/apng_decoder.dart(节选)class ApngDecoder { static ApngDecodeResult decode(Uint8List bytes) { final chunks = _parseChunks(bytes); // 遍历 PNG chunk 流 final frames = <ApngFrame>[]; // 1. IHDR 拿全图画布尺寸 // 2. acTL 拿 frameCount / loopCount // 3. 每个 fcTL 记录 x/y/w/h + delayNum/delayDen // 4. 帧像素 = IDAT(第0帧) 或该帧序号对应的 fdAT // 5. zlib inflate + unfilter(Sub/Up/Average/Paeth 五种滤波) // → RGBA 字节,按 dispose 语义合成到画布 return ApngDecodeResult( frames: frames, width: w, height: h, loopCount: loop); }}关键细节是 dispose 操作:每帧只存增量区域(disposal=0 背景保留 / 1 画布清空 / 2 恢复上一帧),必须在解码阶段就按帧序合成完整 RGBA,否则播放到后面画面会串残影。
播放调度与渲染
sequenceDiagram
participant U as 用户
participant P as ApngPlayer
participant F as ApngFrameView
participant S as 屏幕
U->>P: play() / setSpeed(2.0)
loop 每一帧
P->>P: Timer(durationMs / speed) 推进帧号
P-->>F: notifyListeners → currentFrameData
F->>F: decodeImageFromPixels → ui.Image
F->>S: CustomPainter.paint() 居中绘制
end
U->>P: gotoFrame(n)(进度条拖动)
U->>P: nextFrame() / prevFrame()(步进)ApngPlayer是个ChangeNotifier:Timer按当前帧durationMs / speed自动推进,notifyListeners()触发 UI 重建。ApngFrameView收 RGBA 字节 →decodeImageFromPixels上传成 GPU 纹理ui.Image→CustomPainter保持宽高比居中绘制。- 播放控制栏:上一帧 / 播放暂停 / 下一帧 / 速度(0.25×~4×)/ 进度条拖拽跳转。
- 静态 PNG 直接走
PhotoView双指缩放,不进播放器。
Android 原生桥
MainActivity.kt 暴露两个 MethodChannel 方法:
| 方法 | 行为 |
|---|---|
pickApngFile | SAF ACTION_OPEN_DOCUMENT,MIME 限定 image/png,image/apng,选完复制到应用缓存返回路径 |
getPendingFile | 冷启动时取外部「打开方式」传进来的文件(文件管理器分享 / 打开) |
Kotlin 侧片段:
// android/app/src/main/kotlin/.../MainActivity.kt(节选)private fun openFilePicker(result: MethodChannel.Result) { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) type = "image/*" putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/png", "image/apng")) } // 结果 URI → copyToCache() → 返回缓存文件绝对路径给 Dart}构建产物与发布
Flutter release 构建按 ABI 拆分,arm64 是主产物:
# .github/workflows 核心步骤- uses: subosito/flutter-action@v2 with: flutter-version: '3.47.2'- run: flutter pub get- run: flutter build apk --release --split-per-abi # arm64 / armeabi-v7a- run: flutter build appbundle --release # Play Store 用APK 内部结构:
app-release.apk├── classes.dex # Kotlin/Java 薄桥├── lib/arm64-v8a/libapp.so # Dart AOT 快照(全部业务逻辑)├── lib/arm64-v8a/libflutter.so# Flutter 引擎(剥离符号 11.7MB)├── assets/flutter_assets/ # 字体 / 图标 / 清单└── resources.arsc # 资源表小结
| 层 | 组成 | 规模 |
|---|---|---|
| 表现 | HomePage / ViewerPage / ApngFrameView | 3 个页面组件 |
| 状态 | ApngViewerApp / ApngPlayer | 2 个控制器 |
| 数据 | ApngDecoder / SharedPreferences | 1 个解码器 |
| 平台 | MainActivity.kt(MethodChannel×2) | 约 100 行原生 |
功能少而准:打开、解码、播放、调速、步进、缩放,全部离线。源码在 github.com/TsangAsuna/APNG-View-for-Android,MIT 协议。
APNG 阅览器 —— 架构与开发教程
https://tsangasuna.pages.dev/posts/apng-viewer/分享文章
生成精美分享图或复制链接,与更多人分享本文。
继续阅读
换条路线
从其他文章中稳定抽取
最后更新于 ,距今已过 40 天
部分内容可能已过时