鸿蒙6.0应用开发——基础动画实践案例

文章目录

水波动画

  • 场景描述

在本场景中,圆形按钮上会依次出现多个水波状圆环,这些圆环从中心向外进行扩散,进而凸显功能,实现效果如图所示。

图7 使用显式动画实现水波纹动效

  • 实现原理

水波圆环以圆形按钮为中心,将多个圆形图层逐渐向外扩展放大,每个圆形图层的动画开始时间稍微错开,进而形成多个水波圆环依次扩散的效果。其动效实现步骤如下。

  1. 实现圆形图层,以圆形图层作为水波圆环的基础形状,并设置相关背景属性。
  2. 通过显示动画animateTo实现圆形图层放大的动效,并设置延迟时间。
  • 开发步骤

实现圆形图层,通过Stack将圆形图层与Button组件进行重叠。

typescript 复制代码
@Component
struct ButtonWithWaterRipples {
  @Link isListening: boolean;
  @State immediatelyOpacity: number = 0.5;
  @State immediatelyScale: Scale = { x: 1, y: 1 };
  @State delayOpacity: number = 0.5;
  @State delayScale: Scale = { x: 1, y: 1 };
  private readonly BUTTON_SIZE: number = 120;
  private readonly BUTTON_CLICK_SCALE: number = 0.8;
  private readonly ANIMATION_DURATION: number = 1300;

  @Styles
  ripplesStyle() {
    .width(this.BUTTON_SIZE * this.BUTTON_CLICK_SCALE)
    .height(this.BUTTON_SIZE * this.BUTTON_CLICK_SCALE)
    .borderRadius(this.BUTTON_SIZE * this.BUTTON_CLICK_SCALE / 2)
    .backgroundColor(Color.Red)
  }

  build() {
    Stack() {
      Stack()
        .ripplesStyle()
        .opacity(this.immediatelyOpacity)
        .scale(this.immediatelyScale)
      Stack()
        .ripplesStyle()
        .opacity(this.delayOpacity)
        .scale(this.delayScale)
      Button() {
        Image($r('app.media.ic_public_music_filled'))
          .width($r('app.float.water_ripples_width'))
          .fillColor(Color.White)
      }
      .clickEffect({ level: ClickEffectLevel.HEAVY, scale: this.BUTTON_CLICK_SCALE })
      .backgroundColor($r('app.color.music_icon'))
      .type(ButtonType.Circle)
      .width(this.BUTTON_SIZE)
      .height(this.BUTTON_SIZE)
      .zIndex(1)
      // ...
    }
  }
}

代码逻辑走读:

  1. 组件定义与状态初始化
    • 定义了一个ButtonWithWaterRipples组件,并初始化了几个状态变量,如immediatelyOpacityimmediatelyScaledelayOpacitydelayScale,用于控制水波纹的透明度和缩放。
  2. 样式定义
    • 定义了一个ripplesStyle方法,用于设置水波纹的样式,包括宽度、高度、边框半径和背景颜色。
  3. 构建UI结构
    • 使用Stack布局来构建UI,内部包含两个Stack,分别用于显示立即的水波纹和延迟的水波纹。
    • 每个Stack应用了ripplesStyle样式,并根据状态变量设置了透明度和缩放比例。
  4. 按钮控件
    • Stack布局中嵌套了一个Button控件,按钮内部包含一个Image,用于显示按钮的图标。
    • 按钮设置了点击效果,包括点击级别和缩放比例。
    • 按钮的背景颜色和类型也进行了设置。
  5. 水波纹效果实现
    • 通过状态变量的变化,实现了水波纹的淡入淡出和缩放效果,当按钮被点击时,水波纹的透明度和缩放比例会发生变化,从而产生水波纹效果。

实现圆形图层的放大动效,并设置延迟时间。

typescript 复制代码
Button() {
  Image($r('app.media.ic_public_music_filled'))
    .width($r('app.float.water_ripples_width'))
    .fillColor(Color.White)
}
.clickEffect({ level: ClickEffectLevel.HEAVY, scale: this.BUTTON_CLICK_SCALE })
.backgroundColor($r('app.color.music_icon'))
.type(ButtonType.Circle)
.width(this.BUTTON_SIZE)
.height(this.BUTTON_SIZE)
.zIndex(1)
.onClick(() => {
  this.isListening = !this.isListening;
  if (this.isListening) {
    this.getUIContext().animateTo({
      duration: this.ANIMATION_DURATION,
      iterations: CommonConstants.ITERATIONS,
      curve: Curve.EaseInOut
    }, () => {
      this.immediatelyOpacity = CommonConstants.COMMON_NUMBER_0;
      this.immediatelyScale = {
        x: CommonConstants.COMMON_NUMBER_6,
        y: CommonConstants.COMMON_NUMBER_6
      };
    })
    this.getUIContext().animateTo({
      duration: this.ANIMATION_DURATION,
      iterations: CommonConstants.ITERATIONS,
      curve: Curve.EaseInOut,
      delay: CommonConstants.DELAY_200
    }, () => {
      this.delayOpacity = CommonConstants.COMMON_NUMBER_0;
      this.delayScale = {
        x: CommonConstants.COMMON_NUMBER_6,
        y: CommonConstants.COMMON_NUMBER_6
      };
    })
  } else {
    // Break the animation by modifying the variable with a closure of duration 0.
    this.getUIContext().animateTo({ duration: CommonConstants.COMMON_NUMBER_0 }, () => {
      this.immediatelyOpacity = CommonConstants.COMMON_NUMBER;
      this.delayOpacity = CommonConstants.COMMON_NUMBER;
      this.immediatelyScale = {
        x: CommonConstants.COMMON_NUMBER_1,
        y: CommonConstants.COMMON_NUMBER_1
      };
      this.delayScale = {
        x: CommonConstants.COMMON_NUMBER_1,
        y: CommonConstants.COMMON_NUMBER_1
      };
    })
  }
})

代码逻辑走读:

  1. 按钮定义
    • 使用 Button()创建一个按钮组件。
    • 按钮内部包含一个 Image组件,显示音乐图标。
  2. 图像设置
    • 图像的宽度通过 $r('app.float.water_ripples_width')获取。
    • 图像的填充颜色设置为白色。
  3. 点击效果
    • 设置按钮的点击效果为 ClickEffectLevel.HEAVY,并且缩放比例为 this.BUTTON_CLICK_SCALE
  4. 背景颜色
    • 按钮的背景颜色通过 $r('app.color.music_icon')设置。
  5. 按钮类型和尺寸
    • 按钮类型设置为 ButtonType.Circle
    • 按钮的宽度和高度都设置为 this.BUTTON_SIZE
    • 按钮的 zIndex设置为 1。
  6. 点击事件处理
    • 当按钮被点击时,this.isListening状态会被切换。
    • 如果this.isListening为真,按钮会启动两个动画:
      • 第一个动画立即改变按钮的透明度和缩放比例。
      • 第二个动画在 200 毫秒延迟后继续改变按钮的透明度和缩放比例。
    • 如果 this.isListening为假,按钮会中断动画并恢复到初始状态。

微动画

  • 场景描述

如图所示,在本场景中,在登录页面前需要勾选相关的协议,如果未勾选相关协议,提示框将会通过左右移动进行提示。

图8 使用关键帧动画实现左右移动提示

  • 实现原理

提示框左右移动提醒是将提示框进行左移,然后再进行右移,如此往复循环多次。其动效可以分为提示框左移和提示框右移两段,可以使用keyframeAnimateTo接口实现分段的动画效果,实现步骤如下所示。

  1. 根据需要实现的动画效果,将动画拆分成若干个关键帧,即将动画进行分段,如本案例中将动画分成提示框左移和提示框右移两段。
  2. 根据不同的关键帧设置关键帧状态,即设置该段关键帧动画的持续时间、动画曲线和闭包函数等。
  3. 设置动画触发条件,使用通用事件点击、出现等,选择对应需求的触发方式。
  • 开发步骤

通过keyframeAnimateTo来设置关键帧动画。

typescript 复制代码
startAnimation() {
  if (!this.uiContext) {
    return;
  }
  this.translateX = CONFIGURATION.POSITION_ZERO;
  this.uiContext.keyframeAnimateTo({ iterations: CONFIGURATION.PLAYBACK_COUNT }, [
    {
      duration: CONFIGURATION.ANIMATION_TIME,
      event: () => {
        this.translateX = CONFIGURATION.TRANSLATE_OFFSET_X;
      }
    },
    {
      duration: CONFIGURATION.ANIMATION_TIME,
      event: () => {
        this.translateX = CONFIGURATION.POSITION_ZERO;
      }
    }
  ]);
}

代码逻辑走读:

  1. 检查 uiContext是否存在
    • 如果 uiContext不存在,方法直接返回,不执行后续动画代码。
  2. 初始化 translateX属性
    • translateX属性设置为 CONFIGURATION.POSITION_ZERO,作为动画起始位置。
  3. 调用 uiContext.keyframeAnimateTo方法
    • 该方法接受两个参数:动画配置(包括迭代次数)和关键帧数组。
    • 动画配置中,iterations设置为 CONFIGURATION.PLAYBACK_COUNT,决定动画循环次数。
    • 关键帧数组定义了动画的两个阶段:
      • 第一个关键帧持续时间为 CONFIGURATION.ANIMATION_TIME,动画完成后,translateX被设置为 CONFIGURATION.TRANSLATE_OFFSET_X
      • 第二个关键帧同样持续时间为 CONFIGURATION.ANIMATION_TIME,动画完成后,translateX被重置为 CONFIGURATION.POSITION_ZERO

设置onClick事件,通过onClick事件调用关键帧动画。

typescript 复制代码
Button($r('app.string.login_in'))
  .type(ButtonType.Normal)
  .borderRadius($r('app.integer.comm_border_radius'))
  .fontColor($r('app.color.ohos_id_color_background'))
  .fontSize($r('app.integer.login_button_font_size'))
  .width(Constants.LAYOUT_MAX_PERCENT)
  .onClick(() => {
    if (!this.confirm) {
      this.startVibrate();
      this.startAnimation();
    } else {
      try {
        this.getUIContext().getPromptAction().showToast({
          message: $r('app.string.login_text')
        });
      } catch (err) {
        let error = err as BusinessError;
        hilog.error(0x0000, 'PageVibrateEffect', `error code=${error.code}, message=${error.message}`);
      }
    }
  })

代码逻辑走读:

  1. 创建一个按钮组件,按钮文本通过$r('app.string.login_in')获取资源字符串。
  2. 设置按钮类型为Normal
  3. 设置按钮的边框圆角半径,通过$r('app.integer.comm_border_radius')获取资源整数。
  4. 设置按钮的字体颜色,通过$r('app.color.ohos_id_color_background')获取资源颜色。
  5. 设置按钮的字体大小,通过$r('app.integer.login_button_font_size')获取资源整数。
  6. 设置按钮的宽度为常量Constants.LAYOUT_MAX_PERCENT
  7. 为按钮添加点击事件处理函数:
    • 如果this.confirmfalse,则调用this.startVibrate()this.startAnimation()方法。
    • 如果this.confirmtrue,则尝试显示一个提示消息。如果显示失败,捕获错误并记录错误信息。

手势动画

  • 场景描述

在本场景中,页面主要分为标题和列表两个部分,当向下滑动列表时,标题会跟随下滑手势扩展显示详细信息,其实现效果如下所示。

图9 使用属性动画实现手势动效

  • 实现原理

在实现下拉缩放详情中,主要包含了两个部分,分别是列表下拉的手势和下拉后标题和列表的动画,详细实现步骤如下。

  1. 处理手势事件:通过onTouch事件记录当前的触摸点,判断当前的手势是否为向上或向下滑动。
  2. 通过属性动画animation实现标题缩放的效果:当列表向上或向下滑动时,改变列表的高度,并通过属性动画进行平滑过度,从而实现标题区域缩放的效果。
  3. 实现标题内容的平移:在标题区域进行缩放的同时,标题的内容也会同步进行平移,从而实现标题部分整体缩放的效果。
  • 开发步骤

实现手势事件方法。当手指按下时,触发TouchType.Down事件记录当前触碰的位置。手指按压态在屏幕上移动时,可以通过当前的位置与初始位置进行比较,判断手势是否为向上或向下滑动,进而变更列表的高度。

typescript 复制代码
handleTouchEvent(event: TouchEvent): void {
  switch (event.type) {
    case TouchType.Down:
      this.downY = event.touches[0].y;
      this.lastMoveY = event.touches[0].y;
      this.isMoving = true;
      this.duration = Constants.ANIMATE_DURATION_DRAG;
      break;

    case TouchType.Move:
      const delta = event.touches[0].y - this.lastMoveY;
      this.offsetY = event.touches[0].y - this.downY;
      if (delta < 0) {
        this.heightValue = Constants.AREA_HEIGHT_BEFORE;
        this.isExpanded = false;
        this.atStart = false;
      }
      if (delta > 0 && this.atStart) {
        this.animateToThrottle(() => {
          this.heightValue = Constants.AREA_HEIGHT_AFTER;
          this.isExpanded = true;
        }, 1000);
      }
      this.lastMoveY = event.touches[0].y;
      break;

    case TouchType.Cancel:
      this.isMoving = false;
      this.duration = Constants.ANIMATE_DURATION_BACK;
      break;
    default:
      break;
  }
}

代码逻辑走读:

  1. 事件类型判断 :函数首先通过switch语句判断触摸事件的类型。
  2. 按下事件处理
    • 当事件类型为TouchType.Down时,记录按下时的Y坐标downYlastMoveY
    • 设置isMovingtrue,表示开始移动。
    • 设置动画持续时间duration为拖动动画的持续时间Constants.ANIMATE_DURATION_DRAG
  3. 移动事件处理
    • 当事件类型为TouchType.Move时,计算当前Y坐标与上一次移动的Y坐标差delta
    • 更新offsetY为当前Y坐标减去按下时的Y坐标。
    • 如果delta小于0且atStartfalse,则设置heightValueConstants.AREA_HEIGHT_BEFORE,并将isExpanded设置为false,同时将atStart设置为false
    • 如果delta大于0且atStarttrue,则调用animateToThrottle函数,在1秒后将heightValue设置为Constants.AREA_HEIGHT_AFTER,并将isExpanded设置为true
    • 更新lastMoveY为当前Y坐标。
  4. 取消事件处理
    • 当事件类型为TouchType.Cancel时,设置isMovingfalse,表示停止移动。
    • 设置动画持续时间duration为返回动画的持续时间Constants.ANIMATE_DURATION_BACK
  5. 默认情况
    • 如果事件类型不属于上述任何一种,则不做任何处理。

为列表设置触摸事件onTouch和属性动画,实现标题区域缩放的效果。

typescript 复制代码
Column() {
  List({ space: Constants.SEARCH_MEMO_SPACE }) {
    ListItem() {
      Search({ placeholder: $r('app.string.search_placeholder') })
        .width(Constants.LAYOUT_MAX_PERCENT)
        .height(Constants.LAYOUT_EIGHT_PERCENT)
        .backgroundColor(Color.White)
        .enableKeyboardOnFocus(false)
    }

    LazyForEach(this.memoData, (item: MemoInfo) => {
      ListItem() {
        MemoItem({ memoItem: item })
      }
    }, (item: MemoInfo) => JSON.stringify(item))
  }
  .scrollBar(BarState.Off)
  .margin({ left: $r('app.float.layout_10'), right: $r('app.float.layout_10') })
  .width(Constants.LAYOUT_NINETY_PERCENT)
  .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
  .onReachStart(() => {
    this.atStart = true;
  })
}
.width(Constants.LAYOUT_MAX_PERCENT)
.height(this.heightValue)
.animation({ duration: this.duration, curve: Curve.FastOutLinearIn })
.onTouch((event: TouchEvent) => this.handleTouchEvent(event))

代码逻辑走读:

  1. 组件结构
    • 使用Column组件作为根容器,用于垂直排列子组件。
    • Column中嵌套了一个List组件,用于展示列表项。
  2. 列表项定义
    • List组件包含一个ListItem,内部嵌套了一个Search组件,用于搜索功能。
    • Search组件设置了占位符文本、宽度、高度、背景颜色,并禁用了键盘弹出。
  3. 数据加载与展示
    • 使用LazyForEach组件动态加载并展示memoData中的备忘录信息。
    • 每个备忘录信息通过MemoItem组件展示。
  4. 列表属性设置
    • List组件设置了滚动条隐藏、左右边距、宽度、展开安全区域等属性。
    • 当列表滚动到顶部时,设置atStarttrue
  5. 组件属性设置
    • Column组件设置了宽度、高度、动画效果、触摸事件处理等属性。

通过animation属性动画实现标题内容平移,从而达到标题整体缩放的效果。

typescript 复制代码
Column() {
  Row() {
    Text(!this.isExpanded ? $r('app.string.memo_title') : '')
      .fontSize($r('app.float.init_title_font_size'))
    Blank()
    // Image($r('app.media.is_public_add'))
    //   .width($r('app.float.menu_pic_layout'))
    //   .height($r('app.float.menu_pic_layout'))
    // Image($r('app.media.ic_public_more'))
    //   .width($r('app.float.menu_pic_layout'))
    //   .height($r('app.float.menu_pic_layout'))
    //   .margin({ left: $r('app.float.layout_8') })
  }
  .width(Constants.LAYOUT_MAX_PERCENT)
  .padding($r('app.float.layout_25'))
  .margin({ top: $r('app.float.layout_10') })
  .alignItems(VerticalAlign.Center)
  .translate(this.getMenuTranslateOptions())
  .animation({ duration: this.duration, curve: Curve.FastOutLinearIn })

  Column() {
    Text($r('app.string.memo_title'))
      .fontSize($r('app.float.expanded_title_font_size'))
    Text($r('app.string.memo_counts'))
      .fontSize($r('app.float.memo_counts_font_size'))
      .fontColor(Color.Grey)
  }
  .width(Constants.LAYOUT_MAX_PERCENT)
  .padding({ left: $r('app.float.layout_25') })
  .margin({ top: $r('app.float.layout_10') })
  .alignItems(HorizontalAlign.Start)
  .translate(this.getTitleTranslateOptions())
  .scale(this.getTitleScaleOptions())
  .animation({ duration: this.duration, curve: Curve.FastOutLinearIn })
  .transition({ type: TransitionType.Insert, translate: { y: Constants.TRANSLATE_Y } })
  .visibility(this.isExpanded ? Visibility.Visible : Visibility.Hidden)
}
.height(Constants.LAYOUT_MAX_PERCENT)
.width(Constants.LAYOUT_MAX_PERCENT)

代码逻辑走读:

  1. 整体布局
    • 使用Column组件作为根布局,包含两个子布局,一个是Row,另一个是嵌套的Column
  2. 第一部分:Row布局
    • 包含一个Text组件,根据this.isExpanded的值显示或隐藏标题文本。
    • 使用Blank组件作为占位符,可能用于未来扩展。
    • Row组件设置了宽度、内边距、外边距、对齐方式以及动画效果,用于控制标题行的显示和位置。
  3. 第二部分:嵌套的Column布局
    • 包含两个Text组件,分别显示扩展后的标题和副标题。
    • 设置了宽度、内边距、外边距、对齐方式、缩放比例以及动画效果,用于控制详细信息的显示和位置。
    • 使用transition属性实现插入动画,当this.isExpanded为真时,显示该布局。
    • 使用visibility属性控制布局的可见性,根据this.isExpanded的值显示或隐藏。
  4. 动画和状态控制
    • 使用animationtranslatescale等属性来实现平滑的过渡效果。
      Row布局**:
    • 包含一个Text组件,根据this.isExpanded的值显示或隐藏标题文本。
    • 使用Blank组件作为占位符,可能用于未来扩展。
    • Row组件设置了宽度、内边距、外边距、对齐方式以及动画效果,用于控制标题行的显示和位置。
相关推荐
zhengyquan3 小时前
华为 Pura X Max 将至:阔折叠再升级,4 月 20 日发布!
华为
Swift社区4 小时前
鸿蒙游戏中的“智能 NPC”架构设计
游戏·华为·harmonyos
特立独行的猫a6 小时前
HarmonyOS 鸿蒙PC三方库移植:vcpkg方式的 Port 脚本编写简明教程
华为·harmonyos·openharmony·vcpkg·三方库移植
GLAB-Mary6 小时前
华为职业认证新版全景图介绍及重认证规则变更预通知
运维·服务器·华为·华为认证
Ww.xh6 小时前
鸿蒙Flutter混合开发实战:跨平台UI无缝集成
flutter·华为·harmonyos
苏杰豪8 小时前
DevEco Studio 启动鸿蒙模拟器提示未开启 Hyper-V,怎么处理?
华为·harmonyos
chenjixue8 小时前
记录下我理解的安卓,鸿蒙,ios, rn , fullter, Jetpack Compose,react 的相似与不同
android·华为·harmonyos
想你依然心痛8 小时前
HarmonyOS 6(API 23)悬浮导航与沉浸光感实战:打造下一代玻璃拟态UI体验
ui·华为·harmonyos·悬浮导航·沉浸光感
yumgpkpm8 小时前
Qwen3.6正式开源,华为昇腾910B实现高效适配
华为·ai作画·stable diffusion·开源·ai写作·llama·gpu算力