Android animation-list 到 HarmonyOS ImageAnimator:PNG 序列帧迁移实测

这个案例来自一个很小的 loading 动效:8 张 PNG,每张显示 80ms,循环播放。

Android 工程 LoadingFrames 已跑过完整构建。HarmonyOS 工程 LoadingImage 已完成 ArkTS 编译并生成 unsigned HAP,真机运行也已实测。这个案例值得记录的点不在 API 名字,而在资源系统和组件状态的分工:Android 把帧顺序和时长写进 drawable XML,HarmonyOS 把同一组信息交给 ImageAnimator

下面这张是示例里的第一帧。运行时,组件按 loading_frame_00loading_frame_07 的顺序播放。

loading_frame_00

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 迁移是否成立时,先看 CompileArkTSPackageHap 和 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 的帧数组、播放时长、循环次数和状态控制。

参考

// Kai@CodeHubble // 观测坐标:Android-HarmonyOS/2026-07-07

上一篇
下一篇