组件动画
====

ArkUI为组件提供了通用的属性动画和转场动画能力的同时，还为一些组件提供了默认的动画效果。例如，[List](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-list)的滑动动效、[Button](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-basic-components-button)的点击动效，是组件自带的默认动画效果。在组件默认动画效果的基础上，开发者还可以通过属性动画和转场动画对容器组件内的子组件动效进行定制。

#### 使用组件默认动画

组件默认动效具备以下功能：

*   提示用户当前状态，例如用户点击Button组件时，Button组件默认变灰，用户即确定完成选中操作。
    
*   提升界面精致程度和生动性。
    
*   减少开发者工作量，例如列表滑动组件自带滑动动效，开发者直接调用即可。
    

以下给出Checkbox的默认动画示例和效果。更多效果，请参考对应的组件说明。

示例代码和效果如下。

@Entry
@Component
struct ComponentDemo {
  build() {
    Row() {
      Checkbox({ name: 'checkbox1', group: 'checkboxGroup' })
        .select(true)
        .shape(CheckBoxShape.CIRCLE)
        .size({ width: 50, height: 50 })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/66/v3/MqU_QergTautIDBt6Iasfw/zh-cn_image_0000002633848712.gif?HW-CC-KV=V1&HW-CC-Date=20260709T132308Z&HW-CC-Expire=86400&HW-CC-Sign=65FA3B12232A56AA614E9E17F179E098DBD8B444171A7FC29FC661B250B14934)

#### 打造组件定制化动效

部分组件支持通过[属性动画](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-attribute-animation-overview)和[转场动画](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-transition-overview)自定义组件子Item的动效，实现定制化动画效果。例如，[Scroll](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-scroll)组件中可对各个子组件在滑动时的动画效果进行定制。

*   在滑动或者点击操作时通过改变各个Scroll子组件的仿射属性来实现各种效果。
    
*   如果要在滑动过程中定制动效，可在滑动回调[onDidScroll](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-scrollable-common#ondidscroll12)中监控滑动距离，并计算每个组件的仿射属性。也可以自己定义手势，通过手势监控位置，手动调用ScrollTo改变滑动位置。
    
*   在滑动回调[onScrollStop](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-basic-components-textpicker#onscrollstop14)或手势结束回调中对滑动的最终位置进行微调。
    

定制Scroll组件滑动动效示例代码和效果如下。

import { curves, window, display, mediaquery, UIContext } from '@kit.ArkUI';
import { UIAbility } from '@kit.AbilityKit';

export default class GlobalContext extends AppStorage {
  static mainWin: window.Window | undefined = undefined;
  static mainWindowSize: window.Size | undefined = undefined;
}

/\*\*
 \* 窗口、屏幕相关信息管理类
 \*/
export class WindowManager {
  private static instance: WindowManager | null = null;
  private displayInfo: display.Display | null = null;
  private uiContext: UIContext;
  private orientationListener: mediaquery.MediaQueryListener;

  constructor(uiContext: UIContext) {
    this.uiContext = uiContext;
    this.orientationListener = this.uiContext.getMediaQuery().matchMediaSync('(orientation: landscape)');
    this.orientationListener.on('change', (mediaQueryResult: mediaquery.MediaQueryResult) => {
      this.onPortrait(mediaQueryResult);
    });
    this.loadDisplayInfo();
  }

  /\*\*
   \* 设置主window窗口
   \* @param win 当前app窗口
   \*/
  setMainWin(win: window.Window) {
    if (win == null) {
      return;
    }
    GlobalContext.mainWin = win;
    win.on('windowSizeChange', (data: window.Size) => {
      if (GlobalContext.mainWindowSize == undefined || GlobalContext.mainWindowSize == null) {
        GlobalContext.mainWindowSize = data;
      } else {
        if (GlobalContext.mainWindowSize.width == data.width && GlobalContext.mainWindowSize.height == data.height) {
          return;
        }
        GlobalContext.mainWindowSize = data;
      }

      let winWidth = this.getMainWindowWidth();
      AppStorage.setOrCreate<number>('mainWinWidth', winWidth);
      let winHeight = this.getMainWindowHeight();
      AppStorage.setOrCreate<number>('mainWinHeight', winHeight);
      let context: UIAbility = new UIAbility();
      context.context.eventHub.emit('windowSizeChange', winWidth, winHeight);
    });
  }

  static getInstance(uiContext: UIContext): WindowManager {
    if (WindowManager.instance == null) {
      WindowManager.instance = new WindowManager(uiContext);
    }
    return WindowManager.instance;
  }

  private onPortrait(mediaQueryResult: mediaquery.MediaQueryResult) {
    if (mediaQueryResult.matches == AppStorage.get<boolean>('isLandscape')) {
      return;
    }
    AppStorage.setOrCreate<boolean>('isLandscape', mediaQueryResult.matches);
    this.loadDisplayInfo();
  }

  /\*\*
   \* 切换屏幕方向
   \* @param ori 常量枚举值：window.Orientation
   \*/
  changeOrientation(ori: window.Orientation) {
    if (GlobalContext.mainWin != null) {
      GlobalContext.mainWin.setPreferredOrientation(ori);
    }
  }

  private loadDisplayInfo() {
    this.displayInfo = display.getDefaultDisplaySync();
    AppStorage.setOrCreate<number>('displayWidth', this.getDisplayWidth());
    AppStorage.setOrCreate<number>('displayHeight', this.getDisplayHeight());
  }

  /\*\*
   \* 获取main窗口宽度，单位vp
   \*/
  getMainWindowWidth(): number {
    return GlobalContext.mainWindowSize != null ? this.uiContext.px2vp(GlobalContext.mainWindowSize.width) : 0;
  }

  /\*\*
   \* 获取main窗口高度，单位vp
   \*/
  getMainWindowHeight(): number {
    return GlobalContext.mainWindowSize != null ? this.uiContext.px2vp(GlobalContext.mainWindowSize.height) : 0;
  }

  /\*\*
   \* 获取屏幕宽度，单位vp
   \*/
  getDisplayWidth(): number {
    return this.displayInfo != null ? this.uiContext.px2vp(this.displayInfo.width) : 0;
  }

  /\*\*
   \* 获取屏幕高度，单位vp
   \*/
  getDisplayHeight(): number {
    return this.displayInfo != null ? this.uiContext.px2vp(this.displayInfo.height) : 0;
  }

  /\*\*
   \* 释放资源
   \*/
  release() {
    if (this.orientationListener) {
      this.orientationListener.off('change', (mediaQueryResult: mediaquery.MediaQueryResult) => {
        this.onPortrait(mediaQueryResult);
      });
    }
    if (GlobalContext.mainWin != null) {
      GlobalContext.mainWin.off('windowSizeChange');
    }
    WindowManager.instance = null;
  }
}

/\*\*
 \* 封装任务卡片信息数据类
 \*/
export class TaskData {
  bgColor: Color | string | Resource = Color.White;
  index: number = 0;
  taskInfo: string = 'music';

  constructor(bgColor: Color | string | Resource, index: number, taskInfo: string) {
    this.bgColor = bgColor;
    this.index = index;
    this.taskInfo = taskInfo;
  }
}

export const taskDataArr: Array<TaskData> =
  \[
    new TaskData('#317AF7', 0, 'music'),
    new TaskData('#D94838', 1, 'mall'),
    new TaskData('#DB6B42', 2, 'photos'),
    new TaskData('#5BA854', 3, 'setting'),
    new TaskData('#317AF7', 4, 'call'),
    new TaskData('#D94838', 5, 'music'),
    new TaskData('#DB6B42', 6, 'mall'),
    new TaskData('#5BA854', 7, 'photos'),
    new TaskData('#D94838', 8, 'setting'),
    new TaskData('#DB6B42', 9, 'call'),
    new TaskData('#5BA854', 10, 'music')

  \];

@Entry
@Component
export struct TaskSwitchMainPage {
  displayWidth: number = WindowManager.getInstance(this.getUIContext()).getDisplayWidth();
  scroller: Scroller = new Scroller();
  cardSpace: number = 0; // 卡片间距
  cardWidth: number = this.displayWidth / 2 - this.cardSpace / 2; // 卡片宽度
  cardHeight: number = 400; // 卡片高度
  cardPosition: Array<number> = \[\]; // 卡片初始位置
  clickIndex: boolean = false;
  @State taskViewOffsetX: number = 0;
  @State cardOffset: number = this.displayWidth / 4;
  lastCardOffset: number = this.cardOffset;
  startTime: number | undefined = undefined;

  // 每个卡片初始位置
  aboutToAppear() {
    for (let i = 0; i < taskDataArr.length; i++) {
      this.cardPosition\[i\] = i \* (this.cardWidth + this.cardSpace);
    }
  }

  // 每个卡片位置
  getProgress(index: number): number {
    let progress = (this.cardOffset + this.cardPosition\[index\] - this.taskViewOffsetX +
      this.cardWidth / 2) / this.displayWidth;
    return progress;
  }

  build() {
    Stack({ alignContent: Alignment.Bottom }) {
      // 背景
      Column()
        .width('100%')
        .height('100%')
        .backgroundColor(0xF0F0F0)

      // 滑动组件
      Scroll(this.scroller) {
        Row({ space: this.cardSpace }) {
          ForEach(taskDataArr, (item: TaskData, index) => {
            Column()
              .width(this.cardWidth)
              .height(this.cardHeight)
              .backgroundColor(item.bgColor)
              .borderStyle(BorderStyle.Solid)
              .borderWidth(1)
              .borderColor(0xAFEEEE)
              .borderRadius(15)
              // 计算子组件的仿射属性
              .scale((this.getProgress(index) >= 0.4 && this.getProgress(index) <= 0.6) ?
                {
                  x: 1.1 - Math.abs(0.5 - this.getProgress(index)),
                  y: 1.1 - Math.abs(0.5 - this.getProgress(index))
                } :
                { x: 1, y: 1 })
              .animation({ curve: Curve.Smooth })
              // 滑动动画
              .translate({ x: this.cardOffset })
              .animation({ curve: curves.springMotion() })
              .zIndex((this.getProgress(index) >= 0.4 && this.getProgress(index) <= 0.6) ? 2 : 1)
          }, (item: TaskData) => item.toString())
        }
        .width((this.cardWidth + this.cardSpace) \* (taskDataArr.length + 1))
        .height('100%')
      }
      .gesture(
        GestureGroup(GestureMode.Parallel,
          PanGesture({ direction: PanDirection.Horizontal, distance: 5 })
            .onActionStart((event: GestureEvent | undefined) => {
              if (event) {
                this.startTime = event.timestamp;
              }
            })
            .onActionUpdate((event: GestureEvent | undefined) => {
              if (event) {
                this.cardOffset = this.lastCardOffset + event.offsetX;
              }
            })
            .onActionEnd((event: GestureEvent | undefined) => {
              if (event) {
                let time = 0;
                if (this.startTime) {
                  time = event.timestamp - this.startTime;
                }
                let speed = event.offsetX / (time / 1000000000);
                let moveX = Math.pow(speed, 2) / 7000 \* (speed > 0 ? 1 : -1);

                this.cardOffset += moveX;
                // 左滑大于最右侧位置
                let cardOffsetMax = -(taskDataArr.length - 1) \* (this.displayWidth / 2);
                if (this.cardOffset < cardOffsetMax) {
                  this.cardOffset = cardOffsetMax;
                }
                // 右滑大于最左侧位置
                if (this.cardOffset > this.displayWidth / 4) {
                  this.cardOffset = this.displayWidth / 4;
                }

                // 左右滑动距离不满足/满足切换关系时，补位/退回
                let remainMargin = this.cardOffset % (this.displayWidth / 2);
                if (remainMargin < 0) {
                  remainMargin = this.cardOffset % (this.displayWidth / 2) + this.displayWidth / 2;
                }
                if (remainMargin <= this.displayWidth / 4) {
                  this.cardOffset += this.displayWidth / 4 - remainMargin;
                } else {
                  this.cardOffset -= this.displayWidth / 4 - (this.displayWidth / 2 - remainMargin);
                }

                // 记录本次滑动偏移量
                this.lastCardOffset = this.cardOffset;
              }
            })
        ), GestureMask.IgnoreInternal)
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)

      // 滑动到首尾位置
      Button('Move to first/last')
        .backgroundColor(0x888888)
        .margin({ bottom: 30 })
        .onClick(() => {
          this.clickIndex = !this.clickIndex;

          if (this.clickIndex) {
            this.cardOffset = this.displayWidth / 4;
          } else {
            this.cardOffset = this.displayWidth / 4 - (taskDataArr.length - 1) \* this.displayWidth / 2;
          }
          this.lastCardOffset = this.cardOffset;
        })
    }
    .width('100%')
    .height('100%')
  }
}

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/9e/v3/f22LxpBaQemg4oEuTO-2yA/zh-cn_image_0000002634008616.gif?HW-CC-KV=V1&HW-CC-Date=20260709T132308Z&HW-CC-Expire=86400&HW-CC-Sign=798498B8B716106C78968D9BD9978FB192649F2AD56AA9DEA433FEC8BBC608A0)

通过animateTo可以实现将List中指定的Item替换到首位，List中其余Item依次向下排列。定制List组件动态替换动效的示例代码和效果如下。

import { curves, AnimatorResult } from '@kit.ArkUI';

// 该接口控制列表项视觉属性
class ListItemModify implements AttributeModifier<ListItemAttribute> {
  public offsetY: number = 0; // Y轴偏移量

  applyNormalAttribute(instance: ListItemAttribute): void {
    instance.translate({ y: this.offsetY }); // Y轴位移
  }
}

@Observed
class DragSortCtrl<T> {
  private arr: Array<T>; // 数据数组
  private modify: Array<ListItemModify>; // 属性修改器数组
  private uiContext: UIContext; // 新增UIContext成员
  private dragRefOffset: number = 0; // 拖拽参考偏移量
  offsetY: number = 0; // 当前Y轴偏移量
  private ITEM\_INTV: number = 0; // 列表项间隔

  constructor(arr: Array<T>, intv: number, uiContext: UIContext) {
    this.arr = arr;
    this.uiContext = uiContext;
    this.modify = \[\];
    this.ITEM\_INTV = intv;
    arr.forEach(() => {
      this.modify.push(new ListItemModify());
    });
  }

  itemMove(index: number, newIndex: number): void {
    let tmp = this.arr.splice(index, 1); // 移除当前传入的index
    this.arr.splice(newIndex, 0, tmp\[0\]); // 将当前移除的index插入到数组前一个位置
    let tmp2 = this.modify.splice(index, 1);
    this.modify.splice(newIndex, 0, tmp2\[0\]);
  }

  setDragRef(item: T): void {
    this.dragRefOffset = 0;
  }

  onMove(item: T, offset: number) {
    this.offsetY = offset - this.dragRefOffset; // 逐帧计算传入的offect，每满足一个item高度时，进入下方if逻辑，更新dragRefOffset的值
    let index = this.arr.indexOf(item); // 在数组中查找传入的item
    this.modify\[index\].offsetY = this.offsetY;
    if (this.offsetY < -this.ITEM\_INTV / 2) { // 通过判断使指定的item逐一移动到首位
      // 使用interpolatingSpring曲线生成弹簧动画
      this.uiContext.animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => { // 400: 弹簧刚度，38: 弹簧阻尼
        this.offsetY += this.ITEM\_INTV; // 调整偏移量实现平滑移动
        this.dragRefOffset -= this.ITEM\_INTV; // 移动的总偏移量
        console.info(\`item offsetY ${this.offsetY} dragRefOffset ${this.dragRefOffset}\`);
        this.itemMove(index, index - 1); // 执行列表项位置交换
      });
    }
  }

  getModify(item: T): ListItemModify {
    let index = this.arr.indexOf(item);
    return this.modify\[index\];
  }
}

@Entry
@Component
struct ListAutoSortExample {
  @State private arr: Array<number> = \[0, 1, 2, 3, 4, 5\]; // 列表数据数组
  @State dragSortCtrl: DragSortCtrl<number> =
    new DragSortCtrl<number>(this.arr, 120, this.getUIContext()); // 120: 列表项高度间隔
  @State firstListItemGroupCount: number = 3; // 第一个列表项组包含的项目数量
  private listScroll: ListScroller = new ListScroller(); // 列表滚动控制器
  private backAnimator: AnimatorResult | null = null; // 动画控制器

  @Builder
  itemEnd(item: number, index: number) {
    Row() {
      Button('To TOP').margin('4vp').onClick(() => { // 4vp: 按钮边距
        console.info(\`item number item ${item} index ${index}\`);
        this.listScroll.closeAllSwipeActions({
          onFinish: () => {
            this.dragSortCtrl.setDragRef(item);
            let length = 120 \* (this.arr.indexOf(item)); // 120: 列表项高度间隔
            this.backAnimator = this.getUIContext()?.createAnimator({
              // 创建弹簧动画
              duration: 1000, // 动画持续时间，单位毫秒
              easing: 'interpolating-spring(0, 1, 150, 24)', // 150: 弹簧刚度，24: 弹簧阻尼
              delay: 0, // 动画延迟时间
              fill: 'none',
              direction: 'normal',
              iterations: 1, // 动画迭代次数
              begin: 0, // 动画起始值
              end: -length
            });
            this.backAnimator.onFrame = (value) => { // 逐帧回调更新位置
              this.dragSortCtrl.onMove(item, value); // 处理list的移动替换动效
            };
            this.backAnimator.onFinish = () => {
            };
            this.backAnimator.play(); // 启动动画
          }
        });
      })
    }
    .padding('4vp').justifyContent(FlexAlign.SpaceEvenly) // 4vp: 内边距
  }

  @Builder
  header(title: string) {
    Row() {
      Text(title)
    }
  }

  build() {
    Row() {
      Column() {
        List({ space: 20, scroller: this.listScroll }) { // 20: 列表项间距
          ListItemGroup({ header: this.header('first ListItemGroup'), space: 20 }) { // 20: 列表项组内间距
            ForEach(this.arr, (item: number, index) => {
              if (index < this.firstListItemGroupCount) {
                ListItem() {
                  Text('' + item)
                    .width('100%')
                    .height(100) // 100: 列表项高度
                    .fontSize(16) // 16: 字体大小
                    .borderRadius(10) // 10: 边框圆角半径
                    .textAlign(TextAlign.Center)
                    .backgroundColor(0xFFFFFF) // 0xFFFFFF: 白色背景
                }
                .swipeAction({
                  end: this.itemEnd(item, index)
                })
                .clip(true)
                .attributeModifier(this.dragSortCtrl.getModify(item)) // 动态设置属性修改
                .borderRadius(10) // 10: 边框圆角半径
                .margin({ left: 20, right: 20 }) // 20: 左右外边距
              }
            })
          }

          ListItemGroup({ header: this.header('second ListItemGroup'), space: 20 }) { // 20: 列表项组内间距
            ForEach(this.arr, (item: number, index) => {
              if (index > this.firstListItemGroupCount - 1) { // 1: 索引偏移量
                ListItem() {
                  Text('' + item)
                    .width('100%')
                    .height(100) // 100: 列表项高度
                    .fontSize(16) // 16: 字体大小
                    .borderRadius(10) // 10: 边框圆角半径
                    .textAlign(TextAlign.Center)
                    .backgroundColor(0xFFFFFF) // 0xFFFFFF: 白色背景
                }
                .swipeAction({
                  end: this.itemEnd(item, index)
                })
                .clip(true)
                .attributeModifier(this.dragSortCtrl.getModify(item))
                .borderRadius(10) // 10: 边框圆角半径
                .margin({ left: 20, right: 20 }) // 20: 左右外边距
              }
            })
          }
        }
        .padding({ top: 20 }) // 20: 顶部内边距
        .height('100%')
      }
    }
    .backgroundColor(0xDCDCDC) // 0xDCDCDC: 浅灰色背景
  }
}

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/87/v3/CKClbrASQ5eob_1RlunVJA/zh-cn_image_0000002664207763.gif?HW-CC-KV=V1&HW-CC-Date=20260709T132308Z&HW-CC-Expire=86400&HW-CC-Sign=2DB367301F11B8B3877749C22E2A6FFFAE1771A0DC4DD0D261545E389B85EF30)

#### 示例代码

*   [实现动效功能合集](https://gitcode.com/HarmonyOS_Samples/animation-collection)