iT邦幫忙

2023 iThome 鐵人賽

DAY 13
0
Mobile Development

30天React Native之旅:從入門到活用系列 第 13

Day 13:React Native動畫入門:從基礎到實作

  • 分享至 

  • xImage
  •  

React Native提供了兩種動畫API LayoutAnimationAnimatedAnimated是較常用的一個,可以實現多種精細動畫效果。本篇將介紹Animated

動畫組件

React Native已將許多常用組件,如ViewTextImageScrollViewFlatListSectionList,提供了封裝好的動畫組件版本。此外,如果有自己定義的組件或者需要使用到的第三方組件並希望它有動畫效果,那麼可以通過Animated.createAnimatedComponent()方法進行封裝,使其具有動畫能力。

動畫值

Animated.Value(): 對單一數值進行動畫效果

new Animated.Value(0)

Animated.ValueXY(): 同時對兩個值進行動畫效果,常用於位置的移動

new Animated.ValueXY({x:0, y:0}})

動畫的類型

  • Spring(彈簧): 模擬彈簧的物理特性

    • friction:阻力大小,影響彈簧的彈性和幅度。
    • tension:拉伸張力,主要控制彈簧的反彈速度。
    • speed: 動畫的執行速度
    • bounciness: 彈簧的彈力
  • Timing(時機): 在指定的時間範圍內改變動畫值

    • duration:設定動畫的總持續時間
    • easing:動畫的緩動函數,決定了值的變化節奏
    • delay:設定動畫開始前的等待時間
  • Decay(衰減): 基於給定的初速度,動畫值逐漸減速直到停止

    • velocity:指定動畫的初始速度,這是一個必填參數。
    • deceleration:描述速度的衰減率
  • Composite(組合動畫): 組合多個動畫項目,可以實現同時或按順序執行。

    • sequence: 依序執行一組動畫。
    • parallel: 同時執行多個動畫。
    • stagger: 等間隔地啟動一組動畫。
    • delay: 在指定的延遲後啟動動畫。

    其中timing是最常使用到的,例如要做一個移動一個元素的位置動畫:

    import { Animated, Easing } from 'react-native';
    
    // 假設position是一個Animated.Value初始化為0
    Animated.timing(position, {
        toValue: 100,            // 最終位置值
        easing: Easing.back(),  // 緩動函數,這裡使用了反彈效果
        duration: 2000,         // 持續時間,即動畫從開始到結束共需2秒
        delay: 500              // 延遲時間,即動畫開始前先等待0.5秒
    }).start();                 // 啟動動畫
    

插值函數

插值是將一個值範圍映射到另一個值範圍。
舉例來說,假設你有一個動畫,它的進度是從0到1的變化。但是,你希望一個物件在這期間從0像素移動到100像素的位置。這時,你就可以用這一個方法將這0到1的變化“映射”成0到100的變化。這正是插值的作用。

value.interpolate({
  inputRange: [0, 1],
  outputRange: [0, 100]
});

製作動畫步驟

首先,我們先看看React Native官網怎麼寫的

The core workflow for creating an animation is to create an Animated.Value, hook it up to one or more style attributes of an animated component, and then drive updates via animations using Animated.timing().
soruce

翻譯過來就是:核心流程是先創建一個 Animated.Value,接著將其連接到一個或多個動畫組件的樣式屬性上,最後利用 Animated.timing() 來驅動這些動畫的更新。

簡單的說可以分為三步驟

  1. 建立一個 Animated.Value 來存儲動畫的值
  2. 把這個值連接到一個或多個動畫組件的樣式屬性
  3. Animated.timing() 來驅動這些動畫的更新

Loading動畫實作

看完以上介紹大家可能還是感覺有些抽象,沒關係,我們就來實際做一個Loading動畫吧。

在常用組件篇,有提到RN內建的ActivityIndicator組件,可供我們快速製作Loading動畫,但其依賴於原生組件,在Android/IOS上的表現不同,有時無法滿足客戶需求,若想要做出較客製的Loading動畫就可以用Animated實現。

假設我們的期望的Loading效果是:淺灰圓圈底,藍色4/3圓圈 無限轉動
loading

1. 建立一個 Animated.Value 來存儲動畫的當前值。

設定動畫值(旋轉角度),創建一個 Animated.Value 來代表這個數值。

const rotationValue = useRef(new Animated.Value(0)).current;

這裡的rotationValue即為該動畫的當前角度值。當rotationValue為0時,元素不旋轉;當其為360時,元素旋轉360度。

你或許有注意到,我們使用useRef來保存Animated.Value。這樣做的原因是:使用useRef可以確保動畫值在組件的多次渲染之間保持一致,且不受State重新渲染的影響。

2. 把這個值連接到動畫組件的一個或多個樣式上。

接下來,我們將使用 interpolate 來連接動畫值。在本例中,我們希望 rotationValue 從 0 到 1 變化時,旋轉角度從 '0deg' 到 '360deg'。

const rotationAngle = rotationValue.interpolate({
    inputRange: [0, 1],
    outputRange: ['0deg', '360deg'],
  });
  • inputRange: 設定了動畫值的輸入範圍。在這裡,我們的 rotationValue 會從 0 變到 1。
  • outputRange: 設定了相對應的輸出值,這些輸出值與 inputRange 中的輸入值對應。所以當 rotationValue 是 0,rotationAngle 會是 '0deg';而當 rotationValue 是 1,rotationAngle 會是 '360deg'。

3. 用 Animated.timing() 控制並更新該動畫。

最後,使用 Animated.timing() 來定義動畫的行為。

useEffect(() => {
  Animated.loop(
    Animated.timing(rotationValue, {
      toValue: 1,
      duration: 1000,
      useNativeDriver: true,
      easing: Easing.linear,
    })
  ).start();
}, [rotationValue]);
  • Animated.loop():使動畫無限循環播放。
  • Animated.timing():定義了動畫具體行為。當 rotationValue 逐漸變為 1 時,旋轉角度會從 '0deg' 變為 '360deg',從而實現旋轉效果。
  • toValue: 表示動畫結束時的值。
  • duration: 動畫持續的時間,以毫秒為單位。
  • easing: 隨時間變化的模式。Easing.linear表示動畫速度恆定。
  • useNativeDriver:這是一個非常重要的屬性。當設為true時,動畫會在原生端運行,脫離JS線程。這表示,即使JavaScript線程繁忙或卡住,動畫仍然能流暢運行。

4. UI部分

要使動畫起作用,我們要用Animated.View動畫組件,然後將 rotationAngle 動畫值放入 transform 屬性的 rotate 中,這樣當rotationAngle 變化時,圓圈就會旋轉。

<Animated.View style={{ ...customStyles.container, transform: [{ rotate: rotationAngle }] }}>
    <View style={customStyles.backgroundCircle} /> 
    <View style={customStyles.rotatingCircle} />
</Animated.View>

完成圓圈樣式

// 藍色4/3圓圈樣式
rotatingCircle: {
    width: '100%',
    height: '100%',
    borderRadius: 40,
    borderWidth: 10,
    borderTopColor: primaryBorderColor,
    borderRightColor: primaryBorderColor,
    borderBottomColor: primaryBorderColor,
    borderLeftColor: 'transparent',
  },

// 淺灰色圓圈的樣式
backgroundCircle: {  
  position: 'absolute',
  top: 0,
  left: 0,
  width: '100%',
  height: '100%',
  borderRadius: 40,
  borderWidth: 10,
  borderColor: secondaryBorderColor,
}

完整代碼

import { StyleSheet, View, Animated, Easing } from 'react-native';
import { useEffect, useRef } from 'react';

const primaryBorderColor = '#3498db';
const secondaryBorderColor = '#e0e0e0';

const customStyles = StyleSheet.create({
	container: {
    position: 'absolute',
    left: '50%',
    top: '50%',
    width: 80,
    height: 80,
    marginLeft: -40,
    marginTop: -40,
  },
  rotatingCircle: {
    width: '100%',
    height: '100%',
    borderRadius: 40,
    borderWidth: 10,
    borderTopColor: primaryBorderColor,
    borderRightColor: primaryBorderColor,
    borderBottomColor: primaryBorderColor,
    borderLeftColor: 'transparent',
  },
  backgroundCircle: {  // 淺灰色圓圈的樣式
    position: 'absolute',
    top: 0,
    left: 0,
    width: '100%',
    height: '100%',
    borderRadius: 40,
    borderWidth: 10,
    borderColor: secondaryBorderColor,
  }
});

const RotatingComponent = () => {
  const rotationValue = useRef(new Animated.Value(0)).current;

  const rotationAngle = rotationValue.interpolate({
    inputRange: [0, 1],
    outputRange: ['0deg', '360deg'],
  });

  useEffect(() => {
    Animated.loop(
      Animated.timing(rotationValue, {
        toValue: 1,
        duration: 1000,
        useNativeDriver: true,
        easing: Easing.linear,
      })
    ).start();
  }, [rotationValue]);

  return (
    <Animated.View style={{ ...customStyles.container, transform: [{ rotate: rotationAngle }] }}>
      <View style={customStyles.backgroundCircle} /> 
      <View style={customStyles.rotatingCircle} />
    </Animated.View>
  );
};

export default RotatingComponent;

小結

今天介紹了動畫的基礎,並實現了一個簡易的Loading動畫。不過動畫領域博大精深,如有意深入暸解,可以再自行研究。當然,若React Native內建的動畫API無法滿足你,也可以去看看社區上的動畫套件,目前在社區中,reanimated 是較為流行的套件,下一篇也將會介紹。

Ref

https://juejin.cn/post/7255224807322615866?searchId=2023091500420349FAC813E68F9C75E02C
https://yeeflame.github.io/animate-for-RN/method/Animated.html
https://juejin.cn/post/7100068545953792030
https://reactnative.dev/docs/animations


上一篇
Day 12:Image組件的使用與技巧
下一篇
Day 14:高效React Native動畫:探索Reanimated
系列文
30天React Native之旅:從入門到活用30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言