HarmonyOS玩轉ArkUI動效 - 水母動畫

語言: CN / TW / HK

theme: smartblue

本文正在參加「金石計劃 . 瓜分6萬現金大獎」

一、前言

本文會詳細講解我參加 HarmonyOS【挑戰賽第三期】玩轉ArkUI動效的項目

我的參賽項目源碼:【挑戰賽第三期】JellyfishAnimation

我們的動畫效果參考自:cassie-codes的水母SVG

app_effect.gif

華為鴻蒙已經放棄Java作為鴻蒙的開發語言,開發了一個申明式UI框架ArkUI,開發語言變成了ArkTS。

ArkUI是一套構建分佈式應用界面的聲明式UI開發框架。

ArkTS基於TypeScript(簡稱TS)語言擴展而來,是TS的超集。

ArkTS繼承了TS的所有特性。

我們用一個簡單示例,來説明ArkTS的基本組成:

關於ArkUI更多內容,感興趣的同學,可以點擊這裏快速入門,下面我們進入正題。

二、源碼目錄結構

image.png

三、拆解SVG

我們開頭提到cassie-codes的水母SVG,如果拿到這個SVG的話,要怎麼用程序去渲染它呢?

1.組成

我們精簡一下看看它的組成內容:

```xml


``` 點擊查看SVG全部內容,我們先拿第一個路徑數據來看一下:

txt 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:  關閉路徑

這些命令區分大小寫大寫字母表示絕對座標,而小寫字母表示命令相對於當前位置。

可視化路徑操作

更多細節和知識點請查閲:路徑數據命令規範

2.ArkUI中如何繪製

那麼我們如何在ArkUI中使用這段路徑數據呢?

我們在HarmonyOS文檔中看到了Path繪製組件

Path繪製組件: 根據繪製路徑生成封閉的自定義形狀

Path接口如下:

ts Path(value?: { width?: number | string; height?: number | string; commands?: string }) 參數含義如下:

| 參數名 | 參數類型 | 必填 | 參數描述 | | :------- | :--------------- | :- | :--------------- | | width | number 或 string | 否 | 路徑所在矩形的寬度默認值:0 | | height | number 或 string | 否 | 路徑所在矩形的高度默認值:0 | | commands | string | 否 | 路徑繪製的命令字符串默認值:''|

它還有很多通用的屬性,那麼我們把水母的第一個路徑數據傳遞到commands裏面試試: ts Path() .commands('M226.31 258.64c.77 8.68 2.71 16.48 1.55....') .fillOpacity(0.49) .fill(Color.White)

第一條觸手

我們看到第一條觸手就這麼被我們渲染出來了,是不是感覺也挺簡單的。

3.元素標籤g

這時候可能有同學會問,path外層還有一個有個元素標籤<g class="...">包裹着,那麼這個元素標籤<g class="...">是幹什麼的呢?

問的好👏🏻👏🏻,這個g表示

組合對象的容器,添加到g元素上的變換會應用到其所有的子元素上。

添加到g元素的屬性會被其所有的子元素繼承。

這裏有個小插曲:一開始我也犯了個錯誤,在華為的官方文檔裏面沒有看到Group組件

為什麼會聯想到Group呢?下意識的去聯想ArkUI應該和其他平台的一樣,應該也有。

我想着既然是組合對象的容器,又沒有找到Group那我用Stack不就完事了嗎?

然而,發現事情並不是想象的那麼簡單。

如果你用Stack直接包裹Path,可能會出現的錯誤效果如下:

ts // 類似如下代碼: Stack(){ Path().commands(...) ... }.width('100%').height('100%')

我們可以看到,內容是繪製了,但是遠遠達不到我們要的效果,甚至醜陋不堪,都亂了,到底是什麼原因呢?

每一步失敗的過程,就不在這裏一一描述😂,原因在於我們:沒有設置“路徑所在的矩形寬高”

那我們如果挨個按照下面這樣設置,是不是太蠢了?

ts 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的情況下,這個元素將不會被渲染出來。

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

這裏我們通過Shape組件裏面的viewPort屬性方法來設置viewBox裏面的寬度和高度

所以,我們可以通過下面的方式來實現和SVG等價的內容:

xml <g class="..."> <path class="..." fill="..." d=""/> <path class="..." fill="..." d=""/> </g> 等價於: ```ts Shape() { Path() .commands('....') .fill(...)

Path()
  .commands('...')
  .fill(...)

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

``` 這裏可能又會有同學問道了,為什麼單位是PX?

問的好👏🏻👏🏻,我想這裏有一篇內容解釋的很詳細了:為什麼viewBox裏面的單位是px?

SVG最重要的內容都拆解介紹完了。

4.feTurbulence

可能這個時候又有同學問了,那個SVG裏面的feTurbulence是幹什麼用的?

xml <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屬性的問題,官方給我的回覆是

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

四、製作動畫

華為官方對參加挑戰賽的要求是:

參賽者需要 :①使用animateTo實現顯式動畫,②使用animation為組件添加屬性動畫

點擊查看animation屬性動畫點擊查看animateTo顯示動畫

所以我們就根據官方的要求來寫了個水母動畫參賽作品。

我們的數據狀態存儲都放在JellyFishViewModel裏面。

水母的眨眼睛,使用的是animateTo,通過顯示動畫修改:水母眼睛Y軸的縮放和不透明度來達到眨眼睛效果。

我們來簡單看下眨眼動畫:

ts 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 }) } 然後給我們的水母眼睛設置縮放透明度屬性就能眨眼睛了,下面是左側眼睛的部分代碼: ts 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:

txt 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軸數據達到上下移動的動畫,我們簡單看下下面的偽代碼,它是如何做到不停的上下移動的:

ts 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同步,有錯位和落差的效果呢?

問的好,我們再來看一下水母臉部的動畫怎麼處理的:

```ts 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的值的 // 感興趣的可以打開我們的源碼查看 } })

```

如果學完覺得有幫助的,可以點贊❤️+收藏❤️+分享❤️+關注❤️