iT邦幫忙

2023 iThome 鐵人賽

DAY 19
0

在 React Native 中,可以透過 Animations 達成動畫效果,而 Animations 又包括 Animated 和 LayoutAnimation 兩隻 API 。 Animated 著重在各種小動畫實踐,例如本節將會製作的跑馬燈; LayoutAnimation 則允許開發者在全域建立與更新轉場動畫,例如點擊「觀看更多」按鈕後,到載入實際內容前的轉場。在本節中我們將著重在 Animated 介紹與實作。

一個簡單的 Animated 架構如下:

import {Animated, View, Text, StyleSheet} from 'react-native';

function Marquee() {
  const [startValue, setStartValue] = useState(new Animated.Value(-30));
  const [endValue, setEndValue] = useState(0);

  useEffect(() => {
    Animated.timing(startValue, {
      toValue: endValue,
      duration: 1000,
      useNativeDriver: true,
    }).start();
  }, [startValue, endValue]);

  return (
    <View>
      <Animated.View style={{transform: [{translateY: startValue}]}}>
        <View style={styles.box}>
          <Text style={styles.text}>文字</Text>
        </View>
      </Animated.View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    alignItems: 'center',
    padding: 20,
  },
  box: {
    backgroundColor: '#666',
    width: 100,
    padding: 5,
  },
  text: {
    color: '#fff',
    textAlign: 'center',
  },
});

開發者須以 new Animated.Value() 初始化,設定動畫第一個狀態值,並提供一個結束值。其他能提供的值還有動畫長度 duration ,及是否以 Native OS 處理動畫,使動畫跑起來較順暢的 useNativeDriver 。最後,以 Animated 接上要渲染的元件,透過 style 來賦予剛剛初始值意義。

Animated 內建六種動畫元件: View 、 Text 、 Image 、 ScrollView 、 FlatList 和 SectionList ,開發者也可透過 Animated.createAnimatedComponent() 創建自己所需要的元件。

Animated.timing 只會跑一次動畫,如果要讓動畫不間斷,需搭配 Animated.loopAnimated.sequence 。寫法如下:

useEffect(() => {
    Animated.loop(
      Animated.sequence([
        Animated.timing(startValue, {
          toValue: endValue,
          duration: 1000,
          useNativeDriver: true,
        }),
      ]),
    ).start();
}, [startValue, endValue]);
Animated.delay(2000)

最後,透過 Animated.delay 可以延遲動畫,參數 1000 為一秒。

Animated.delay(1000)

運用上面學習到的知識點,我們其實就已經可以做出一個簡單的跑馬燈。在 <Animated.View> 外包一個框框,設 overflow:hidden ,再把本來文字的背景拿掉;稍微美化一下,一個雛形就出來了。

import React, {useState, useEffect} from 'react';
import {Animated, StyleSheet, Text, View} from 'react-native';

const Marquee = () => {
  const [startValue, setStartValue] = useState(new Animated.Value(-30));
  const [endValue, setEndValue] = useState(30);

  useEffect(() => {
    Animated.loop(
      Animated.sequence([
        Animated.timing(startValue, {
          toValue: endValue,
          duration: 3000,
          useNativeDriver: true,
        }),
      ]),
    ).start();
  }, [startValue, endValue]);

  return (
    <View style={styles.container}>
      <Animated.View style={{transform: [{translateY: startValue}]}}>
        <View style={styles.box}>
          <Text style={styles.text}>Loop</Text>
        </View>
      </Animated.View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    backgroundColor: '#121db76e',
    height: 30,
    margin: 20,
    paddingVertical: 1,
    borderRadius: 25,
    overflow: 'hidden',
  },
  box: {
    width: 100,
    padding: 5,
  },
  text: {
    color: '#000',
    textAlign: 'center',
  },
});

export default Marquee;

https://ithelp.ithome.com.tw/upload/images/20230921/20129635BdTwi3Ia4w.png

到此為止,我們已經能讓文字動起來,不過垂直跑馬燈讓文字這樣呼嘯而過似乎不太好。應該讓他跑到中間後,停一下,再繼續跑動。為了解決這個問題,讓我們把動畫拆成兩組:第一組為初始值 startValue 和中間值 midValue ,第二組為中間值和結束值 endValue 。而 midValue 因為也要擺在 Animated.timing 第一個參數,所以記得要使用 new Animated ,再在 Animated.sequence 設定需要的動畫即可。

const [startValue, setStartValue] = useState(new Animated.Value(-30));
const [midValue, setMidValue] = useState(new Animated.Value(0));
const [endValue, setEndValue] = useState(30);

useEffect(() => {
  Animated.loop(
    Animated.sequence([
      Animated.timing(startValue, {
        toValue: midValue,
        duration: 3000,
        useNativeDriver: true,
      }),
      Animated.delay(2000),
      Animated.timing(midValue, {
        toValue: endValue,
        duration: 3000,
        useNativeDriver: true,
      }),
      Animated.delay(2000),
    ]),
  ).start();
}, [startValue, midValue, endValue]);

接下來又遇到第三個問題。我們前面的資料只有一筆,叫 Loop 的文字。不過跑馬燈當然是好幾筆資料在那裡跑,不會只有一筆。又為增加元件重用性,跑馬燈的資料往往會從父元件傳入。

function HomeScreen() {
  return (
    <View>
      <Marquee
        data={[
          {id: 1, title: '測試用標題一'},
          {id: 2, title: '測試用標題二'},
          {id: 3, title: '測試用標題三'},
        ]}
      />
    </View>
  );
}

對此,我們可以在 Marquee.js 內另設一個 Content 物件,然後用 useEffect 搭配 setInterval ,每隔幾秒就更新一次 Content 的值,而 Content 的值必須依序從 props 來取。最後,在 return 裡移除 map ,只渲染 Content 內的內容。

const Marquee = ({data}) => {
  const [content, setContent] = useState({});
  useEffect(() => {
    let index = 0;
    const maxLength = data.length - 1;
    // 讓跑馬燈一開始就會先秀第一筆
    setContent(data[0]);
    setInterval(() => {
      index = index < maxLength ? index + 1 : 0;
      setContent(data[index]);
    }, 10000); // 此為整個動畫的時間長度(3000+2000+3000+2000 = 10000)
    return () => clearInterval();
  }, [data]);

https://ithelp.ithome.com.tw/upload/images/20230921/20129635HjfsDqjcrq.png

為了讓跑馬燈一開始就會先印出第一筆,必須在 setInterval 外先 setContent(props.data[0]) 。假設我的跑馬燈只有三項資料,那他應該依序載入 props.data[0]props.data[1]props.data[2] 之後,再次回到 props.data[0] 。因此我們可以宣告一個 index ,當 index<2 時,讓他印下一筆的資料;當 index=2 時,他的下一筆應該要是 0 ,所以把 index 設為 0 。

不過,不一定每次跑馬燈都固定只跑三筆資料,應該視傳進來幾筆資料而定。所以為了增加元件重用性,可以接著把 index<2 改成 index<maxLength ,而 maxLength 則設為 props.data.length-1

最後, setInterval 的秒數是用前面 animated 裡所有的秒數加總,在我們的例子裡是 3000+2000+3000+2000 = 10000 。這個秒數不能多不能少,否則動畫會跑到一半被咖掉,或是跑完後因為秒數還沒到而再閃跳一次資料。


參考:


上一篇
Day 18. 運用 Platform 與 react-native-responsive-screen 解決響應式問題吧!
下一篇
Day 20. 從實作 onTop 按鈕,認識 useRef 與 scrollTo
系列文
即使明天老闆突然叫你用 React Native 也可以跟他說好沒問題30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言