这个案例来自一个很小的 loading 动效:8 张 PNG,每张显示 80ms,循环播放。
Android 工程 LoadingFrames 已跑过完整构建。HarmonyOS 工程 LoadingImage 已完成 ArkTS 编译并生成 unsigned HAP,真机运行也已实测。这个案例值得记录的点不在 API 名字,而在资源系统和组件状态的分工:Android 把帧顺序和时长写进 drawable XML,HarmonyOS 把同一组信息交给 ImageAnimator。
下面这张是示例里的第一帧。运行时,组件按 loading_frame_00 到 loading_frame_07 的顺序播放。

Android:播放逻辑写在 drawable XML 里
Android 这边的核心文件是:
app/src/main/res/drawable/loading_frames.xml
app/src/main/res/drawable-nodpi/loading_frame_00.png
...
app/src/main/res/drawable-nodpi/loading_frame_07.png
app/src/main/java/com/example/loadingframes/MainActivity.java
loading_frames.xml 使用 animation-list:
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false">
<item
android:drawable="@drawable/loading_frame_00"
android:duration="80" />
<item
android:drawable="@drawable/loading_frame_01"
android:duration="80" />
<item
android:drawable="@drawable/loading_frame_02"
android:duration="80" />
<item
android:drawable="@drawable/loading_frame_03"
android:duration="80" />
<item
android:drawable="@drawable/loading_frame_04"
android:duration="80" />
<item
android:drawable="@drawable/loading_frame_05"
android:duration="80" />
<item
android:drawable="@drawable/loading_frame_06"
android:duration="80" />
<item
android:drawable="@drawable/loading_frame_07"
android:duration="80" />
</animation-list>
Android 官方文档把这种资源称为 frame animation:XML 定义一组按顺序显示的图片,编译后是指向 AnimationDrawable 的资源。android:oneshot="false" 表示循环,android:duration 是每帧毫秒数。
Activity 里只需要把这个 drawable 放进 ImageView,再启动 AnimationDrawable:
loadingImage.setImageResource(R.drawable.loading_frames);
Drawable drawable = loadingImage.getDrawable();
if (drawable instanceof AnimationDrawable) {
loadingAnimation = (AnimationDrawable) drawable;
}
startButton.setOnClickListener(v -> startLoadingAnimation());
stopButton.setOnClickListener(v -> stopLoadingAnimation());
启动时机也有一个小坑。AnimationDrawable.start() 不适合在视图还没 attach 完的时候直接调用,所以示例里放在 onStart() 后再 post 一次:
@Override
protected void onStart() {
super.onStart();
loadingImage.post(this::startLoadingAnimation);
}
@Override
protected void onStop() {
stopLoadingAnimation();
super.onStop();
}
这就是 Android 版本的特点:PNG 是资源,帧顺序和播放时长也写在资源 XML 里。ImageView 拿到的是一个已经带播放语义的 drawable。
HarmonyOS:图片仍是资源,播放语义移到 ArkUI 组件
HarmonyOS 里没有同名的 res/drawable/animation-list.xml 机制。空应用迁移后,文件拆成两类:
entry/src/main/resources/base/media/loading_frame_00.png
...
entry/src/main/resources/base/media/loading_frame_07.png
entry/src/main/ets/model/LoadingFrames.ets
entry/src/main/ets/pages/Index.ets
LoadingFrames.ets 只维护帧资源和时长:
export const FRAME_DURATION_MS: number = 80;
export const FRAME_RESOURCE_NAMES: string[] = [
'loading_frame_00',
'loading_frame_01',
'loading_frame_02',
'loading_frame_03',
'loading_frame_04',
'loading_frame_05',
'loading_frame_06',
'loading_frame_07'
];
export function createLoadingFrameInfos(): ImageFrameInfo[] {
return [
{ src: $r('app.media.loading_frame_00'), duration: FRAME_DURATION_MS },
{ src: $r('app.media.loading_frame_01'), duration: FRAME_DURATION_MS },
{ src: $r('app.media.loading_frame_02'), duration: FRAME_DURATION_MS },
{ src: $r('app.media.loading_frame_03'), duration: FRAME_DURATION_MS },
{ src: $r('app.media.loading_frame_04'), duration: FRAME_DURATION_MS },
{ src: $r('app.media.loading_frame_05'), duration: FRAME_DURATION_MS },
{ src: $r('app.media.loading_frame_06'), duration: FRAME_DURATION_MS },
{ src: $r('app.media.loading_frame_07'), duration: FRAME_DURATION_MS }
];
}
页面里使用 ImageAnimator:
import {
FRAME_DURATION_MS,
FRAME_RESOURCE_NAMES,
createLoadingFrameInfos
} from '../model/LoadingFrames';
@Entry
@Component
struct Index {
private readonly frameInfos: ImageFrameInfo[] = createLoadingFrameInfos();
private readonly totalDurationMs: number =
FRAME_DURATION_MS * FRAME_RESOURCE_NAMES.length;
@State private animationState: AnimationStatus = AnimationStatus.Running;
build() {
Column() {
Text('PNG frame animation')
.fontSize(26)
.fontWeight(FontWeight.Bold)
Column() {
ImageAnimator()
.images(this.frameInfos)
.duration(this.totalDurationMs)
.iterations(-1)
.state(this.animationState)
.fillMode(FillMode.Forwards)
.fixedSize(true)
.width(164)
.height(164)
}
.width(220)
.height(220)
.padding(28)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.backgroundColor('#F5F7FB')
.border({ width: 1, color: '#D7DDEA' })
.borderRadius(24)
Row({ space: 16 }) {
Button('Start')
.layoutWeight(1)
.onClick(() => {
this.animationState = AnimationStatus.Running;
})
Button('Pause')
.layoutWeight(1)
.onClick(() => {
this.animationState = AnimationStatus.Paused;
})
}
.width('100%')
}
.width('100%')
.height('100%')
.padding(24)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
}
这里的对应关系很清楚:
ImageFrameInfo.src对应每张 PNG 资源。duration可以写在每帧里,也可以用ImageAnimator().duration(...)给整段动画设置总时长。iterations(-1)表示循环播放。state(AnimationStatus.Running / Paused)由 ArkUI 状态控制。
Android 的 animation-list 把顺序、时长、循环写进资源。HarmonyOS 的 ImageAnimator 把这些行为放在组件属性里。PNG 资源本身没有问题,迁移的是“播放语义的位置”。
设备 API 版本也要一起查
这个 demo 迁到真机时还遇到一个常见问题:DevEco 新建空项目默认写的是:
"targetSdkVersion": "6.1.1(24)",
"compatibleSdkVersion": "6.1.1(24)"
当前连接设备查出来是:
hdc list targets -v
192.168.3.3:33411 TCP Connected
hdc shell param get const.ohos.apiversion
23
hdc shell param get const.ohos.fullname
OpenHarmony-6.1.0.115
因此工程要改成:
"targetSdkVersion": "6.1.0(23)",
"compatibleSdkVersion": "6.1.0(23)"
这个检查很容易被忘掉。Android 迁 HarmonyOS 时,UI 代码能编过不代表真机能安装,先用 hdc shell param get const.ohos.apiversion 看设备 API,比在 DevEco 里盲猜版本省时间。
实测验证
Android 侧:
./gradlew :app:assembleDebug
BUILD SUCCESSFUL
HarmonyOS 侧:
/Users/kai/Library/Huawei/command-line-tools/bin/hvigorw assembleHap --no-daemon --stacktrace
Finished :entry:default@CompileArkTS
Finished :entry:default@PackageHap
HarmonyOS 产物:
entry/build/default/outputs/default/entry-default-unsigned.hap
如果 DevEco 已经写入本机签名配置,assembleHap 会继续走签名任务;如果签名证书、JDK 或密码不匹配,失败点会落在 SignHap。这个差异不影响本文讨论的 ImageAnimator 迁移,判断 demo 迁移是否成立时,先看 CompileArkTS、PackageHap 和 unsigned HAP。
这类迁移可以怎么判断
这个案例可以整理成一条简单规则:
| Android 侧对象 | 迁到 HarmonyOS 时先看什么 | 本案例里的结果 |
|---|---|---|
res/drawable/animation-list.xml |
资源里是否包含行为语义 | 包含帧顺序、时长、循环 |
AnimationDrawable |
HarmonyOS 是否有组件级承接对象 | ImageAnimator |
| PNG 帧资源 | 是否能作为普通 media 资源保留 | 可以,放到 resources/base/media |
start() / stop() |
播放状态由谁持有 | ArkUI @State + AnimationStatus |
targetSdkVersion |
真机 API 是否低于项目声明 | 设备 API 23,项目要改成 6.1.0(23) |
所以这不是一个“Android 不能迁”的例子。它更像一个小型解剖:Android 把播放语义塞进了 drawable 资源系统,HarmonyOS 用 ArkUI 组件把这段行为显式写出来。
实测后的结论是:PNG 序列帧动效可以完整迁移。迁移时别寻找 animation-list 的同名 XML,直接把它改写成 ImageAnimator 的帧数组、播放时长、循环次数和状态控制。
参考
- Android Developers: Animation resources / Frame animation
- Android Developers: AnimationDrawable
- HarmonyOS References: ImageAnimator
// Kai@CodeHubble // 观测坐标:Android-HarmonyOS/2026-07-07