885 words
4 minutes
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 方法:

方法行为
pickApngFileSAF 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 / ApngFrameView3 个页面组件
状态ApngViewerApp / ApngPlayer2 个控制器
数据ApngDecoder / SharedPreferences1 个解码器
平台MainActivity.kt(MethodChannel×2)约 100 行原生

功能少而准:打开、解码、播放、调速、步进、缩放,全部离线。源码在 github.com/TsangAsuna/APNG-View-for-Android,MIT 协议。

APNG 阅览器 —— 架构与开发教程
https://tsangasuna.pages.dev/posts/apng-viewer/
Author
TsangAsuna
Published at
2026-09-15

Share Article

Generate a share poster or copy the link to share this article.

Continue reading

Take another route

A consistent pick from other articles