RN - 动画

Posted liuxiaokun

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了RN - 动画相关的知识,希望对你有一定的参考价值。

RN - 动画

流畅、有意义的动画对于移动应用用户体验来说是非常重要的。现实生活中的物体在开始移动和停下来的时候都具有一定的惯性,我们在界面中也可以使用动画来实现契合物理规律的交互。

React Native 提供了两个互补的动画系统:用于创建精细的交互控制的动画Animated和用于全局的布局动画LayoutAnimation。

  • Animated Animated使得开发者可以简洁地实现各种各样的动画和交互方式,并且具备极高的性能。Animated旨在以声明的形式来定义动画的输入与输出,在其中建立一个可配置的变化函数,然后使用start/stop方法来控制动画按顺序执行。 Animated仅封装了6个可以动画化的组件:View、Text、Image、ScrollView、FlatList和SectionList,不过你也可以使用Animated.createAnimatedComponent()来封装你自己的组件。下面是一个在加载时带有淡入动画效果的视图:
import React, { useState, useEffect } from ‘react‘;
import { Animated, Text, View } from ‘react-native‘;

const FadeInView = (props) => {
  const [fadeAnim] = useState(new Animated.Value(0))  // 透明度初始值设为0

  React.useEffect(() => {
    Animated.timing(                  // 随时间变化而执行动画
      fadeAnim,                       // 动画中的变量值
      {
        toValue: 1,                   // 透明度最终变为1,即完全不透明
        duration: 10000,              // 让动画持续一段时间
      }
    ).start();                        // 开始执行动画
  }, [])

  return (
    <Animated.View                 // 使用专门的可动画化的View组件
      style={{
        ...props.style,
        opacity: fadeAnim,         // 将透明度绑定到动画变量值
      }}
    >
      {props.children}
    </Animated.View>
  );
}

// 然后你就可以在组件中像使用`View`那样去使用`FadeInView`了
export default () => {
  return (
    <View style={{flex: 1, alignItems: ‘center‘, justifyContent: ‘center‘}}>
      <FadeInView style={{width: 250, height: 50, backgroundColor: ‘powderblue‘}}>
        <Text style={{fontSize: 28, textAlign: ‘center‘, margin: 10}}>Fading in</Text>
      </FadeInView>
    </View>
  )
}
  • 配置动画

动画拥有非常灵活的配置项。自定义的或预定义的 easing 函数、延迟、持续时间、衰减系数、弹性常数等都可以在对应类型的动画中进行配置。

下面这个例子创建了一个2秒长的动画,在移动目标到最终位置前会稍微往后退一点:

Animated.timing(this.state.xPosition, {
  toValue: 100,
  easing: Easing.back(),
  duration: 2000
}).start();
  • 组合动画

多个动画可以通过parallel(同时执行)、sequence(顺序执行)、stagger和delay来组合使用。它们中的每一个都接受一个要执行的动画数组,并且自动在适当的时候调用start/stop。

默认情况下,如果任何一个动画被停止或中断了,组内所有其它的动画也会被停止。Parallel 有一个stopTogether属性,如果设置为false,可以禁用自动停止。

在Animated文档的组合动画一节中列出了所有的组合方法。

合成动画值

你可以使用加减乘除以及取余等运算来把两个动画值合成为一个新的动画值。

const a = new Animated.Value(1);
const b = Animated.divide(1, a);

Animated.spring(a, {
  toValue: 2
}).start();
value.interpolate({
  inputRange: [0, 1],
  outputRange: [0, 100]
});
style={{
    opacity: this.state.fadeAnim, // Binds directly
    transform: [{
      translateY: this.state.fadeAnim.interpolate({
        inputRange: [0, 1],
        outputRange: [150, 0]  // 0 : 150, 0.5 : 75, 1 : 0
      }),
    }],
  }}

interpolate()还支持定义多个区间段落,常用来定义静止区间等。举个例子,要让输入在接近-300 时取相反值,然后在输入接近-100 时到达 0,然后在输入接近 0 时又回到 1,接着一直到输入到 100 的过程中逐步回到 0,最后形成一个始终为 0 的静止区间,对于任何大于 100 的输入都返回 0。具体写法如下:

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

interpolate()还支持到字符串的映射,从而可以实现颜色以及带有单位的值的动画变换。例如你可以像下面这样实现一个旋转动画:

value.interpolate({
  inputRange: [0, 360],
  outputRange: ["0deg", "360deg"]
});

interpolate()还支持任意的渐变函数,其中有很多已经在Easing类中定义了,包括二次、指数、贝塞尔等曲线以及 step、bounce 等方法。interpolation还支持限制输出区间outputRange。你可以通过设置extrapolate、extrapolateLeft或extrapolateRight属性来限制输出区间。默认值是extend(允许超出),不过你可以使用clamp选项来阻止输出值超过outputRange。

  • 跟踪动态值

动画中所设的值还可以通过跟踪别的值得到。你只要把 toValue 设置成另一个动态值而不是一个普通数字就行了。比如我们可以用弹跳动画来实现聊天头像的闪动,又比如通过timing设置duration:0来实现快速的跟随。他们还可以使用插值来进行组合

Animated.spring(follower, { toValue: leader }).start();
Animated.timing(opacity, {
  toValue: pan.x.interpolate({
    inputRange: [0, 300],
    outputRange: [1, 0]
  })
}).start();

The leader and follower animated values would be implemented using Animated.ValueXY(). 是一个方便的处理 2D 交互的办法,譬如旋转或拖拽。它是一个简单的包含了两个Animated.Value实例的包装,然后提供了一系列辅助函数,使得ValueXY在许多时候可以替代Value来使用。比如在上面的代码片段中,leader和follower可以同时为valueXY类型,这样 x 和 y 的值都会被跟踪。

  • 跟踪手势

Animated.event是 Animated 中与输入有关的部分,允许手势或其它事件直接绑定到动态值上。它通过一个结构化的映射语法来完成,使得复杂事件对象中的值可以被正确的解开。第一层是一个数组,允许同时映射多个值,然后数组的每一个元素是一个嵌套的对象。在下面的例子里,你可以发现scrollX被映射到了event.nativeEvent.contentOffset.x(event通常是回调函数的第一个参数),并且pan.x和pan.y分别映射到gestureState.dx和gestureState.dy(gestureState是传递给PanResponder回调函数的第二个参数)。

 onScroll={Animated.event(
   // scrollX = e.nativeEvent.contentOffset.x
   [{ nativeEvent: {
        contentOffset: {
          x: scrollX
        }
      }
    }]
 )}
onPanResponderMove={Animated.event(
  [null, // ignore the native event
  // extract dx and dy from gestureState
  // like ‘pan.x = gestureState.dx, pan.y = gestureState.dy‘
  {dx: pan.x, dy: pan.y}
])}

响应当前的动画值

你可能会注意到这里没有一个明显的方法来在动画的过程中读取当前的值——这是出于优化的角度考虑,有些值只有在原生代码运行阶段中才知道。如果你需要在 javascript 中响应当前的值,有两种可能的办法:

  • spring.stopAnimation(callback)会停止动画并且把最终的值作为参数传递给回调函数callback——这在处理手势动画的时候非常有用。

  • spring.addListener(callback)会在动画的执行过程中持续异步调用callback回调函数,提供一个最近的值作为参数。这在用于触发状态切换的时候非常有用,譬如当用户拖拽一个东西靠近的时候弹出一个新的气泡选项。不过这个状态切换可能并不会十分灵敏,因为它不像许多连续手势操作(如旋转)那样在 60fps 下运行。

  • 启用原生动画驱动

Animated的 API 是可序列化的(即可转化为字符串表达以便通信或存储)。通过启用原生驱动,我们在启动动画前就把其所有配置信息都发送到原生端,利用原生代码在 UI 线程执行动画,而不用每一帧都在两端间来回沟通。如此一来,动画一开始就完全脱离了 JS 线程,因此此时即便 JS 线程被卡住,也不会影响到动画了。

在动画中启用原生驱动非常简单。只需在开始动画之前,在动画配置中加入一行useNativeDriver: true,如下所示:

Animated.timing(this.state.animatedValue, {
  toValue: 1,
  duration: 500,
  useNativeDriver: true // <-- 加上这一行
}).start();

动画值在不同的驱动方式之间是不能兼容的。因此如果你在某个动画中启用了原生驱动,那么所有和此动画依赖相同动画值的其他动画也必须启用原生驱动。

<Animated.ScrollView // <-- 使用可动画化的ScrollView组件
  scrollEventThrottle={1} // <-- 设为1以确保滚动事件的触发频率足够密集
  onScroll={Animated.event(
    [
      {
        nativeEvent: {
          contentOffset: { y: this.state.animatedValue }
        }
      }
    ],
    { useNativeDriver: true } // <-- 加上这一行
  )}
>
  {content}
</Animated.ScrollView>
  • LayoutAnimation API

LayoutAnimation允许你在全局范围内创建和更新动画,这些动画会在下一次渲染或布局周期运行。它常用来更新 flexbox 布局,因为它可以无需测量或者计算特定属性就能直接产生动画。尤其是当布局变化可能影响到父节点(譬如“查看更多”展开动画既增加父节点的尺寸又会将位于本行之下的所有行向下推动)时,如果不使用LayoutAnimation,可能就需要显式声明组件的坐标,才能使得所有受影响的组件能够同步运行动画。

注意尽管LayoutAnimation非常强大且有用,但它对动画本身的控制没有Animated或者其它动画库那样方便,所以如果你使用LayoutAnimation无法实现一个效果,那可能还是要考虑其他的方案。

另外,如果要在android上使用 LayoutAnimation,那么目前还需要在UIManager中启用::

/ 在执行任何动画代码之前,比如在入口文件App.js中执行
UIManager.setLayoutAnimationEnabledExperimental &&
  UIManager.setLayoutAnimationEnabledExperimental(true);

import React from ‘react‘;
import {
  NativeModules,
  LayoutAnimation,
  Text,
  TouchableOpacity,
  StyleSheet,
  View,
} from ‘react-native‘;

const { UIManager } = NativeModules;

UIManager.setLayoutAnimationEnabledExperimental &&
  UIManager.setLayoutAnimationEnabledExperimental(true);

export default class App extends React.Component {
  state = {
    w: 100,
    h: 100,
  };

  _onPress = () => {
    // Animate the update
    LayoutAnimation.spring();
    this.setState({w: this.state.w + 15, h: this.state.h + 15})
  }

  render() {
    return (
      <View style={styles.container}>
        <View style={[styles.box, {width: this.state.w, height: this.state.h}]} />
        <TouchableOpacity onPress={this._onPress}>
          <View style={styles.button}>
            <Text style={styles.buttonText}>Press me!</Text>
          </View>
        </TouchableOpacity>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: ‘center‘,
    justifyContent: ‘center‘,
  },
  box: {
    width: 200,
    height: 200,
    backgroundColor: ‘red‘,
  },
  button: {
    backgroundColor: ‘black‘,
    paddingHorizontal: 20,
    paddingVertical: 15,
    marginTop: 15,
  },
  buttonText: {
    color: ‘#fff‘,
    fontWeight: ‘bold‘,
  },
});
  • 其他要注意的地方

requestAnimationFrame

requestAnimationFrame是一个对浏览器标准 API 的兼容实现,你可能已经熟悉它了。它接受一个函数作为唯一的参数,并且在下一次重绘之前调用此函数。一些基于 JavaScript 的动画库高度依赖于这一 API。通常你不必直接调用它——那些动画库会替你管理好帧的更新。

setNativeProps

正如直接操作文档所说,setNativeProps方法可以使我们直接修改基于原生视图的组件的属性,而不需要使用setState来重新渲染整个组件树。

如果我们要更新的组件有一个非常深的内嵌结构,并且没有使用shouldComponentUpdate来优化,那么使用setNativeProps就将大有裨益。

如果你发现你的动画丢帧(低于 60 帧每秒),可以尝试使用setNativeProps或者shouldComponentUpdate来优化它们。Or you could run the animations on the UI thread rather than the JavaScript thread with the useNativeDriver option. 你还可以考虑将部分计算工作放在动画完成之后进行,这时可以使用InteractionManager。你还可以使用应用内的开发者菜单中的“FPS Monitor”工具来监控应用的帧率。

无障碍功能属性

accessible (ios, Android)

设置为true时表示当前视图是一个“无障碍元素”(accessibility element)。无障碍元素会将其所有子组件视为一整个可以选中的组件。默认情况下,所有可点击的组件(Touchable 系列组件)都是无障碍元素。

在 Android 上,React Native 视图的accessible={true}属性会被转译为原生视图对应的focusable={true}属性。

<View accessible={true}>
  <Text>text one</Text>
  <Text>text two</Text>
</View>

无障碍标签 accessibilityLabel (iOS, Android)

当一个视图启用无障碍属性后,最好再加上一个 accessibilityLabel(无障碍标签),这样可以让使用 VoiceOver 的人们清楚地知道自己选中了什么。VoiceOver 会读出选中元素的无障碍标签。

设定accessibilityLabel属性并赋予一个字符串内容即可在 View、Text 或是 Touchable 中启用无障碍标签:

<TouchableOpacity
  accessible={true}
  accessibilityLabel="Tap me!"
  onPress={this._onPress}>
  <View style={styles.button}>
    <Text style={styles.buttonText}>Press me!</Text>
  </View>
</TouchableOpacity>

无障碍提示 accessibilityHint (iOS, Android)

无障碍提示用于帮助用户理解操作可能导致什么后果,尤其是当这些后果并不能从无障碍标签中清楚地了解时。

要启用无障碍提示只需在需要设置的元素上设置accessibilityHint属性,并赋予用于解释的文本:

<TouchableOpacity
  accessible={true}
  accessibilityLabel="返回"
  accessibilityHint="返回到上一个页面"
  onPress={this._onPress}>
  <View style={styles.button}>
    <Text style={styles.buttonText}>Back</Text>
  </View>
</TouchableOpacity>
 

以上是关于RN - 动画的主要内容,如果未能解决你的问题,请参考以下文章

VSCode自定义代码片段7——CSS动画

VSCode自定义代码片段7——CSS动画

RN - 动画

[RN] requestAnimationFrame帧动画

使用嵌套片段和动画对象

Android:将“ViewPager”动画从片段更改为片段