RN自定義組件封裝 - 播放類似PPT動畫

1. 前言

近日,被安排做一個開場動畫的任務。雖然RN提供了Animated來自定義動畫,但是本次動畫中的元素頗多,交互甚煩。。。在完成任務的同時,發現很多步驟其實是重復的,于是封裝了一個小組件記錄一下,分享給大家。

2. 初步嘗試

分析一下:雖然這次的動畫需求步驟挺多的,但是把每一步動畫拆解成step1, step2, step3, step4... 講道理應該還是能夠實現的吧?嗯,用Animated.Value()創建值,然后再配上Animated.timing應該就好了。

想到這,反手就是創建一個demo.js,先做個往上飄的氣球試試先吧。

export class Demo1 extends PureComponent {constructor(props) {super(props);}componentWillMount() {this._initAnimation();}componentDidMount() {this._playAnimation();}_initAnimation() {this.topAnimatedValue = new Animated.Value(400);this.balloonStyle = {position: 'absolute',left: 137.5,top: this.topAnimatedValue.interpolate({inputRange: [-999999, 999999],outputRange: [-999999, 999999]})};}_playAnimation() {Animated.timing(this.topAnimatedValue, {toValue: 200,duration: 1500}).start();}render() {return (<View style={styles.demoContainer}><Animated.Imagestyle={[styles.balloonImage, this.balloonStyle]}source={require('../../pic/demo1/balloon.png')}/></View>);}
}

balloon.gif

當然,這是再簡單不過的基礎動畫了。。。如果我們讓這里的氣球一開始最好先是從底部的一個點放大,并且有一個漸入的效果,完了之后再往上飄,這該怎么實現呢?于是代碼變成了這樣:

export class Demo1 extends PureComponent {..._interpolateAnimation(animatedValue, inputRange, outputRange) {return animatedValue.interpolate({inputRange, outputRange});}_initAnimation() {this.opacityAnimatedValue = new Animated.Value(0);this.scaleAnimatedValue = new Animated.Value(0);this.topAnimatedValue = new Animated.Value(400);this.balloonStyle = {position: 'absolute',left: 137.5,opacity: this._interpolateAnimation(this.opacityAnimatedValue, [0, 1], [0, 1]),top: this._interpolateAnimation(this.topAnimatedValue, [-999999, 999999], [-999999, 999999]),transform:[{scale: this._interpolateAnimation(this.scaleAnimatedValue, [0, 1], [0, 1])}]};}_playAnimation() {Animated.sequence([this.step1(),this.step2()]).start();}step1() {return Animated.parallel([Animated.timing(this.opacityAnimatedValue, {toValue: 1,duration: 500}),Animated.timing(this.scaleAnimatedValue, {toValue: 1,duration: 500})]);}step2() {return Animated.timing(this.topAnimatedValue, {toValue: 200,duration: 1500});}...
}

balloon-2.gif

插句話:在動畫銜接的時候,還是糾結了一下。因為Animated提供的方法還是比較多的,這里用到了sequence、parallel,分別可以讓動畫順序執行和并行。除此之外,animtaion的start方法是支持傳入一個回調函數的,表示在當前動畫運行結束的時候會觸發這個回調。所以我們還可以這么寫:

  _playAnimation() {this.step1(() => this.step2());    // 不同之處1:step2作為step1動畫結束之后的回調傳入}step1(callback) {Animated.parallel([Animated.timing(this.opacityAnimatedValue, {toValue: 1,duration: 500}),Animated.timing(this.scaleAnimatedValue, {toValue: 1,duration: 500})]).start(() => {callback && callback();    // 不同之處2:調用傳入的回調});}step2() {Animated.timing(this.topAnimatedValue, {toValue: 200,duration: 1500}).start();}

雖然同樣能夠實現效果,但是還是覺得這種方式不是很舒服,所以棄之。。。

到這里,我們已經對這個氣球做了漸變、放大、平移等3項操作。但是,如果有5個氣球,還有其他各種元素又該怎么辦呢?這才一個氣球我們就已經用了opacityAnimatedValue,scaleAnimatedValue,topAnimatedValue三個變量來控制,更多的動畫元素那直就gg,不用下班了。。。

3. 實現升級

說實話,要做這么個東西,怎么就那么像在做一個PPT呢。。。

“屏幕就好比是一張PPT背景圖;每一個氣球就是PPT上的元素;你可以通過拖動鼠標來擺放各個氣球,我可以用絕對定位來確定每個氣球的位置;至于動畫嘛,剛才的demo已經證明并不難實現,無非就是控制透明度、xy坐標、縮放比例罷了。”

想到這,心中不免一陣竊喜。哈哈,有路子了,可以對PPT上的這些元素封裝一個通用的組件,然后提供常用的一些動畫方法,剩下的事情就是調用這些動畫方法組裝成更復雜的動畫了。新建一個PPT:“出現、飛躍、淡化、浮入、百葉窗、棋盤。。。”看著這令人眼花繚亂的各種動畫,我想了下:嗯,我還是從最簡單的做起吧。。。

首先,我們可以將動畫分成兩種:一次性動畫和循環動畫。
其次,作為一個元素,它可以用作動畫的屬性主要包括有:opacity, x, y, scale, angle等(這里先只考慮了二維平面的,其實還可以延伸擴展成三維立體的)。
最后,基本動畫都可以拆解為這幾種行為:出現/消失、移動、縮放、旋轉。

3.1 一次性動畫

想到這,反手就是創建一個新文件,代碼如下:

// Comstants.js
export const INF = 999999999;// Helper.js
export const Helper = {sleep(millSeconds) {return new Promise(resolve => {setTimeout(() => resolve(), millSeconds);});},animateInterpolate(animatedValue, inputRange, outputRange) {if(animatedValue && animatedValue.interpolate) {return animatedValue.interpolate({inputRange, outputRange});}}
};// AnimatedContainer.js
import {INF} from "./Constants";
import {Helper} from "./Helper";export class AnimatedContainer extends PureComponent {constructor(props) {super(props);}componentWillMount() {this._initAnimationConfig();}_initAnimationConfig() {const {initialConfig} = this.props;const {opacity = 1, scale = 1, x = 0, y = 0, rotate = 0} = initialConfig;// create animated values: opacity, scale, x, y, rotatethis.opacityAnimatedValue = new Animated.Value(opacity);this.scaleAnimatedValue = new Animated.Value(scale);this.rotateAnimatedValue = new Animated.Value(rotate);this.xAnimatedValue = new Animated.Value(x);this.yAnimatedValue = new Animated.Value(y);this.style = {position: 'absolute',left: this.xAnimatedValue,top: this.yAnimatedValue,opacity: Helper.animateInterpolate(this.opacityAnimatedValue, [0, 1], [0, 1]),transform: [{scale: this.scaleAnimatedValue},{rotate: Helper.animateInterpolate(this.rotateAnimatedValue, [-INF, INF], [`-${INF}rad`, `${INF}rad`])}]};}show() {}hide() {}scaleTo() {}rotateTo() {}moveTo() {}render() {return (<Animated.View style={[this.style, this.props.style]}>{this.props.children}</Animated.View>);}
}AnimatedContainer.defaultProps = {initialConfig: {opacity: 1,scale: 1,x: 0,y: 0,rotate: 0}
};

第一步的骨架這就搭好了,簡單到自己都難以置信。。。接下來就是具體實現每一個動畫的方法了,先拿show/hide開刀。

show(config = {opacity: 1, duration: 500}) {Animated.timing(this.opacityAnimatedValue, {toValue: config.opacity,duration: config.duration}).start();
}hide(config = {opacity: 0, duration: 500}) {Animated.timing(this.opacityAnimatedValue, {toValue: config.opacity,duration: config.duration}).start();
}

試了一下,簡直是文美~

但是!仔細一想,卻有個很嚴重的問題,這里的動畫銜接該怎處理?要想做一個先show,然后過1s之后再hide的動畫該怎么實現?貌似又回到了一開始考慮過的問題。不過這次,我卻是用Promise來解決這個問題。于是代碼又變成了這樣:

sleep(millSeconds) {return new Promise(resolve => setTimeout(() => resolve(), millSeconds));
}show(config = {opacity: 1, duration: 500}) {return new Promise(resolve => {Animated.timing(this.opacityAnimatedValue, {toValue: config.opacity,duration: config.duration}).start(() => resolve());});
}hide(config = {opacity: 0, duration: 500}) {return new Promise(resolve => {Animated.timing(this.opacityAnimatedValue, {toValue: config.opacity,duration: config.duration}).start(() => resolve());});
}

現在我們再來看剛才的動畫,只需這樣就能實現:

playAnimation() {this.animationRef.show()                                 // 先出現.sleep(1000)                            // 等待1s.then(() => this.animationRef.hide());  // 消失
}

甚至還可以對createPromise這個過程再封裝一波:

_createAnimation(animationConfig = []) {const len = animationConfig.length;if (len === 1) {const {animatedValue, toValue, duration} = animationConfig[0];return Animated.timing(animatedValue, {toValue, duration});} else if (len >= 2) {return Animated.parallel(animationConfig.map(config => {return this._createAnimation([config]);}));}
}_createAnimationPromise(animationConfig = []) {return new Promise(resolve => {const len = animationConfig.length;if(len <= 0) {resolve();} else {this._createAnimation(animationConfig).start(() => resolve());}});
}opacityTo(config = {opacity: .5, duration: 500}) {return this._createAnimationPromise([{toValue: config.opacity,duration: config.duration,animatedValue: this.opacityAnimatedValue}]);
}show(config = {opacity: 1, duration: 500}) {this.opacityTo(config);
}hide(config = {opacity: 0, duration: 500}) {this.opacityTo(config);
}

然后,我們再把其他的幾種基礎動畫(scale, rotate, move)實現也加上:

scaleTo(config = {scale: 1, duration: 1000}) {return this._createAnimationPromise([{toValue: config.scale,duration: config.duration,animatedValue: this.scaleAnimatedValue}]);
}rotateTo(config = {rotate: 0, duration: 500}) {return this._createAnimationPromise([{toValue: config.rotate,duration: config.duration,animatedValue: this.rotateAnimatedValue}]);
}moveTo(config = {x: 0, y: 0, duration: 1000}) {return this._createAnimationPromise([{toValue: config.x,duration: config.duration,animatedValue: this.xAnimatedValue}, {toValue: config.y,duration: config.duration,animatedValue: this.yAnimatedValue}]);
}

3.2 循環動畫

一次性動畫問題就這樣解決了,再來看看循環動畫怎么辦。根據平時的經驗,一個循環播放的動畫一般都會這么寫:

roll() {this.rollAnimation = Animated.timing(this.rotateAnimatedValue, {toValue: Math.PI * 2,duration: 2000});this.rollAnimation.start(() => {this.rotateAnimatedValue.setValue(0);this.roll();});
}play() {this.roll();
}stop() {this.rollAnimation.stop();
}

沒錯,就是在一個動畫的start中傳入回調,而這個回調就是遞歸地調用播放動畫本身這個函數。那要是對應到我們要封裝的這個組件,又該怎么實現呢?

思考良久,為了保持和一次性動畫API的一致性,我們可以給animatedContainer新增了以下幾個函數:

export class AnimatedContainer extends PureComponent {...constructor(props) {super(props);this.cyclicAnimations = {};}_createCyclicAnimation(name, animations) {this.cyclicAnimations[name] = Animated.sequence(animations);}_createCyclicAnimationPromise(name, animations) {return new Promise(resolve => {this._createCyclicAnimation(name, animations);this._playCyclicAnimation(name);resolve();});}  _playCyclicAnimation(name) {const animation = this.cyclicAnimations[name];animation.start(() => {animation.reset();this._playCyclicAnimation(name);});}_stopCyclicAnimation(name) {this.cyclicAnimations[name].stop();}...
}

其中,_createCyclicAnimation,_createCyclicAnimationPromise是和一次性動畫的API對應的。但是,不同點在于傳入的參數發生了很大的變化:animationConfg -> (name, animations)

  1. name是一個標志符,循環動畫之間不能重名。_playCyclicAnimation和_stopCyclicAnimation都是通過name來匹配相應animation并調用的。
  2. animations是一組動畫,其中每個animation是調用_createAnimation生成的。由于循環動畫可以是由一組一次性動畫組成的,所以在_createCyclicAnimation中也是直接調用了Animated.sequence,而循環播放的實現就在于_playCyclicAnimation中的遞歸調用。

到這里,循環動畫基本也已經封裝完畢。再來封裝兩個循環動畫roll(旋轉),blink(閃爍)試試:

blink(config = {period: 2000}) {return this._createCyclicAnimationPromise('blink', [this._createAnimation([{toValue: 1,duration: config.period / 2,animatedValue: this.opacityAnimatedValue}]),this._createAnimation([{toValue: 0,duration: config.period / 2,animatedValue: this.opacityAnimatedValue}])]);
}stopBlink() {this._stopCyclicAnimation('blink');
}roll(config = {period: 1000}) {return this._createCyclicAnimationPromise('roll', [this._createAnimation([{toValue: Math.PI * 2,duration: config.period,animatedValue: this.rotateAnimatedValue}])]);
}stopRoll() {this._stopCyclicAnimation('roll');
}

4. 實戰

忙活了大半天,總算是把AnimatedContainer封裝好了。先找個素材練練手吧~可是,找個啥呢?“叮”,只見手機上挖財的一個提醒亮了起來。嘿嘿,就你了,挖財的簽到頁面真的很適合(沒有做廣告。。。)效果圖如下:

WACAI-DEMO.GIF

渲染元素的render代碼就不貼了,但是我們來看看動畫播放的代碼:

startOpeningAnimation() {// 簽到(一次性動畫)Promise.all([this._header.show(),this._header.scaleTo({scale: 1}),this._header.rotateTo({rotate: Math.PI * 2})]).then(() => this._header.sleep(100)).then(() => this._header.moveTo({x: 64, y: 150})).then(() => Promise.all([this._tips.show(),this._ladder.sleep(150).then(() => this._ladder.show())])).then(() => Promise.all([this._today.show(),this._today.moveTo({x: 105, y: 365})]));// 星星閃爍(循環動畫)this._stars.forEach(item => item.sleep(Math.random() * 2000).then(() => item.blink({period: 1000})));
}

光看代碼,是不是就已經腦補整個動畫了~ 肥腸地一目了然,真的是美滋滋。

5. 后續思考

  1. 講道理,現在這個AnimatedContainer能夠創建的動畫還是稍顯單薄,僅包含了最基礎的一些基本操作。不過,這也說明了還有很大的擴展空間,根據_createCyclicAnimationPromise和_createAnimationPromise這兩個函數,可以自由地封裝我們想要的各種復雜動畫效果。而調用方就只要通過promise的all和then方法來控制動畫順序就行了。個人感覺,甚至有那么一丁點在使用jQuery。。。
  2. 除此之外,還有一個問題就是:由于這些元素都是絕對定位布局的,那這些元素的x, y坐標值怎么辦?在有視覺標注稿的前提下,那感覺還可行。但是一旦元素的數量上去了,那在使用上還是有點麻煩的。。。所以啊,要是有個什么工具能夠真的像做PPT一樣,支持元素拖拽并實時獲得元素的坐標,那就真的是文美了。。。。。。

老規矩,本文代碼地址:https://github.com/SmallStoneSK/AnimatedContainer

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/278185.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/278185.shtml
英文地址,請注明出處:http://en.pswp.cn/news/278185.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

target存放的是編譯后的.class文件地方 默認情況下不會講非class文件放入進入 如果要使用非.class文件 需要通過增加配置方式自動加入文件...

target存放的是編譯后的.class文件地方 默認情況下不會講非class文件放入進入 如果要使用非.class文件 需要通過增加配置方式自動加入文件轉載于:https://www.cnblogs.com/classmethond/p/10520615.html

dropbox mac_如何在Windows或Mac上啟動時阻止Dropbox打開

dropbox macDropbox is a handy way to synchronize files across devices via the cloud. By default, Dropbox starts whenever you turn on your Windows PC or Mac, but sometimes you might not want it to. Here’s how to make sure it doesn’t launch when you startu…

spring cloud 總結

Finchley版本Spring Cloud Finchley; Spring Boot 2.0.3 史上最簡單的 SpringCloud 教程 | 第一篇: 服務的注冊與發現&#xff08;Eureka&#xff09;(Finchley版本) 所有的服務向這里注冊 史上最簡單的SpringCloud教程 | 第二篇: 服務消費者&#xff08;restribbon&#xff09…

深入分析 ThreadLocal 內存泄漏問題

2019獨角獸企業重金招聘Python工程師標準>>> ThreadLocal 的作用是提供線程內的局部變量&#xff0c;這種變量在線程的生命周期內起作用&#xff0c;減少同一個線程內多個函數或者組件之間一些公共變量的傳遞的復雜度。但是如果濫用 ThreadLocal&#xff0c;就可能會…

如何將iPhone應用程序從應用程序庫移動到主屏幕

Justin Duino賈斯汀杜伊諾(Justin Duino)So as to not clutter up your home screen, newly-downloaded apps from the App Store can be sent directly to the App Library. But what if you later want to open the app without digging through the library? Here’s how t…

luogu4389 付公主的背包

題目鏈接&#xff1a;洛谷 題目大意&#xff1a;現在有$n$個物品&#xff0c;每種物品體積為$v_i$&#xff0c;對任意$s\in [1,m]$&#xff0c;求背包恰好裝$s$體積的方案數&#xff08;完全背包問題&#xff09;。 數據范圍&#xff1a;$n,m\leq 10^5$ 這道題&#xff0c;看到…

Git與Github的連接與使用

2019獨角獸企業重金招聘Python工程師標準>>> Git與Github的連接與使用 下面繼續&#xff0c;使用git 將項目上傳到GitHub上 首先要有GitHub賬號,這就不用說了&#xff0c;沒有的先注冊&#xff0c;地址&#xff1a;https://github.com 沒有倉庫的話&#xff0c;先新…

ttl電路制作pong游戲_如何玩Mozilla Firefox的隱藏的獨角獸Pong游戲

ttl電路制作pong游戲It seems like every browser has a hidden game these days. Chrome has a dinosaur game, Edge has surfing, and Firefox has . . . unicorn pong? Yep, you read that right—here’s how to play it. 這些天似乎每個瀏覽器都有一個隱藏的游戲。 Chrom…

nginx 注釋配置及詳解

前言 DMZ(Demilitarized Zone) 非軍事區&#xff0c;生產環境 WEB 服務部署的區域&#xff0c;公司的架構為一臺nginx 充當 load balance 服務&#xff0c;負載到兩臺 nginx 上面&#xff0c;反向代理至后臺服務&#xff0c;但是nginx 用的全是默認配置加上 proxy_pass 和 upst…

為什么無法運行谷歌play_什么是Google Play積分,以及如何使用它們?

為什么無法運行谷歌playThe Google Play Store is home to thousands of apps, games, movies, e-books, and more. You might find yourself making a lot of purchases there, so why not get rewarded for it? That’s where Google Play Points come in. Google Play商店提…

2019年春季學期第三周作業

本周作業 本周請大家完成上周挑戰作業的第一部分&#xff1a;給定一個整數數組(包含正負數)&#xff0c;找到一個具有最大和的子數組&#xff0c;返回其最大的子數組的和。 例如&#xff1a;[1, -2, 3, 10, -4, 7, 2, -5]的最大子數組為[3, 10, -4, 7, 2] 1&#xff09;.實驗代…

Linux實驗二:linux 常用命令練習

ls命令 列出目錄內容 echo命令 顯示字符串 date命令 顯示或設置系統的日期與時間 cal命令 顯示日歷 who命令 列出登錄用戶信息 chown命令 chgrp命令 改變文件所屬的用戶組 chmod命令 改變文件訪問權限 find命令 在目錄中搜索文件 轉載于:https://www.cnblogs.com/nullno/p/87…

python數據類型之元組類型

#為何要有元組&#xff0c;存放多個值&#xff0c;元組不可變&#xff0c;更多的是用來做查詢 t(1,[1&#xff0c;2,3],a,(1,2)) #ttuple((1,[1,2,3],a,(1,2))) # print(type(t)) <class tuple># #元組可以作為字典的key # d{(1,2):egon} # print(d,type(d),d[(1,2)])# …

短語密碼_使用密碼短語以提高安全性

短語密碼Did you know that Windows supports using passwords of up to 127 characters? I don’t use passwords anymore, and I haven’t for years. I’ve switched to using password phrases instead. 您知道Windows支持使用最多127個字符的密碼嗎&#xff1f; 我不再使…

「單點登錄與權限管理」系列概述

首先&#xff0c;感謝幾位朋友在朋友圈轉發我的文章&#xff0c;小聲的告訴你們&#xff0c;是我主動讓他們幫忙轉發的&#xff1a;&#xff09;在朋友們的分享下&#xff0c;凌晨推送的一篇文章&#xff0c;閱讀人數達到了280多&#xff0c;很滿足&#xff0c;我會堅持寫下去&…

Jupyter notebook: TypeError: __init__() got an unexpected keyword argument 'io_loop 問題

使用環境&#xff1a;Anaconda3&#xff08;Python3.6&#xff09; 創建一個新的notebook時&#xff0c;無法連接到kernel&#xff0c;terminal上顯示錯誤為&#xff1a;TypeError: __init__() got an unexpected keyword argument io_loop 解決方法&#xff1a; conda install…

在Ubuntu Linux中獲取上次訪問的文件時間

Ubuntu Linux has a rich set of commands for manipulating and accessing files. The stat utility gives detailed access to file information, including last accessed and last modified file time. Ubuntu Linux具有一組豐富的用于操作和訪問文件的命令。 stat實用程序…

MySQL新增從庫

項目背景描述&#xff1a;在項目的開始只有一個MySQL實例在運行&#xff0c;后期因為安全性&#xff0c;壓力&#xff0c;備份等原因需要在此實例的基礎上面新增一個從庫。分析&#xff1a;MySQL主從是基于binlog日志來實現的&#xff0c;那么需要主服務器開啟binlog&#xff0…

第一個議題

① 在每個問題后面&#xff0c;請說明哪一章節的什么內容引起了你的提問&#xff0c;提供一些上下文 ② 列出一些事例或資料&#xff0c;支持你的提問 。 ③ 說說你提問題的原因&#xff0c;你說因為自己的假設和書中的不同而提問&#xff0c;還是不懂書中的術語&#xff0c;還…

在Windows Vista中使用符號鏈接

One of the long-awaited features in Windows Vista was the ability to use symbolic links, the way you can in linux. Sadly, they don’t work quite as well as they could, but it’s a big upgrade from prior versions, and has solved a number of problems for me …