头图

HarmonyOS 玩转 ArkUI 动效 - 水母动画

一、前言

本文会详细讲解我参加 HarmonyOS【挑战赛第三期】玩转ArkUI动效的项目

我的参赛项目源码:【挑战赛第三期】JellyfishAnimation

我们的动画效果参考自:cassie-codes的水母SVG

水母动画.gif

华为鸿蒙已经放弃Java作为鸿蒙的开发语言,开发了一个申明式UI框架ArkUI,开发语言变成了ArkTS。

ArkUI是一套构建分布式应用界面的声明式UI开发框架。

ArkTS基于TypeScript(简称TS)语言扩展而来,是TS的超集。

ArkTS继承了TS的所有特性。

我们用一个简单示例,来说明ArkTS的基本组成:
ArkTS的基本组成.png

关于ArkUI更多内容,感兴趣的同学,可以点击这里快速入门,下面我们进入正题。

二、源码目录结构

源码目录结构.png

三、拆解SVG

我们开头提到cassie-codes的水母SVG,如果拿到这个SVG的话,要怎么用程序去渲染它呢?

1.组成

我们精简一下看看它的组成内容:

<!-- 为了直观的查看组成结构,我们删除了路径数据 --> 
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 530.46 563.1">
  <defs>
  <filter id="turbulence" filterUnits="objectBoundingBox" x="0" y="0" width="100%" height="100%">
    <feTurbulence data-filterId="3" baseFrequency="0.02 0.03" result="turbulence" id="feturbulence" type="fractalNoise" numOctaves="1" seed="1"></feTurbulence>
    <feDisplacementMap id="displacement" xChannelSelector="R" yChannelSelector="G" in="SourceGraphic" in2="turbulence" scale="13" />
  </filter>    
  </defs>
  <g class="jellyfish" filter="url(#turbulence)">
    <path class="tentacle"/>
    <path class="tentacle"/>
    <path class="tentacle" />
    <path class="tentacle" />
    <path class="tentacle"/>
    <path class="tentacle"/>
    <path class="tentacle"/>
    <path class="tentacle"/>
    <path class="tentacle"/>
    <path class="face" />
    <path class="outerJelly"/>
    <path id="freckle" />
    <path id="freckle"/>
    <path id="freckle-4"/>
  </g>
  <g id="bubbles" fill="#fff">
    <path class="bubble"/>
    <path class="bubble"/>
    <path class="bubble" />
    <path class="bubble"/>
    <path class="bubble"/>
    <path class="bubble"/>
    <path class="bubble" />
  </g>
  <g class="jellyfish face">
    <path class="eye lefteye"  fill="#b4bebf" d=""/>
    <path class="eye righteye" fill="#b4bebf" d=""/>
    <path class="mouth" fill="#d3d3d3" opacity=".72"/>
  </g>
</svg>

点击查看SVG全部内容,我们先拿第一个路径数据来看一下:

M226.31 258.64c.77 8.68 2.71 16.48 1.55 25.15-.78 8.24-5 15.18-7.37 23-3.1 10.84-4.65 22.55 1.17 32.52 4.65 7.37 7.75 11.71 5.81 21.25-2.33 8.67-7.37 16.91-2.71 26 4.26 8.68 7.75 4.34 8.14-3 .39-12.14 0-24.28.77-36 .78-16.91-12-27.75-2.71-44.23 7-12.15 11.24-33 7.76-46.83z
对于不熟悉SVG相关内容的同学,你可能看不懂,甚至有点烦躁,不过也不要急,看不懂也不要紧,跟着我们一起往下看,学完你也可以在GSAP动画平台里面找一些相关的SVG练手。

我们简单看一下路径数据里面的一些常见命令含义:

  • M,m:  Move to:移至,移动到
  • L, l, H, h, V, v: Line to:画线
  • C, c, S, s:   三次贝塞尔曲线
  • Q, q, T, t:  二次贝塞尔曲线
  • A,a:  椭圆弧曲线
  • Z, z:  关闭路径

这些命令区分大小写大写字母表示绝对坐标,而小写字母表示命令相对于当前位置。

可视化路径操作.gif

更多细节和知识点请查阅:路径数据命令规范

2.ArkUI中如何绘制

那么我们如何在ArkUI中使用这段路径数据呢?

我们在HarmonyOS文档中看到了Path绘制组件

Path绘制组件: 根据绘制路径生成封闭的自定义形状

Path接口如下:

Path(value?: { width?: number | string; height?: number | string; commands?: string })

参数含义如下:

参数名参数类型必填参数描述
widthnumber 或 string路径所在矩形的宽度默认值:0
heightnumber 或 string路径所在矩形的高度默认值:0
commandsstring路径绘制的命令字符串默认值:''

它还有很多通用的属性,那么我们把水母的第一个路径数据传递到commands里面试试:

Path()
    .commands('M226.31 258.64c.77 8.68 2.71 16.48 1.55....')
    .fillOpacity(0.49)
    .fill(Color.White)

第一条触手.png

我们看到第一条触手就这么被我们渲染出来了,是不是感觉也挺简单的。

3.元素标签g

这时候可能有同学会问,path外层还有一个有个元素标签<g class="...">包裹着,那么这个元素标签<g class="...">是干什么的呢?

问的好👏🏻👏🏻,这个g表示

组合对象的容器,添加到g元素上的变换会应用到其所有的子元素上。

添加到g元素的属性会被其所有的子元素继承。

这里有个小插曲:一开始我也犯了个错误,在华为的官方文档里面没有看到Group组件

为什么会联想到Group呢?下意识的去联想ArkUI应该和其他平台的一样,应该也有。

我想着既然是组合对象的容器,又没有找到Group那我用Stack不就完事了吗?

然而,发现事情并不是想象的那么简单。

如果你用Stack直接包裹Path,可能会出现的错误效果如下:

错乱.png

// 类似如下代码:
Stack(){
    Path().commands(...)
    ...
}.width('100%').height('100%')

我们可以看到,内容是绘制了,但是远远达不到我们要的效果,甚至丑陋不堪,都乱了,到底是什么原因呢?

每一步失败的过程,就不在这里一一描述😂,原因在于我们:没有设置“路径所在的矩形宽高”

那我们如果挨个按照下面这样设置,是不是太蠢了?

Path().commands(...).width(...).height(...)

后来我再次仔细查阅HarmonyOS文档,在绘制组件中找到了Shape组件,官方文档的解释,它有2种意思:

1、绘制组件使用Shape作为父组件,实现类似SVG的效果。

2、绘制组件单独使用,用于在页面上绘制指定的图形。

至此,我们再回头看一下,width/height造成的一些问题。

我们在SVG的XML中通过viewBox属性获取到,viewBox="0 0 530.46 563.1"

viewBox 属性的值是一个包含 4 个参数的列表 min-x, min-y, width and height,以空格或者逗号分隔开。

不允许宽度和高度为负值,width或height的值,等于0的情况下,这个元素将不会被渲染出来。

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 530.46 563.1">
......

这里我们通过Shape组件里面的viewPort属性方法来设置viewBox里面的宽度和高度

所以,我们可以通过下面的方式来实现和SVG等价的内容:

<g class="...">
    <path class="..."  fill="..." d=""/>
    <path class="..." fill="..." d=""/>
  </g>

等价于

Shape() {
    Path()
      .commands('....')
      .fill(...)

    Path()
      .commands('...')
      .fill(...)
    
    }.viewPort({
      width: '530.46px',
      height: '563.1px',
    })

这里可能又会有同学问道了,为什么单位是PX?

问的好👏🏻👏🏻,我想这里有一篇内容解释的很详细了:为什么viewBox里面的单位是px?

SVG最重要的内容都拆解介绍完了。

4.feTurbulence

可能这个时候又有同学问了,那个SVG里面的feTurbulence是干什么用的?

<filter id="turbulence" filterUnits="objectBoundingBox" x="0" y="0" width="100%" height="100%">
    <feTurbulence data-filterId="3" baseFrequency="0.02 0.03" result="turbulence" id="feturbulence" type="fractalNoise" numOctaves="1" seed="1"></feTurbulence>
    <feDisplacementMap id="displacement" xChannelSelector="R" yChannelSelector="G" in="SourceGraphic" in2="turbulence" scale="13" />
  </filter>    

feTurbulence含义

SVG滤波器会产生噪声,这用于模拟一些自然现象,如:云、火和烟, 有助于生成复杂的纹理,如大理石或花岗岩等效果。

点击查看feTurbulence了解更多

那么ArkUI中如何实现这个这种效果呢?HarmonyOS文档里面Shape组件有个Mesh属性,按理说它可以实现这种效果,我就提了个工单,询问关于Mesh属性的问题,官方给我的回复是

官方回复.png

所以本项目也不能使用Mesh的特性了,期待官方更新。

四、制作动画

华为官方对参加挑战赛的要求是:

参赛者需要 :①使用animateTo实现显式动画,②使用animation为组件添加属性动画

点击查看animation属性动画点击查看animateTo显示动画

所以我们就根据官方的要求来写了个水母动画参赛作品。

我们的数据状态存储都放在JellyFishViewModel里面。

水母的眨眼睛,使用的是animateTo,通过显示动画修改:水母眼睛Y轴的缩放和不透明度来达到眨眼睛效果。

我们来简单看下眨眼动画:

  blinkAnimateTo() {
    animateTo({
      duration: 150,
      curve: Curve.EaseOut,
      iterations: 1,
      playMode: PlayMode.Normal,
      onFinish:()=> {
        // 闭眼之后,再恢复回睁眼状态
        this.blinkScale =this.blinkScale == 0.3?1:0.3
        this.blinkAlpha =this.blinkAlpha == 0? 1: 0
      }
    }, () => {
        this.blinkScale =this.blinkScale == 0.3?1:0.3
        this.blinkAlpha =this.blinkAlpha == 0? 1: 0
    })
  }

然后给我们的水母眼睛设置缩放透明度属性就能眨眼睛了,下面是左侧眼睛的部分代码:

    Shape() {
      Path()
        .fill(...)
        .commands(...)
    }
    .scale({ y: this.blinkScale, centerY: '233px' })
    .opacity(this.blinkAlpha)
    .viewPort({
      width: '530.46px',
      height: '563.1px'
    })

到这里可能又有同学,又要疑惑提问题了,怎么设置缩放还要设置centerY呢?

我们当然要设置,缩放的中心点啦,并且现在是针对于Y轴,所以需要:设置Y轴中心点,不然它缩放就偏了。

那为什么是233px呢?

问的好,我们看一下左侧眼睛的pathData:

M262 233.63a3.1 3.1 0 1 0-3 3.19 3.1 3.1 0 0 0 3-3.19z

我们上面拆解SVG的时候,介绍了M的含义是:移动到,且大写字母表示:绝对坐标,当然你填233.63px也可以。

我们整个水母body上下移动,是如何做到的呢?

我们利用了属性动画animation更新组件的属性translate里面的y轴数据达到上下移动的动画,我们简单看下下面的伪代码,它是如何做到不停的上下移动的:

Stack() {
    // 水母的body元素分组
    ...
}
.translate({ y : this.translateY })
.onAreaChange(()=>{
   this.translateY = -30
})
.animation({
    duration: 3000, // 动画时长
    iterations: 1, // 播放次数
    playMode: PlayMode.Normal, // 动画模式
    onFinish: () => {
      // 动画播放完成回调
      this.translateY = this.translateY == 0? -30 : 0
   }
})

这样我们就做到,让水母整个body元素上下动画移动了,且不会停止。

那么水母的脸部怎么做到上下左右动画移动,且不会和body同步,有错位和落差的效果呢?

问的好,我们再来看一下水母脸部的动画怎么处理的:

Stack() {
    // 脸部数据
    ...
}
.translate({ y : this.translateY, x: this.translateX })
.onAreaChange(()=>{
   this.translateY = -25
   this.translateX = ... 
   // 这里其实我们是在viewModel中,使用Math.random来计算translateX的值的
   // 感兴趣的可以打开我们的源码查看
})
.animation({
    duration: 3000, // 动画时长
    iterations: 1, // 播放次数
    playMode: PlayMode.Normal, // 动画模式
    onFinish: () => {
      // 动画播放完成回调
      this.translateY = this.translateY == 0? -25 : 0
      this.translateX = ...
      // 这里其实我们是在viewModel中,使用Math.random来计算translateX的值的
      // 感兴趣的可以打开我们的源码查看
   }
})

如果学完觉得有帮助的,可以点赞❤️+收藏❤️+分享❤️+关注❤️

星光不问赶路人,岁月不负有心人

1 声望
0 粉丝
0 条评论
推荐阅读
如何编写一个d.ts文件
总结一下:从类型type角度分为:基本类型(string、number、boolean等)及其混合;复杂类型(class、function、object)及其混合(比如说又是class又是function)。从代码有效范围分为:全局变量、模块变量和又是...

Midqiu282阅读 111.8k评论 45

「多图预警」完美实现一个@功能
一天产品大大向 boss 汇报完研发成果和产品业绩产出,若有所思的走出来,劲直向我走过来,嘴角微微上扬。产品大大:boss 对我们的研发成果挺满意的,balabala...(内心 OS:不听,讲重点)产品大大:咱们的客服 I...

wuwhs39阅读 4.7k评论 5

封面图
涨姿势了,有意思的气泡 Loading 效果
今日,群友提问,如何实现这么一个 Loading 效果:这个确实有点意思,但是这是 CSS 能够完成的?没错,这个效果中的核心气泡效果,其实借助 CSS 中的滤镜,能够比较轻松的实现,就是所需的元素可能多点。参考我们...

chokcoco19阅读 2k评论 2

在前端使用 JS 进行分类汇总
最近遇到一些同学在问 JS 中进行数据统计的问题。虽然数据统计一般会在数据库中进行,但是后端遇到需要使用程序来进行统计的情况也非常多。.NET 就为了对内存数据和数据库数据进行统一地数据处理,发明了 LINQ (L...

边城17阅读 1.9k

封面图
过滤/筛选树节点
又是树,是我跟树杠上了吗?—— 不,是树的问题太多了!🔗 相关文章推荐:使用递归遍历并转换树形数据(以 TypeScript 为例)从列表生成树 (JavaScript/TypeScript) 过滤和筛选是一个意思,都是 filter。对于列表来...

边城18阅读 7.6k评论 3

封面图
你可能不需要JS!CSS实现一个计时器
CSS现在可不仅仅只是改一个颜色这么简单,还可以做很多交互,比如做一个功能齐全的计时器?样式上并不复杂,主要是几个交互的地方数字时钟的变化开始、暂停操作重置操作如何仅使用 CSS 来实现这样的功能呢?一起...

XboxYan20阅读 1.5k评论 1

封面图
「彻底弄懂」this全面解析
当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在 哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this就是记录的其中一个属性,会在 函数执行的过程中用到...

wuwhs17阅读 2.4k

封面图

星光不问赶路人,岁月不负有心人

1 声望
0 粉丝
宣传栏