React Native系列(五)- 动画Animated

Yukino 1,025 2022-09-25

一、简介

React native中实现动画是依赖的Animated库,主要侧重于输入和输出之间的声明性关系,以及两者之间的可配置变换,此外还提供了简单的 start/stop方法来控制基于时间的动画执行。

二、与CSS 动画的区别

CSS实现动画一般有两种,第一种是@keyframes结合animation实现,另外一种是用transition实现;

而React Native中则是创建一个Animated.Value,这是一个用于控制动画的值,然后使用Animated中的动画函数(如:timing)或者类似ScrollViewonScroll函数去更改这个值,然后可以使用这个值赋给对应的Animated组件style中的trasnform属性。

三、Animated库

Animated库中主要提供三部分内容:

  • Animated.Value - 驱动动画的一维标量值,;
  • Animated.timing等 - 动画函数,还有loopadd等用于计算、合成动画,event用于处理手势和其他事件(如:onScroll),startstopreset用于控制动画;
  • Animated.Component - 动画组件,基础组件的style是没有transform属性的,可以用createAnimatedComponent方法创建动画组件或者用Animted.ViewAnimated.ImageAnimated提供的动画组件;

四、例子

1. 嵌套滚动

有一些像博客内容的场景,最上面是封面标题,占住大量空间,下面是文章主体,在滚动的过程中,封面、标题被遮住或高度缩小以将更大的空间留给内容主体,看上去像嵌套滚动:

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

const IMAGE_HEIGHT = 200;
const MIN_TITLE_HEIGHT = 80;
export default function () {
  const data: number[] = new Array(10).fill(0);
  const [scrollY] = useState(new Animated.Value(0));
  const imageTranslateY = scrollY.interpolate({
    inputRange: [0, IMAGE_HEIGHT - MIN_TITLE_HEIGHT],
    outputRange: [0, IMAGE_HEIGHT - MIN_TITLE_HEIGHT],
    extrapolate: 'clamp'
  });
  const headertranslateY = scrollY.interpolate({
    inputRange: [0, IMAGE_HEIGHT - MIN_TITLE_HEIGHT],
    outputRange: [0, MIN_TITLE_HEIGHT - IMAGE_HEIGHT],
    extrapolate: 'clamp'
  });
  return (
    <View style={styles.fill}>
      <Animated.FlatList
        style={[styles.fill]}
        data={data}
        renderItem={({ index }) => {
          return (
            <View style={styles.block}>
              <Text style={styles.text}>{index}</Text>
            </View>
          );
        }}
        onScroll={Animated.event([{ nativeEvent: { contentOffset: { y: scrollY } } }], {
          useNativeDriver: true
        })}
        ListHeaderComponent={
          <Animated.View style={[styles.header, { transform: [{ translateY: headertranslateY }] }]}>
            <Animated.Image
              style={[styles.image, { transform: [{ translateY: imageTranslateY }] }]}
              source={require('~/assets/image/zelda.jpg')}
            />
          </Animated.View>
        }
        stickyHeaderIndices={[0]}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  fill: {
    flex: 1
  },
  header: {
    width: '100%',
    height: IMAGE_HEIGHT,
    overflow: 'hidden',
    backgroundColor: 'rgba(0, 0, 0, 0.3)'
  },
  image: {
    width: '100%',
    height: IMAGE_HEIGHT,
    resizeMode: 'stretch'
  },
  block: {
    width: '100%',
    height: 100,
    borderWidth: 1,
    borderColor: '#000',
    justifyContent: 'center',
    alignItems: 'center'
  },
  text: {
    fontSize: 36,
    fontWeight: 'bold'
  }
});

效果如下:

React Native - 嵌套滚动

关键点有两个:

  1. 使用Animated.eventFlatListscrollY映射到创建的Animated.Value
  2. 再利用这个变量创建出映射值结合transform,使得滚动时,header整体向上移动,image往反方向移动,结合overflow: 'hidden'创造出一种图片已知在列表下没有移动位置的效果;

另外推荐另外一篇文章React Native ScrollView animated header,以上代码就是看完这篇文章之后写出来的,但也有不一样的地方,比如文章中使用的ScrollView,且Header是用的absolute布局结合paddingTop实现的,也是一个不错的案例;

2. 扫码loop效果

像微信的扫码会有一条扫描线,这个例子就是实现类似的扫描线的效果,是一个比较简单的组合动画:

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

export default function () {
  const [scanLightY] = useState(new Animated.Value(0));
  const windowHeight = Dimensions.get('window').height;
  useEffect(() => {
    Animated.loop(
    	Animated.timing(scanLightY, {
      	 toValue: windowHeight,
         easing: Easing.inOut(Easing.quad),
         duration: 2000,
         useNativeDriver: true
      })
    ).start();
  }, [])
  return (
  	<View style={{flex: 1}}>
    	<Animated.Image
        style={[
          styles.scanLight,
          {
            transform: [
              {
                translateY: scanLightY
              }
            ]
          }
        ]}
        source={require('~/assets/image/scan-light.png')} 
      />
    </View>
  )
}
        
const styles = StyleSheet.create({
  scanLight: {
    position: 'absolute',
    width: '100%',
    resizeMode: 'stretch'
  }
});

效果如下:

React native扫码Gif