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