Taro 一直以來都沒有一個能相容 RN 的動畫方案,duxapp 中擴充套件了 createAnimation 方法,讓這個方法相容了 RN 端,下面讓我們來看看實現思路
createAnimation方法
這個方法是用來建立一個動畫例項的,使用方法像下面這樣,每次 step 建立一組動畫,每組動畫同時執行,執行完一組繼續執行下一組,直到所有的動畫執行完
const an = createAnimation()
.translate(50, 50).rotate(180).step()
.translate(0, 0).rotate(0).step()
.export()
將建立的結果設定給 View 的 animation
屬性,這個動畫就能被執行了
在 Taro 裡面這個方法目前相容小程式 和 H5 端,我們要實現的就是讓他相容 RN,這樣後面要給我們的元件加動畫就更加簡單了,不用在要加動畫的元件 H5 寫一套,RN 再寫一套
RN的動畫方案
RN 裡面的動畫和 createAnimation
這個方式可以說是天差地別,下面來看這個實現淡入動畫的程式碼,這是一個官方示例
import React, { useRef, useEffect } from 'react';
import { Animated, Text, View } from 'react-native';
const FadeInView = (props) => {
const fadeAnim = useRef(new Animated.Value(0)).current // 透明度初始值設為0
React.useEffect(() => {
Animated.timing( // 隨時間變化而執行動畫
fadeAnim, // 動畫中的變數值
{
toValue: 1, // 透明度最終變為1,即完全不透明
duration: 10000, // 讓動畫持續一段時間
}
).start(); // 開始執行動畫
}, [fadeAnim])
return (
<Animated.View // 使用專門的可動畫化的View元件
style={{
...props.style,
opacity: fadeAnim, // 將透明度繫結到動畫變數值
}}
>
{props.children}
</Animated.View>
);
}
好在 RN 的動畫庫本身是很強大的,我們要做的是在 RN 端模擬實現一個 createAnimation
, 還是有辦法解決的
實現
要實現 RN,我們需要兩步
- 1、編寫一個類,用來模擬
createAnimation
方法,透過這個類建立一些動畫資料 - 2、將這個類建立的資料傳遞給一個自定義元件,這個元件裡面將這些資料解析成動畫,並執行這些動畫
建立 Animation 類
這個類就比較簡單,模擬 createAnimation 每一個方法即可,並在 export()
之後生成一個資料並返回,因程式碼過長,下面是部分程式碼展示
export const create = option => new Animation(option)
class Animation {
constructor(option = {}) {
if (!option.duration) {
option.duration = 400
}
if (!option.timingFunction) {
option.timingFunction = 'linear'
}
if (!option.delay) {
option.delay = 0
}
if (!option.transformOrigin) {
option.transformOrigin = '50% 50% 0'
}
this.defaultOption = option
}
result = []
current = {}
export() {
const res = this.result
this.result = []
return res
}
step(option) {
if (Object.keys(this.current).length) {
this.result.push({
option: {
...this.defaultOption,
...option
},
action: this.current
})
this.current = {}
}
return this
}
set(name, value) {
this.current[name] = value
return this
}
translate(x, y) {
this.translateX(x)
return this.translateY(y)
}
translate3D(x, y, z) {
this.translateX(x)
this.translateY(y)
return this.translateZ(z)
}
translateX(val) {
return this.set('translateX', val)
}
translateY(val) {
return this.set('translateY', val)
}
translateZ(val) {
return this.set('translateZ', val)
}
}
建立元件實現動畫
這個地方相對會複雜一些,其中的難點有幾個
- 在小程式上的動畫,是會根據 css 的預設值去執行變化的,但是 RN 上的預設值需要在動畫中設定,因此,需要獲取這個預設值
- 將動畫拆分成適合 RN 端動畫元件的 style 屬性
此處程式碼過長,可以前往 github 檢視,或者使用 duxapp 建立專案之後就能看到
使用
動畫庫寫好之後我們就能給我們原有的一些元件進行改造了,例如 PullView,在這之前,這個元件是針對 RN 端和其他端寫了兩套程式碼的,現在只需要一套程式碼就實現了,下面的示例程式碼展示了這個元件動畫實現方式
import { isValidElement, cloneElement, forwardRef, useState, useEffect, useRef, useImperativeHandle, useCallback } from 'react'
import { View } from '@tarojs/components'
import { asyncTimeOut, nextTick, px, pxNum, transformStyle, useBackHandler } from '@/duxapp/utils'
import { Absolute } from '../Absolute'
import { Animated } from '../Animated'
import './index.scss'
export const PullView = forwardRef(({
side = 'bottom',
style,
overlayOpacity = 0.5,
children,
masking = true,
group,
onClose,
modal,
mask = modal,
duration = 200
}, ref) => {
const [mainAn, setMainAn] = useState(Animated.defaultState)
const [maskAn, setMaskAn] = useState(Animated.defaultState)
const ans = useRef()
const refs = useRef({})
refs.current.onClose = onClose
refs.current.overlayOpacity = overlayOpacity
const translate = siteTranslates[side]
const close = useCallback(async () => {
let an = ans.current.main
if (side === 'center' && process.env.TARO_ENV !== 'rn') {
an = an.translate('-50%', '-50%')
}
setMainAn(an[translate.key](pxNum(translate.value)).opacity(0).step(
process.env.TARO_ENV !== 'rn' ? {
transformOrigin: '25% 25% 0'
} : {}
).export())
setMaskAn(ans.current.mask.opacity(0).step().export())
await asyncTimeOut(duration)
refs.current.onClose?.()
}, [duration, side, translate.key, translate.value])
useBackHandler(close, !mask)
useImperativeHandle(ref, () => {
return {
close
}
})
useEffect(() => {
nextTick(() => {
if (!ans.current) {
ans.current = {
main: Animated.create({
duration,
timingFunction: 'ease-in-out'
}),
mask: Animated.create({
duration,
timingFunction: 'ease-in-out'
})
}
}
if (side === 'center') {
let an = ans.current.main.scale(1).opacity(1)
if (process.env.TARO_ENV !== 'rn') {
an = an.translateX('-50%').translateY('-50%')
}
setMainAn(an.step().export())
} else {
setMainAn(ans.current.main.translateX(0).translateY(0).opacity(1).step().export())
}
setMaskAn(ans.current.mask.opacity(refs.current.overlayOpacity).step().export())
})
}, [duration, side])
return <Absolute group={group}>
{masking && <Animated.View
animation={maskAn}
className='PullView'
>
<View className='PullView__other'
onClick={() => {
if (mask) {
return
}
close()
}}
></View>
</Animated.View>}
<Animated.View
animation={mainAn}
className={`PullView__main PullView__main--${side}`}
style={{
...style,
transform: transformStyle(side === 'center' ? {
translateX: '-50%',
translateY: '-50%',
scaleX: 0.4,
scaleY: 0.4
} : {
[translate.key]: px(translate.value)
})
}}
>
{
isValidElement(children) ?
cloneElement(children, {
style: {
...children.props.style,
...(side === 'center'
? {}
: side === 'bottom' || side === 'top'
? { width: '100%' }
: { height: '100%' })
}
}) :
children
}
</Animated.View>
</Absolute>
})
const siteTranslates = {
top: { key: 'translateY', value: -200 },
bottom: { key: 'translateY', value: 200 },
left: { key: 'translateX', value: -200 },
right: { key: 'translateX', value: 200 },
center: { key: 'scale', value: 0.4 }
}
最後
當然這個動畫也不是完美的,只是實現了一個基礎的動畫,甚至使用的時候還有諸多的限制,你可以點選下面的動畫文件檢視詳細的使用方法以及限制
Animated 動畫文件
開發文件
GitHub