Skip to content

Latest commit

 

History

History
1105 lines (885 loc) · 40.5 KB

File metadata and controls

1105 lines (885 loc) · 40.5 KB

六、向应用添加基本动画

在本章中,我们将介绍以下配方:

  • 创建简单动画
  • 运行多个动画
  • 创建动画通知
  • 胀缩容器
  • 创建带有加载动画的按钮

介绍

为了提供良好的用户体验,我们可能希望添加一些动画来引导用户的注意力,突出显示特定的动作,或者只是为我们的应用添加独特的触感。

正在进行一项计划,将所有处理从 JavaScript 移动到本机端。在编写本文时(React Native Version 0.58),我们可以选择使用本机驱动程序在本机世界中运行所有这些计算。遗憾的是,这不能用于所有动画,尤其是与布局相关的动画,例如 flexbox 属性。请阅读文档中有关使用本机动画时的注意事项的更多信息 http://facebook.github.io/react-native/docs/animations#caveats

本章中的所有方法都使用 JavaScript 实现。React Native 团队承诺在将所有处理移到本机端时使用相同的 API,因此我们不必担心破坏对现有 API 的更改。

创建简单动画

在本食谱中,我们将学习动画的基础知识。我们将使用一个图像来创建一个简单的从屏幕右侧到左侧的线性运动。

准备

为了完成这个配方,我们需要创建一个空的应用。我们叫它simple-animation

我们将在这个配方中使用一个云的 PNG 图像。您可以在 GitHub 上托管的配方存储库中的中找到该图像 https://github.com/warlyware/react-native-cookbook/tree/master/chapter-6/simple-animation/img/images 。将图像放入/img/images文件夹中,以便在应用中使用。

怎么做。。。

  1. 让我们首先打开App.js并导入App类的依赖项。Animated类将负责为动画创建值。它提供了一些可以设置动画的组件,还提供了一些方法和帮助程序来运行平滑动画。 Easing类提供了几种辅助方法,用于计算动作(如linearquadratic)和预定义动画(如bounceeaseelastic。 我们将使用Dimensions类获取当前设备大小,以便我们知道在动画初始化中放置元素的位置:
import React, { Component } from 'react';
import {
  Animated,
  Easing,
  Dimensions,
  StyleSheet,
  View,
} from 'react-native';
  1. 我们还将初始化应用中需要的一些常量。在本例中,我们将获取设备尺寸,设置图像大小,并require我们将设置动画的图像:
const { width, height } = Dimensions.get('window');
const cloudImage = require('./iimg/cloud.png');
const imageHeight = 200;
const imageWidth = 300;
  1. 现在,让我们创建App组件。我们将使用组件生命周期系统中的两种方法。如果您不熟悉此概念,请查看相关 React 文档(http://reactjs.cn/react/docs/component-specs.html 。本页还有一个关于生命周期挂钩如何工作的非常好的教程:
export default class App extends Component { 
  componentWillMount() { 
    // Defined on step 4 
  } 

  componentDidMount() { 
    // Defined on step 7 
  } 

  startAnimation () { 
    // Defined on step 5 
  } 

  render() { 
    // Defined on step 6 
  } 
} 

const styles = StyleSheet.create({ 
  // Defined on step 8 
}); 
  1. 为了创建动画,我们需要定义一个标准值来驱动动画。Animated.Value是一个处理随时间变化的每帧动画值的类。我们需要做的第一件事是在创建组件时创建这个类的实例。在这种情况下,我们使用的是componentWillMount方法,但我们也可以使用constructor甚至属性的默认值:
  componentWillMount() {
    this.animatedValue = new Animated.Value();
  }
  1. 创建动画值后,可以定义动画。我们还通过传递Animated.timingstart方法创建了一个循环,该方法使用箭头函数再次执行startAnimation函数。现在,当图像到达动画的末尾时,我们将再次启动相同的动画以创建无限循环动画:
  startAnimation() {
    this.animatedValue.setValue(width);
    Animated.timing(
      this.animatedValue,
      {
        toValue: -imageWidth,
        duration: 6000,
        easing: Easing.linear,
        useNativeDriver: true,
      }
    ).start(() => this.startAnimation());
  }
  1. 我们已经准备好了动画,但我们目前只计算每一帧随时间变化的值,而不使用这些值进行任何操作。下一步是在屏幕上渲染图像,并设置要设置动画的样式的属性。在这种情况下,我们希望在x轴上移动元素;因此,我们应该更新left属性:
  render() {
    return (
      <View style={styles.background}>
        <Animated.Image
          style={[
            styles.image,
            { left: this.animatedValue },
          ]}
          source={cloudImage}
        />
      </View>
    );
  }
  1. 如果我们刷新模拟器,我们会在屏幕上看到图像,但它还没有被设置动画。为了解决这个问题,我们需要调用startAnimation方法。一旦组件完全渲染,我们将使用componentDidMount生命周期挂钩启动动画:
  componentDidMount() {
    this.startAnimation();
  }
  1. 如果我们再次运行该应用,我们将看到图像如何在屏幕顶部移动,就像我们想要的那样!最后一步,让我们向应用添加一些基本样式:
const styles = StyleSheet.create({
  background: {
    flex: 1,
    backgroundColor: 'cyan',
  },
  image: {
    height: imageHeight,
    position: 'absolute',
    top: height / 3,
    width: imageWidth,
  },
});

输出如以下屏幕截图所示:

它是如何工作的。。。

步骤 5中,我们设置动画值。每次调用此方法时,第一行重置初始值。对于这个例子,初始值将是设备的width,它将把图像移动到屏幕的右侧,我们希望在那里开始动画。

然后,我们使用Animated.timing函数创建基于时间的动画,并获取两个参数。对于第一个参数,我们传入了在步骤 4中的componentWillMount生命周期钩子中创建的animatedValue。第二个参数是具有动画配置的对象。在本例中,我们将结束值设置为减去图像的宽度,这将把图像放置在屏幕的左侧。我们在那里完成动画。

在整个配置就绪的情况下,Animated类将计算分配给从右到左执行线性动画的 6 秒内所需的所有帧(通过duration属性设置为6000毫秒)。

我们有 React Native 提供的另一个助手,可以与Animated配对,称为Easing。在本例中,我们使用的是Easing助手类的linear属性。Easing提供了其他常用的缓和方法,如elasticbounce。查看Easing类文档,尝试为easing属性设置不同的值,以了解每个属性的工作方式。您可以在找到文档 https://facebook.github.io/react-native/docs/easing.html

一旦动画配置正确,我们需要运行它。我们通过调用start方法来实现这一点。此方法接收一个可选的callback函数参数,该参数将在动画完成时执行。在本例中,我们递归地运行相同的startAnimation函数。这将创建一个无限循环,这就是我们想要实现的。

步骤 6中,我们正在渲染图像。如果我们想给一个图像设置动画,我们应该始终使用Animate.Image组件。在内部,该组件将处理动画的值,并为本机组件上的每个帧设置每个值。这样可以避免在每一帧上运行 JavaScript 层中的 render 方法,从而实现更平滑的动画。

除了Image,我们还可以设置ViewTextScrollView组件的动画。这四个组件都支持开箱即用,但我们也可以创建一个新组件,并通过Animated.createAnimatedComponent()添加对动画的支持。这四个组件都能够处理样式更改。我们所要做的就是将animatedValue传递给我们想要设置动画的属性,在本例中是left属性,但我们可以在每个组件上使用任何可用的样式。

运行多个动画

在这个配方中,我们将学习如何在几个元素中使用相同的动画值。通过这种方式,我们可以重用相同的值以及插值,为其余元素获得不同的值。

此动画将类似于上一个配方。这一次,我们将有两个云:一个较小,移动较慢,另一个较大,移动较快。在屏幕中央,我们将看到一架静止的飞机。我们不会向飞机添加任何动画,但移动的云将使飞机看起来好像在移动。

准备

让我们从创建一个名为multiple-animations的空应用开始

我们将使用三种不同的图像:两朵云和一架飞机。您可以从配方的存储库下载图像,该存储库位于 GitHub 上的https://github.com/warlyware/react-native-cookbook/tree/master/chapter-6/multiple-animations/img/images 。确保将图像放在/img/images文件夹中。

怎么做。。。

  1. 让我们先打开App.js并添加我们的进口:
import React, { Component } from 'react';
import {
  View,
  Animated,
  Image,
  Easing,
  Dimensions,
  StyleSheet,
} from 'react-native';
  1. 此外,我们需要定义一些常量,并需要用于动画的图像。请注意,我们使用的云图像与cloudImage1cloudImage2相同,但在此配方中,我们将它们视为单独的实体:
const { width, height } = Dimensions.get('window');
const cloudImage1 = require('./iimg/cloud.png');
const cloudImage2 = require('./iimg/cloud.png');
const planeImage = require('./iimg/plane.gif');
const cloudHeight = 100;
const cloudWidth = 150;
const planeHeight = 60;
const planeWidth = 100;
  1. 在下一步中,我们将在创建组件时创建animatedValue实例,然后在组件完全渲染时启动动画。我们正在创建一个无限循环运行的动画。初始值为1,最终值为0。如果您不清楚此代码,请务必阅读本章中的第一个配方:
export default class App extends Component { 
  componentWillMount() { 
    this.animatedValue = new Animated.Value(); 
  } 

  componentDidMount() { 
    this.startAnimation(); 
  } 

  startAnimation () { 
    this.animatedValue.setValue(1); 
    Animated.timing( 
      this.animatedValue, 
      { 
        toValue: 0, 
        duration: 6000, 
        easing: Easing.linear, 
      } 
    ).start(() => this.startAnimation()); 
  } 

  render() { 
    // Defined in a later step
  } 
} 

const styles = StyleSheet.create({ 
  // Defined in a later step
}); 
  1. 这个食谱中的render方法将与上一个完全不同。在这个配方中,我们将使用相同的animatedValue设置两个图像的动画。动画值将返回从10的值;但是,我们希望将云从右向左移动,因此需要在每个元素上设置left值。 为了设置正确的值,我们需要插值animatedValue。对于较小的云,我们将初始left值设置为设备的宽度,但对于较大的云,我们将初始left值设置为远离设备右侧边缘。这将使移动距离更大,因此移动速度更快:
  render() {
    const left1 = this.animatedValue.interpolate({
      inputRange: [0, 1],
      outputRange: [-cloudWidth, width],
    });

    const left2 = this.animatedValue.interpolate({
      inputRange: [0, 1],
      outputRange: [-cloudWidth*5, width + cloudWidth*5],
    });

    // Defined in a later step
  } 
  1. 一旦我们有了正确的left值,我们就需要定义要设置动画的元素。这里,我们将插值设置为leftstyles 属性:
  render() {
    // Defined in a later step

    return (
      <View style={styles.background}>
        <Animated.Image
          style={[
            styles.cloud1,
            { left: left1 },
          ]}
          source={cloudImage1}
        />
        <Image
          style={styles.plane}
          source={planeImage}
        />
        <Animated.Image
          style={[
            styles.cloud2,
            { left: left2 },
          ]}
          source={cloudImage2}
        />
      </View>
    );
  }
  1. 最后一步,我们需要定义一些样式,只需要设置每个云的widthheight,并将样式分配给top
const styles = StyleSheet.create({
  background: {
    flex: 1,
    backgroundColor: 'cyan',
  },
  cloud1: {
    position: 'absolute',
    width: cloudWidth,
    height: cloudHeight,
    top: height / 3 - cloudWidth / 2,
  },
  cloud2: {
    position: 'absolute',
    width: cloudWidth * 1.5,
    height: cloudHeight * 1.5,
    top: height/2,
  },
  plane: {
    position: 'absolute',
    height: planeHeight,
    width: planeWidth,
    top: height / 2 - planeHeight,
    left: width / 2 - planeWidth,
  }
});
  1. 如果我们刷新应用,我们将看到动画:

它是如何工作的。。。

步骤 4中,我们定义了插值以获得每个云的left值。interpolate方法接收具有两个必需配置的对象inputRangeoutputRange

inputRange配置接收一个值数组。这些值应始终为升序值;也可以使用负值,只要值是递增的。

outputRange应与inputRange上定义的值数量相匹配。这些是插值结果所需的值。

对于这个配方,inputRange01,这是我们的animatedValue的值。在outputRange中,我们定义了我们所需要的移动限制。

创建动画通知

在这个配方中,我们将从头创建一个通知组件。显示通知时,组件将从屏幕顶部滑入。几秒钟后,我们将通过将其滑出自动隐藏它。

准备

我们将创建一个应用。我们叫它notification-animation

怎么做。。。

  1. 我们将从处理App组件开始。首先,让我们导入所有必需的依赖项:
import React, { Component } from 'react';
import {
  Text,
  TouchableOpacity,
  StyleSheet,
  View,
  SafeAreaView,
} from 'react-native';
import Notification from './Notification';
  1. 一旦我们导入了所有依赖项,我们就可以定义App类。在本例中,我们将使用等于falsenotify属性初始化state。我们将使用此属性显示或隐藏通知。默认情况下,通知不会显示在屏幕上。为了简单起见,我们将在state中使用要显示的文本定义message属性:
export default class App extends Component {
  state = {
    notify: false,
    message: 'This is a notification!',
  };

  toggleNotification = () => {
    // Defined on later step
  }

  render() {
    // Defined on later step
  }
}

const styles = StyleSheet.create({
    // Defined on later step
});
  1. render方法中,只有notify属性为true时才需要显示通知,我们可以通过if语句来实现:
  render() {
    const notify = this.state.notify
      ? <Notification
          autoHide
          message={this.state.message}
          onClose={this.toggleNotification}
        />
    : null;
    // Defined on next step
  }
  1. 在上一步中,我们只定义了对Notification组件的引用,但我们还没有使用它。让我们用这个应用所需的所有 JSX 来定义一个return。为了简单起见,我们只需要定义一个工具栏、一些文本和一个按钮,以在按下时切换通知的状态:
  render() {
    // Code from previous step
    return (
      <SafeAreaView>
        <Text style={styles.toolbar}>Main toolbar</Text>
        <View style={styles.content}>
          <Text>
            Lorem ipsum dolor sit amet, consectetur adipiscing 
            elit,
            sed do eiusmod tempor incididunt ut labore et 
            dolore magna.
          </Text>
          <TouchableOpacity
            onPress={this.toggleNotification}
            style={styles.btn}
          >
            <Text style={styles.text}>Show notification</Text>
          </TouchableOpacity>
          <Text>
            Sed ut perspiciatis unde omnis iste natus error sit 
            accusantium doloremque laudantium.
          </Text>
          {notify}
        </View>
      </SafeAreaView>
    );
  }
  1. 我们还需要定义切换statenotify属性的方法,非常简单:
  toggleNotification = () => {
    this.setState({
      notify: !this.state.notify,
    });
  }
  1. 这节课我们差不多讲完了。只剩下款式了。在这种情况下,我们只添加基本样式,如colorpaddingfontSizebackgroundColormargin,没有什么特别之处:
        const styles = StyleSheet.create({ 
          toolbar: { 
            backgroundColor: '#8e44ad', 
            color: '#fff', 
            fontSize: 22, 
            padding: 20, 
            textAlign: 'center', 
          }, 
          content: { 
            padding: 10, 
            overflow: 'hidden', 
          }, 
          btn: { 
            margin: 10, 
            backgroundColor: '#9b59b6', 
            borderRadius: 3, 
            padding: 10, 
          }, 
          text: { 
            textAlign: 'center', 
            color: '#fff', 
          }, 
        }); 
  1. 如果我们尝试运行应用,我们将看到一个错误,./Notification模块无法解决。让我们通过定义Notification组件来解决这个问题。让我们创建一个Notifications文件夹,其中包含一个index.js文件。然后,我们可以导入依赖项:
import React, { Componen } from 'react';
import {
  Animated,
  Easing,
  StyleSheet,
  Text,
} from 'react-native';
  1. 导入依赖项后,让我们定义道具和新组件的初始状态。我们将定义一些非常简单的东西,只是一个接收要显示的消息的属性,以及两个callback函数,允许在通知出现在屏幕上和关闭时运行某些操作。我们还将添加一个属性,设置自动隐藏通知之前显示通知的毫秒数:
export default class Notification extends Component {
  static defaultProps = {
    delay: 5000,
    onClose: () => {},
    onOpen: () => {},
  };

  state = {
    height: -1000,
  };
}
  1. 终于到了制作动画的时候了!我们需要在组件渲染后立即启动动画。如果以下代码中有不清楚的地方,我建议您查看本章中的第一个和第二个食谱:
  componentWillMount() {
    this.animatedValue = new Animated.Value();
  }

  componentDidMount() {
    this.startSlideIn();
  }

  getAnimation(value, autoHide) {
    const { delay } = this.props;
    return Animated.timing(
      this.animatedValue,
      {
        toValue: value,
        duration: 500,
        easing: Easing.cubic,
        delay: autoHide ? delay : 0,
      }
    );
  }
  1. 到目前为止,我们已经定义了一种获取动画的方法。对于滑入运动,我们需要计算从01的值。动画完成后,我们需要运行onOpen回调。如果调用onOpen方法时autoHide属性设置为true,我们将自动运行滑出动画以移除组件:
  startSlideIn () {
    const { onOpen, autoHide } = this.props;

    this.animatedValue.setValue(0);
    this.getAnimation(1)
      .start(() => {
        onOpen();
        if (autoHide){
          this.startSlideOut();
        }
      });
  }
  1. 与前一步类似,我们需要一种滑出运动的方法。这里,我们需要计算从10的值。我们将autoHide值作为参数发送给getAnimation方法。这将自动将动画延迟delay属性定义的毫秒数(在本例中为 5 秒)。动画完成后,我们需要运行onClose回调函数,该函数将从App类中删除组件:
  startSlideOut() {
    const { autoHide, onClose } = this.props;

    this.animatedValue.setValue(1);
    this.getAnimation(0, autoHide)
      .start(() => onClose());
  }
  1. 最后,让我们添加render方法。这里我们将得到props提供的message值。我们还需要组件的height来将组件移动到动画的初始位置;默认情况下,它是-1000,但我们将在接下来的步骤中在运行时设置正确的值。animatedValue0110,取决于通知是打开还是关闭;因此,我们需要对其进行插值以获得实际值。动画将从组件的高度减至0;这将产生一个漂亮的滑入/滑出动画:
  render() {
    const { message } = this.props;
    const { height } = this.state;
    const top = this.animatedValue.interpolate({
       inputRange: [0, 1],
       outputRange: [-height, 0],
     });
    // Defined on next step
   }
}
  1. 为了使事情尽可能简单,我们将返回一个带有一些文本的Animated.View。在这里,我们使用插值结果设置top样式,这意味着我们将为顶部样式设置动画。如前所述,我们需要在运行时计算组件的高度。为了实现这一点,我们需要使用视图的onLayout属性。每次布局更新时都会调用此函数,并将此组件的新尺寸作为参数发送:
  render() {
     // Code from previous step
     return (
      <Animated.View
        onLayout={this.onLayoutChange}
        style={[
          styles.main,
          { top }
        ]}
      >
        <Text style={styles.text}>{message}</Text>
      </Animated.View>
    );
   }
}
  1. onLayoutChange方法将非常简单。我们只需要得到新的height并更新state。此方法接收到一个event。从这个对象中,我们可以获取有用的信息。出于我们的目的,我们将访问event对象中nativeEvent.layout处的数据。layout对象包含屏幕的widthheight以及Animated.View调用此功能的屏幕上的xy位置:
  onLayoutChange = (event) => {
    const {layout: { height } } = event.nativeEvent;
     this.setState({ height });
   }
  1. 最后一步,我们将向通知组件添加一些样式。因为我们想让这个组件在任何其他组件之上设置动画,所以我们需要将position设置为absolute,并将leftright属性设置为0。我们还将添加一些颜色和填充:
        const styles = StyleSheet.create({ 
          main: { 
            backgroundColor: 'rgba(0, 0, 0, 0.7)', 
            padding: 10, 
            position: 'absolute', 
            left: 0, 
            right: 0, 
          }, 
          text: { 
            color: '#fff', 
          }, 
       }); 
  1. 最终的应用应类似于以下屏幕截图:

它是如何工作的。。。

步骤 3中,我们定义了Notification组件。该组件接收三个参数:一个在几秒钟后自动隐藏组件的标志、我们想要显示的消息和一个callback函数,该函数将在通知关闭时执行。

当执行onClose回调时,我们将切换notify属性以移除Notification实例并清除内存。

步骤 4中,我们定义了 JSX 来呈现我们应用的组件。在其他组件之后渲染Notification组件非常重要,这样该组件将显示在所有其他组件之上

步骤 6中,我们定义了组件的statedefaultProps对象为每个属性设置默认值。如果没有为给定属性指定值,则将应用这些值。

我们将每个callback的默认值定义为空函数。这样,我们不必在尝试执行这些道具之前检查它们是否有值。

对于初始的state,我们定义了height属性。实际的height值将在运行时根据message属性中接收到的内容进行计算。这意味着我们首先需要渲染远离原始位置的组件。因为在计算布局时有一个短暂的延迟,所以我们根本不希望在通知移动到正确位置之前显示它。

步骤 9中,我们创建了动画。getAnimation方法接收两个参数:要应用的delay和确定通知是否自动关闭的autoHide布尔值。我们在步骤 10步骤 11中使用了这种方法。

步骤 13中,我们为该组件定义了 JSX。当布局有更新时,onLayout功能对于获取组件的尺寸非常有用。例如,如果设备方向更改,则尺寸将更改,在这种情况下,我们希望更新动画的初始和最终坐标。

还有更多。。。

当前的实现工作得很好,但是我们应该解决一个性能问题。目前,在动画的每一帧上都会执行onLayout方法,这意味着我们正在更新每一帧上的状态,这将导致每一帧上的组件重新渲染!我们应该避免这种情况,并且只更新一次以获得实际高度。

为了解决这个问题,如果当前值与初始值不同,我们可以添加一个简单的验证来更新状态。这将避免在每一帧上更新state,并且我们不会一次又一次地强制渲染:

onLayoutChange = (event) => { 
  const {layout: { height } } = event.nativeEvent; 
 if (this.state.height === -1000) { 
    this.setState({ height }); 
 } 
} 

虽然这对于我们的目的是有效的,但我们还可以更进一步,确保height在方向改变时也得到更新。不过,我们就到此为止,因为这个食谱已经很长了。

胀缩容器

在这个配方中,我们将创建一个带有titlecontent的定制容器元素。当用户按下标题时,内容将折叠或展开。这个配方将允许我们探索LayoutAnimationAPI。

准备

让我们从创建一个新的应用开始。我们称之为collapsable-containers

一旦我们创建了应用,我们还将创建一个包含有index.js文件的Panel文件夹,用于存放我们的Panel组件。

怎么做。。。

  1. 让我们从关注Panel组件开始。首先,我们需要导入将用于此类的所有依赖项:
import React, { Component } from 'react';
import {
  View,
  LayoutAnimation,
  StyleSheet,
  Text,
  TouchableOpacity,
} from 'react-native';
  1. 一旦我们有了依赖项,让我们声明用于初始化该组件的defaultProps。在这个配方中,我们只需要将expanded属性初始化为false
export default class Panel extends Component {
  static defaultProps = {
    expanded: false
  };
}

const styles = StyleSheet.create({
  // Defined on later step
});
  1. 我们将使用state对象的height属性来展开或折叠容器。第一次创建此组件时,我们需要检查expanded属性以设置正确的初始height
  state = {
    height: this.props.expanded ? null : 0,
  };
  1. 让我们呈现此组件所需的 JSX 元素。我们需要从state中获取height值,并将其设置为内容的样式视图。当按下title元素时,我们将执行toggle方法(后面定义)来更改状态的height值:
  render() {
    const { children, style, title } = this.props;
    const { height } = this.state;

    return (
      <View style={[styles.main, style]}>
        <TouchableOpacity onPress={this.toggle}>
          <Text style={styles.title}>
            {title}
          </Text>
        </TouchableOpacity>
        <View style={{ height }}>
          {children}
        </View>
      </View>
    );
  }
  1. 如前所述,toggle方法将在按下title元素时执行。在这里,我们将切换state上的height,并调用在下一个渲染周期更新样式时要使用的动画:
  toggle = () => {
    LayoutAnimation.spring();
    this.setState({
      height: this.state.height === null ? 0 : null,
    })
  }
  1. 为了完成这个组件,让我们添加一些简单的样式。我们需要将overflow设置为hidden,否则组件折叠时会显示内容:
const styles = StyleSheet.create({
  main: {
    backgroundColor: '#fff',
    borderRadius: 3,
    overflow: 'hidden',
    paddingLeft: 30,
    paddingRight: 30,
  },
  title: {
    fontWeight: 'bold',
    paddingTop: 15,
    paddingBottom: 15,
  }
  1. 一旦我们定义了Panel组件,让我们在App类上使用它。首先,我们需要App.js中的所有依赖项:
import React, { Component } from 'react';
import {
  Text,
  StyleSheet,
  View,
  SafeAreaView,
  Platform,
  UIManager
} from 'react-native';
import Panel from './Panel';
  1. 在上一步中,我们导入了Panel组件。我们将在 JSX 中声明此类的三个实例:
 export default class App extends Component {
  render() {
    return (
      <SafeAreaView style={[styles.main]}>
        <Text style={styles.toolbar}>Animated containers</Text>
        <View style={styles.content}>
          <Panel
            title={'Container 1'}
            style={styles.panel}
          >
            <Text style={styles.panelText}>
              Temporibus autem quibusdam et aut officiis
              debitis aut rerum necessitatibus saepe
              eveniet ut et voluptates repudiandae sint et
              molestiae non recusandae.
            </Text>
          </Panel>
          <Panel
            title={'Container 2'}
            style={styles.panel}
              >
            <Text style={styles.panelText}>
              Et harum quidem rerum facilis est et expedita 
              distinctio. Nam libero tempore,
              cum soluta nobis est eligendi optio cumque.
            </Text>
          </Panel>
          <Panel
            expanded
            title={'Container 3'}
            style={styles.panel}
           >
            <Text style={styles.panelText}>
              Nullam lobortis eu lorem ut vulputate.
            </Text>
            <Text style={styles.panelText}>
              Donec id elementum orci. Donec fringilla lobortis 
              ipsum, vitae commodo urna.
            </Text>
          </Panel>
        </View>
      </SafeAreaView>
    );
  }
}
  1. 我们在这个配方中使用的是 React NativeLayoutAnimationAPI。在当前版本的 React Native 中,Android 上默认禁用此 API。在安装App组件之前,我们将使用Platform助手和UIManager在 Android 设备上启用此功能:
  componentWillMount() {
    if (Platform.OS === 'android') {
      UIManager.setLayoutAnimationEnabledExperimental(true);
    }
  }
  1. 最后,让我们向工具栏和主容器添加一些样式。我们只需要一些您现在可能已经习惯的简单样式:paddingmargincolor
const styles = StyleSheet.create({
  main: {
    flex: 1,
  },
  toolbar: {
    backgroundColor: '#3498db',
    color: '#fff',
    fontSize: 22,
    padding: 20,
    textAlign: 'center',
  },
  content: {
    padding: 10,
    backgroundColor: '#ecf0f1',
    flex: 1,
  },
  panel: {
    marginBottom: 10,
  },
  panelText: {
    paddingBottom: 15,
  }
});
  1. 最终的应用应类似于以下屏幕截图:

它是如何工作的。。。

步骤 3中,我们设置了内容的初始height。如果expanded属性设置为true,那么我们应该显示内容。通过将height值设置为null,版面系统将根据内容计算height;否则,我们需要将该值设置为0,这将在组件折叠时隐藏内容。

步骤 4中,我们为Panel组件定义了所有 JSX。本步骤中有几个概念值得介绍。首先,从props对象传入children属性,当App类中使用该组件时,该对象将包含在<Panel></Panel>之间定义的任何元素。这非常有用,因为通过使用此属性,我们允许此组件作为子组件接收任何其他组件。

在同一步骤中,我们还将从state对象获取height,并将其设置为应用于Viewstyle,其中包含可折叠内容。这将更新height,导致组件相应膨胀或折叠。我们还声明了onPress回调,它在按下 title 元素时切换state上的height

步骤 7 中我们定义了toggle方法,该方法切换height值。在这里,我们使用了LayoutAnimation类。通过调用spring方法,布局系统将在下一次渲染时为布局发生的每个更改设置动画。在这种情况下,我们只更改了height,但我们可以更改我们想要的任何其他属性,例如opacitypositioncolor

LayoutAnimation类包含两个预定义的动画。在这个配方中,我们使用了spring,但我们也可以使用lineareaseInEaseOut,或者您可以使用configureNext方法创建自己的配方。

如果我们移除LayoutAnimation,我们将看不到动画;组件将通过从0跳到总高度而膨胀和塌陷。但通过添加这一行,我们可以轻松地添加一个漂亮、平滑的动画。如果需要对动画进行更多控制,则可能需要使用动画 API。

步骤 9中,我们检查了Platform助手上的 OS 属性,该属性返回'android''ios'字符串,具体取决于应用运行的设备。如果应用在 Andriod 上运行,我们将使用UIManager助手的setLayoutAnimationEnabledExperimental方法启用LayoutAnimationAPI。

另见

创建带有加载动画的按钮

在本食谱中,我们将继续使用LayoutAnimation类。在这里,我们将创建一个按钮,当用户按下按钮时,我们将显示一个加载指示器并为样式设置动画。

准备

要开始,我们需要创建一个空应用。我们叫它button-loading-animation

我们还要为我们的Button组件创建一个Button文件夹,其中包含一个index.js文件。

怎么做。。。

  1. 让我们从Button/index.js文件开始。首先,我们将导入此组件的所有依赖项:
import React, { Component } from 'react';
import {
  ActivityIndicator,
  LayoutAnimation,
  StyleSheet,
  Text,
  TouchableOpacity,
  View,
} from 'react-native';
  1. 对于这个组件,我们将只使用四个道具:一个label、一个loading布尔值来切换显示按钮内的加载指示器或标签、一个按下按钮时执行的回调函数以及自定义样式。在这里,我们将init加载到falsedefaultProps,以及加载到空函数的handleButtonPress
export default class Button extends Component {
  static defaultProps = {
    loading: false,
    onPress: () => {},
  };
  // Defined on later steps
}
  1. 我们将尽可能简化此组件的render方法。我们将根据loading属性的值呈现标签和活动指示器:
  render() {
    const { loading, style } = this.props;

    return (
      <TouchableOpacity
        style={[
          styles.main,
          style,
          loading ? styles.loading : null,
        ]}
        activeOpacity={0.6}
        onPress={this.handleButtonPress}
      >
        <View>
          {this.renderLabel()}
          {this.renderActivityIndicator()}
        </View>
      </TouchableOpacity>
    );
  }
  1. 为了呈现label,我们需要检查loading属性是否为false。如果是,那么我们只返回一个带有我们从props收到的labelText元素:
  renderLabel() {
    const { label, loading } = this.props;
    if(!loading) {
      return (
        <Text style={styles.label}>{label}</Text>
      );
    }
  }
  1. 同样地,renderActivityIndicator指示器应仅在loading属性的值为true时应用。如果是,我们将退回ActivityIndicator组件。我们将使用ActivityIndicator的道具来定义一个size小的color白的#fff
  renderActivityIndicator() {
    if (this.props.loading) {
      return (
        <ActivityIndicator size="small" color="#fff" />
      );
    }
  }
  1. 我们班还有一种方法:handleButtonPress。我们需要在按下按钮时通知该组件的父级,这可以通过调用通过props传递给该组件的onPress回调来完成。我们还将使用LayoutAnimation在下一次渲染时对动画进行排队:
  handleButtonPress = () => {
    const { loading, onPress } = this.props;

    LayoutAnimation.easeInEaseOut();
    onPress(!loading);
  }
  1. 要完成此组件,我们需要添加一些样式。我们将定义一些颜色、圆角、对齐、填充等。对于loading样式,将在显示加载指示器时应用,我们将更新填充以在加载指示器周围创建一个圆圈:
const styles = StyleSheet.create({
  main: {
    backgroundColor: '#e67e22',
    borderRadius: 20,
    padding: 10,
    paddingLeft: 50,
    paddingRight: 50,
  },
  label: {
    color: '#fff',
    fontWeight: 'bold',
    textAlign: 'center',
    backgroundColor: 'transparent',
  },
  loading: {
    padding: 10,
    paddingLeft: 10,
    paddingRight: 10,
  },
});
  1. 我们已经完成了Button组件。现在,让我们学习App课程。让我们从导入所有依赖项开始:
import React, { Component } from 'react';
import {
  Text,
  StyleSheet,
  View,
  SafeAreaView,
  Platform,
  UIManager
} from 'react-native';
import Button from './Button';
  1. App类相对简单。我们只需要在state对象上定义一个loading属性,它将切换Button的动画。我们还将呈现一个toolbar和一个Button
export default class App extends Component {
  state = {
    loading: false,
  };

  // Defined on next step

  handleButtonPress = (loading) => {
    this.setState({ loading });
  }

  render() {
    const { loading } = this.state;

    return (
      <SafeAreaView style={[styles.main, android]}>
        <Text style={styles.toolbar}>Animated containers</Text>
        <View style={styles.content}>
          <Button
            label="Login"
            loading={loading}
            onPress={this.handleButtonPress}
          />
        </View>
      </SafeAreaView>
    );
  }
}
  1. 与上一个配方一样,我们需要在 Android 设备上手动启用LayoutAnimationAPI:
  componentWillMount() {
    if (Platform.OS === 'android') {
      UIManager.setLayoutAnimationEnabledExperimental(true);
    }
  }
  1. 最后,我们将添加一些styles,只是一些颜色、填充和对齐,以便在屏幕上使按钮居中:
const styles = StyleSheet.create({
  main: {
    flex: 1,
  },
  toolbar: {
    backgroundColor: '#f39c12',
    color: '#fff',
    fontSize: 22,
    padding: 20,
    textAlign: 'center',
  },
  content: {
    padding: 10,
    backgroundColor: '#ecf0f1',
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
});
  1. 最终的应用应类似于以下屏幕截图:

它是如何工作的。。。

步骤 3中,我们为Button组件添加了render方法。在这里,我们收到了loading属性,并基于该值,将相应的样式应用于TouchableOpacity按钮元素。我们还使用了两种方法:一种用于呈现标签,另一种用于呈现活动指示器。

步骤 6中,我们执行了onPress回调。默认情况下,我们声明了一个空函数,因此不必检查值是否存在。

调用onPress回调时,此按钮的父级应负责更新加载属性。在这个组件中,我们只负责在按下此按钮时通知家长。

LayoutAnimation.eadeInEaseOut方法仅对下一渲染阶段的动画进行排队,这意味着动画不会立即执行。我们负责更改要设置动画的样式。如果我们不更改任何样式,则不会看到任何动画。

Button组件不知道loading属性是如何更新的。这可能是由于提取请求、超时或任何其他操作造成的。父组件负责更新loading属性。无论何时发生任何更改,我们都会将新样式应用于按钮,并将出现平滑的动画。

步骤 9中,我们定义了App类的内容。这里,我们使用我们的Button组件。当按下按钮时,loading属性的state被更新,这将导致每次按下按钮时动画运行。

结论

在本章中,我们介绍了制作 React 本机应用动画的基本原理。这些方法旨在提供有用的实用代码解决方案,并确定如何使用基本构建块,以便您能够更好地创建适合应用的动画。希望到现在为止,您应该已经习惯于使用AnimatedLayoutAnimation动画助手了。在第 7 章将高级动画添加到您的应用中,我们将结合在这里学到的内容,构建更复杂、更有趣的以应用为中心的 UI 动画