0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

OpenHarmony实战开发-如何实现组件动画。

码牛程序猿 来源:jf_71304091 作者:jf_71304091 2024-04-28 15:49 次阅读

ArkUI为组件提供了通用的属性动画和转场动画能力的同时,还为一些组件提供了默认的动画效果。例如,List的滑动动效,Button的点击动效,是组件自带的默认动画效果。在组件默认动画效果的基础上,开发者还可以通过属性动画和转场动画对容器组件内的子组件动效进行定制。

使用组件默认动画

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

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

更多效果,可以参考组件说明。

示例代码和效果如下。

@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)
  }
}
ts

打造组件定制化动效

部分组件支持通过属性动画和转场动画自定义组件子Item的动效,实现定制化动画效果。例如,Scroll组件中可对各个子组件在滑动时的动画效果进行定制。

  • 在滑动或者点击操作时通过改变各个Scroll子组件的仿射属性来实现各种效果。
  • 如果要在滑动过程中定制动效,可在滑动回调onScroll中监控滑动距离,并计算每个组件的仿射属性。也可以自己定义手势,通过手势监控位置,手动调用ScrollTo改变滑动位置。
  • 在滑动回调onScrollStop或手势结束回调中对滑动的最终位置进行微调。

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

import curves from '@ohos.curves';
import window from '@ohos.window';
import display from '@ohos.display';
import mediaquery from '@ohos.mediaquery';
import UIAbility from '@ohos.app.ability.UIAbility';

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 orientationListener = mediaquery.matchMediaSync('(orientation: landscape)');

  constructor() {
    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(): WindowManager {
    if (WindowManager.instance == null) {
      WindowManager.instance = new WindowManager();
    }
    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 ? px2vp(GlobalContext.mainWindowSize.width) : 0
  }

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

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

  /**
   * 获取屏幕高度,单位vp
   */
  getDisplayHeight(): number {
    return this.displayInfo != null ? 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().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%')
  }
}
ts

审核编辑 黄宇

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 组件
    +关注

    关注

    1

    文章

    506

    浏览量

    17809
  • 鸿蒙
    +关注

    关注

    57

    文章

    2325

    浏览量

    42766
  • OpenHarmony
    +关注

    关注

    25

    文章

    3682

    浏览量

    16183
收藏 人收藏

    评论

    相关推荐

    HarmonyOS开发案例:【购物车app】

    OpenHarmony ArkUI框架提供了丰富的动画组件和接口,开发者可以根据实际场景和开发需求,选用丰富的
    的头像 发表于 05-14 18:19 1017次阅读
    HarmonyOS<b class='flag-5'>开发</b>案例:【购物车app】

    OpenHarmony实战开发-如何实现动画

    你们的 『点赞和评论』,才是我创造的动力。 关注小编,同时可以期待后续文章ing?,不定期分享原创知识。 更多鸿蒙最新技术知识点,请关注作者博客:鸿蒙实战经验分享:鸿蒙基础入门开发宝典! (qq.com)**
    发表于 05-06 14:11

    OpenHarmony实战开发-如何实现窗口开发概述

    操作系统而言,窗口模块提供了不同应用界面的组织管理逻辑。 窗口模块的用途 在OpenHarmony中,窗口模块主要负责以下职责: 提供应用和系统界面的窗口对象。 应用开发者通过窗口加载UI界面,实现界面
    发表于 05-06 14:29

    HarmonyOS Lottie组件,让动画绘制更简单

    了丰富的API,让开发者能轻松控制动画,大大提高了开发效率。 二、Lottie实战 通过上文对Lottie的介绍,相信很多小伙伴已经感受到了Lottie
    发表于 02-22 14:55

    OpenHarmony标准设备应用开发笔记汇总

    如何在标准设备上运行一个最简单的 OpenHarmony 程序。2、如何在OpenHarmony实现音乐的播放本章是 OpenHarmony 标准设备应用
    发表于 03-28 14:19

    OpenHarmony标准设备应用开发(二)——布局、动画与音乐

    节的基础上,学到更多 ArkUI 组件和布局在 OpenHarmony 中的应用,以及如何在自己的应用中实现显示动画的效果。代码链接:https://gitee.com/
    发表于 04-07 17:09

    OpenHarmony有氧拳击之应用端开发

    变化的动画,便很适合使用属性动画实现。属性动画是指组件的通用属性发生变化时,会根据开始状态和通用属性改变后的状态作为
    发表于 10-09 15:19

    HarmonyOS/OpenHarmony应用开发-属性动画

    组件的某些通用属性变化时,可以通过属性动画实现渐变过渡效果,提升用户体验。支持的属性包括width、height、backgroundColor、opacity、scale、rotate
    发表于 01-03 10:51

    OpenHarmony应用开发—ArkUI组件集合

    页面 接口参考:@ohos.curves, @ohos.router 显示动画 用到全局组件TitleBar,IntroductionTitle实现页面 接口参考:animateTo 属性
    发表于 09-22 14:56

    基于openharmony适配移植的多种动画效果实现教程

    项目介绍 项目名称: OhosLoadingAnimation 所属系列:openharmony的第三方组件适配移植 功能:实现多种动画效果 项目移植状态:主功能完成 调用差异
    发表于 04-02 10:55 0次下载

    openharmony第三方组件适配移植的动画实现

    项目介绍 项目名称:CanAnimation 所属系列:openharmony的第三方组件适配移植 功能:使用openharmony属性动画写的一个库,可组建
    发表于 04-02 11:30 3次下载

    openharmony第三方组件适配移植的SVGA动画渲染库

    项目介绍 项目名称:SVGAPlayer-Ohos 所属系列:openharmony的第三方组件适配移植 功能:SVGAPlayer-Ohos 是一个轻量的动画渲染库。你可以使用工具从 Adobe
    发表于 04-02 11:47 15次下载

    OpenHarmony技术论坛:传统动画实现的不足

    OpenHarmony技术论坛:流畅动画可传统动画实现的不足。
    的头像 发表于 04-25 14:21 1063次阅读
    <b class='flag-5'>OpenHarmony</b>技术论坛:传统<b class='flag-5'>动画</b><b class='flag-5'>实现</b>的不足

    2022 OpenHarmony组件大赛,共建开源组件

    原标题:共建开源组件生态 2022 OpenHarmony组件大赛等你来 2022年4月15日,2022 OpenHarmony组件大赛(下
    的头像 发表于 04-26 17:31 1538次阅读
    2022 <b class='flag-5'>OpenHarmony</b><b class='flag-5'>组件</b>大赛,共建开源<b class='flag-5'>组件</b>

    OpenHarmony轻量系统书籍推荐《OpenHarmony轻量设备开发理论与实战

    最近大家问的智能家居套件方面有没有可以参考的资料,这里给大家统一回复一下 推荐大家可以看这本书 《OpenHarmony轻量设备开发理论与实战》 本书系统地讲授OpenHarmony
    的头像 发表于 07-20 12:43 1175次阅读