React Native中的实际动画示例

本文详细介绍了如何在React Native中实现各种动画效果,包括视觉反馈、系统状态显示、过渡动画和注意力吸引。通过构建包含新闻、按钮、进度和展开页面的应用,演示了如何使用react-native-animatable库来创建这些效果,提升移动应用的用户体验。
摘要由CSDN通过智能技术生成

在本教程中,您将学习如何实现移动应用程序中常用的动画。 具体来说,您将学习如何实现以下动画:

  • 提供视觉反馈:例如,当用户按下某个按钮时,您想使用动画向用户显示该按钮确实已被按下。
  • 显示当前系统状态:在执行无法立即完成的过程(例如,上传照片或发送电子邮件)时,您想要显示动画,以便用户知道该过程将花费多长时间。
  • 直观地连接过渡状态:当用户按下按钮以将某些东西带到屏幕的前面时,应对过渡进行动画处理,以便用户知道元素的起源。
  • 吸引用户的注意力:当收到重要通知时,您可以使用动画来吸引用户的注意力。

另外,如果您想继续学习,可以在其GitHub repo中找到本教程中使用的完整源代码。

我们正在建设什么

我们将构建一个应用程序,该应用程序可以实现我前面提到的每种不同类型的动画。 具体来说,我们将创建以下页面,每个页面将出于不同目的实现动画。

  • 新闻页面 :使用手势提供视觉反馈并显示当前系统状态。
  • 按钮页面 :使用按钮提供视觉反馈并显示当前系统状态。
  • 进度页 :使用进度条显示当前系统状态。
  • 扩展页面 :使用扩展和收缩运动直观地连接过渡状态。
  • AttentionSeeker页面 :使用醒目的动作来吸引用户的注意力。

如果要查看每个动画的预览,请查看此Imgur相册

设置项目

首先创建一个新的React Native项目:

react-native init RNPracticalAnimations

创建项目后,在新创建的文件夹中导航,打开package.json文件,并将以下内容添加到dependencies

"react-native-animatable": "^0.6.1",
"react-native-vector-icons": "^3.0.0"

执行npm install安装这两个软件包。 react-native-animatable用于轻松实现动画, react-native-vector-icons用于稍后为扩展页面呈现图标。 如果您不想使用图标,则可以坚持使用“ Text组件。 否则,请按照react-native-vector-icons在其GitHub页面上的安装说明进行操作。

构建应用

打开index.android.js文件或index.ios.js文件,并将现有内容替换为以下内容:

import React, { Component } from 'react';

import {
  AppRegistry
} from 'react-native';

import NewsPage from './src/pages/NewsPage';
import ButtonsPage from './src/pages/ButtonsPage';
import ProgressPage from './src/pages/ProgressPage';
import ExpandPage from './src/pages/ExpandPage';
import AttentionSeekerPage from './src/pages/AttentionSeekerPage';

class RNPracticalAnimation extends Component {
  render() {
    return (
       <NewsPage />
    );
  }
}

AppRegistry.registerComponent('RNPracticalAnimation', () => RNPracticalAnimation);

完成后,请确保创建相应的文件,以免出现任何错误。 我们将要处理的所有文件都存储在src目录下。 该目录内有以下文件夹:

  • components :其他组件或页面将使用的可重用组件。
  • img :将在整个应用程序中使用的图像。 您可以从GitHub存储库中获取图像。
  • pages :应用程序的页面。

新闻页面

让我们从“新闻”页面开始。

首先,添加我们将要使用的组件:

import React, { Component } from 'react';

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

import NewsItem from '../components/NewsItem';

除了RefreshControl和自定义NewsItem组件(我们将在以后创建)之外,您应该已经熟悉其中的大多数内容。 RefreshControl用于在ScrollViewListView组件内添加“拉动刷新”功能。 因此,实际上它是为我们处理向下滑动手势和动画的人。 无需自己执行。 随着您获得更多使用React Native的经验,您会注意到动画实际上是内置在某些组件中的,因此无需使用Animated类来实现自己的组件。

创建将容纳整个页面的组件:

export default class NewsPage extends Component {
    ...
}

constructor函数内部,初始化一个动画值,用于存储新闻项的当前不透明度( opacityValue )。 我们希望新闻在刷新时具有更少的不透明度。 这使用户有了一个想法,即在刷新新闻项时他们无法与整个页面进行交互。 is_news_refreshing用作指示新闻项当前是否正在刷新的开关。

constructor(props) {
    super(props);
    this.opacityValue = new Animated.Value(0);
    this.state = {
        is_news_refreshing: false,
        news_items: [
            {
                title: 'CTO Mentor Network – a virtual peer-to-peer network of CTOs',
                website: 'ctomentor.network',
                url: 'https://ctomentor.network/'
            },
            {
                title: 'The No More Ransom Project',
                website: 'nomoreransom.org',
                url: 'https://www.nomoreransom.org/'
            },
            {
                title: 'NASA Scientists Suggest We’ve Been Underestimating Sea Level Rise',
                website: 'vice.com',
                url: 'http://motherboard.vice.com/read/nasa-scientists-suggest-weve-been-underestimating-sea-level-rise'
            },
            {
                title: 'Buttery Smooth Emacs',
                website: 'facebook.com',
                url: 'https://www.facebook.com/notes/daniel-colascione/buttery-smooth-emacs/10155313440066102/'
            },
            {
                title: 'Elementary OS',
                website: 'taoofmac.com',
                url: 'http://taoofmac.com/space/blog/2016/10/29/2240'
            },
            {
                title: 'The Strange Inevitability of Evolution',
                website: 'nautil.us',
                url: 'http://nautil.us/issue/41/selection/the-strange-inevitability-of-evolution-rp'
            },
        ]
    }
}

opacity()函数是将触发动画以更改不透明度的函数。

opacity() {
    this.opacityValue.setValue(0);
    Animated.timing(
      this.opacityValue,
      {
        toValue: 1,
        duration: 3500,
        easing: Easing.linear
      }
    ).start();
}

render()函数内部,定义不透明度值将如何变化。 在这里, outputRange[1, 0, 1] outputRange [1, 0, 1] ,这意味着它将以完全不透明开始,然后变为零不透明,然后再次回到完全不透明。 正如在opacity()函数中定义的那样,此转换将在3500毫秒(3.5秒)的过程中完成。

render() {

    const opacity = this.opacityValue.interpolate({
      inputRange: [0, 0.5, 1],
      outputRange: [1, 0, 1]
    });

    ...
}

<RefreshControl>组件被添加到<ScrollView> 。 每当用户在列表顶部时向下滑动(当scrollY0 ),此方法都会调用refreshNews()函数。 您可以添加colors属性以自定义刷新动画的颜色。

return (
    <View style={styles.container}>
        <View style={styles.header}>
        </View>
        <ScrollView 
            refreshControl={
                <RefreshControl
                    colors={['#1e90ff']}
                    refreshing={this.state.is_news_refreshing}
                    onRefresh={this.refreshNews.bind(this)}
                />
            }
            style={styles.news_container}>
            ...
        </ScrollView>
    </View>
);

<ScrollView> ,使用<Animated.View>组件,并将style设置为opacity值的style

<Animated.View style={[{opacity}]}>
    { this.renderNewsItems() }
</Animated.View>

refreshNews()函数调用opacity()函数,并将is_news_refreshing的值更新为true 。 这使<RefreshControl>组件知道刷新动画应该已经显示。 之后,使用setTimeout()函数在3,500毫秒(3.5秒)后将is_news_refreshing的值更新回false 。 这将从视图中隐藏刷新动画。 到那时,还应该完成不透明度动画,因为我们之前在opacity函数中为持续时间设置了相同的值。

refreshNews() {
    this.opacity();
    this.setState({is_news_refreshing: true});
    setTimeout(() => {
        this.setState({is_news_refreshing: false});
    }, 3500);
}

renderNewsItems()需要的新闻条目的阵列,我们较早的内声明constructor()和使用使得它们中的每<NewsItem>组件。

renderNewsItems() {
    return this.state.news_items.map((news, index) => {
        return (
            <NewsItem key={index} index={index} news={news} />
        );
    });
}
NewsItem组件

NewsItem组件( src/components/NewsItem.js )呈现新闻项目的标题和网站,并将它们包装在<Button>组件内,以便可以与它们进行交互。

import React, { Component } from 'react';

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

import Button from './Button';

const NewsItem = ({ news, index }) => {

    function onPress(news) {
        //do anything you want
    }
    
    return (
        <Button
            key={index}
            noDefaultStyles={true}
            onPress={onPress.bind(this, news)}
        >
            <View style={styles.news_item}>
                <Text style={styles.title}>{news.title}</Text>
                <Text>{news.website}</Text>
            </View>
        </Button>
    );
}

const styles = StyleSheet.create({
    news_item: {
        flex: 1,
        flexDirection: 'column',
        paddingRight: 20,
        paddingLeft: 20,
        paddingTop: 30,
        paddingBottom: 30,
        borderBottomWidth: 1,
        borderBottomColor: '#E4E4E4'
    },
    title: {
        fontSize: 20,
        fontWeight: 'bold'
    }
});

export default NewsItem;
按钮组件

Button组件( src/components/Button.js )使用TouchableHighlight组件来创建一个按钮。 underlayColor道具用于指定按下按钮时参考底色的颜色。 这是React Native提供视觉反馈的内置方式。 稍后,在“ 按钮页面”部分中,我们将介绍按钮可以提供视觉反馈的其他方式。

import React, { Component } from 'react';

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

const Button = (props) => {

    function getContent() {
        if(props.children){
            return props.children;
        }
        return <Text style={props.styles.label}>{props.label}</Text>
    }

    return (
        <TouchableHighlight 
            underlayColor="#ccc" 
            onPress={props.onPress} 
            style={[
                props.noDefaultStyles ? '' : styles.button, 
                props.styles ? props.styles.button : '']}
        >
            { getContent() }
        </TouchableHighlight>
    );
}

const styles = StyleSheet.create({
    button: {
        alignItems: 'center',
        justifyContent: 'center',
        padding: 20,
        borderWidth: 1,
        borderColor: '#eee',
        margin: 20
    }
});

export default Button;

回到NewsPage组件,添加样式:

const styles = StyleSheet.create({
    container: {
        flex: 1,
    },
    header: {
        flexDirection: 'row',
        backgroundColor: '#FFF',
        padding: 20,
        justifyContent: 'space-between',
        borderBottomColor: '#E1E1E1',
        borderBottomWidth: 1
    },
    news_container: {
        flex: 1,
    }
});

按钮页面

按钮页面( src/pages/ButtonsPage.js )显示三种按钮:突出显示的常用按钮,变大的按钮以及显示操作当前状态的按钮。 首先添加必要的组件:

import React, { Component } from 'react';

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

import Button from '../components/Button';
import ScalingButton from '../components/ScalingButton';
import StatefulButton from '../components/StatefulButton';

之前,您了解了Button组件的工作方式,因此我们仅关注其他两个按钮。

缩放按钮组件

首先,让我们看一下缩放按钮( src/components/ScalingButton.js )。 与我们之前使用的按钮不同,它使用内置的TouchableWithoutFeedback组件来创建按钮。 之前,我们使用了TouchableHighlight组件,该组件随附所有的钟声和哨声,可以将其视为按钮。 您可以将TouchableWithoutFeedback视为一个简单的按钮,您必须在其中指定用户点击时需要执行的所有操作。 这对于我们的用例来说是完美的,因为我们不必担心默认按钮的行为会妨碍我们要实现的动画。

import React, { Component } from 'react';

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

就像Button组件一样,这将是组件的一种功能类型,因为我们实际上不需要使用状态。

const ScalingButton = (props) => {
    ...
}

在组件内部,创建一个动画值,该值将存储当前按钮比例。

var scaleValue = new Animated.Value(0);

添加将启动比例动画的功能。 我们不希望该应用程序显示缓慢,因此请使duration尽可能duration ,但又要足够长,以使用户可以感知正在发生的事情。 300毫秒是一个很好的起点,但是请随意使用该值。

function scale() {
    scaleValue.setValue(0);
    Animated.timing(
        scaleValue,
        {
          toValue: 1,
          duration: 300,
          easing: Easing.easeOutBack
        }
    ).start();
}

定义按钮将如何缩放( outputRange ),具体取决于当前值( inputRange )。 我们不希望它变得太大,所以我们坚持将1.1设为最高值。 这意味着它将比整个动画( 0.5 )中途的原始大小大0.1

const buttonScale = scaleValue.interpolate({
  inputRange: [0, 0.5, 1],
  outputRange: [1, 1.1, 1]
});

return (
    <TouchableWithoutFeedback onPress={onPress}>
        <Animated.View style={[ 
            props.noDefaultStyles ? styles.default_button : styles.button, 
            props.styles ? props.styles.button : '',
            {
                transform: [
                    {scale: buttonScale}
                ]
            }
            ]}
        >
            { getContent() }
        </Animated.View>
    </TouchableWithoutFeedback>
);

onPress()函数首先执行缩放动画,然后调用用户通过道具传递的方法。

function onPress() {
    scale();
    props.onPress();
}

getContent()函数输出子组件(如果有)。 如果不是,则呈现包含label props的Text组件。

function getContent() {
    if(props.children){
        return props.children;
    }
    return <Text style={props.styles.label}>{ props.label }</Text>;
}

添加样式并导出按钮:

const styles = StyleSheet.create({
    default_button: {
        alignItems: 'center',
        justifyContent: 'center'
    },
    button: {
        alignItems: 'center',
        justifyContent: 'center',
        padding: 20,
        borderWidth: 1,
        borderColor: '#eee',
        margin: 20
    },
});

export default ScalingButton;
有状态按钮组件

接下来是有状态按钮( src/components/StatefulButton.js )。 按下时,此按钮将更改其背景颜色并显示加载图像,直到完成其执行的操作为止。

我们将使用的加载图像是gif动画。 默认情况下,Android上的React Native不支持动画gif。 为了使其工作,您必须编辑android/app/build.gradle文件并在dependencies下添加compile 'com.facebook.fresco:animated-gif:0.12.0' ,如下所示:

dependencies {
    //default dependencies here
    
    compile 'com.facebook.fresco:animated-gif:0.12.0'
}

如果您使用的是iOS,则默认情况下,动画gif应该可以使用。

回到状态按钮组件,就像缩放按钮一样,它使用TouchableWithoutFeedback组件创建按钮,因为它还将实现自己的动画。

import React, { Component } from 'react';

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

与缩放按钮不同,此组件将是成熟的基于类的组件,因为它管理自己的状态。

在builder constructor()内部,创建一个动画值来存储当前背景色。 之后,初始化用作开关的状态,以存储按钮的当前状态。 默认情况下,它设置为false 。 一旦用户点击该按钮,它将被更新为true并且仅在完成虚拟过程后再次设置为false

export default class StatefulButton extends Component {

    constructor(props) {
        super(props);
        this.colorValue = new Animated.Value(0);
        this.state = {
            is_loading: false
        }
    }
}

render()函数中,根据动画值的当前值指定将使用的不同背景色。

render() {

    const colorAnimation = this.colorValue.interpolate({
      inputRange: [0, 50, 100],
      outputRange: ['#2196f3', '#ccc', '#8BC34A']
    });

    ...
}

接下来,将所有内容包装在TouchableWithoutFeedback组件内,并在<Animated.View>内应用动画背景色。 如果is_loading的当前值为true我们还将渲染加载器图像。 按钮标签也会根据该值而变化。

return (
    <TouchableWithoutFeedback onPress={this.onPress.bind(this)}>
        <Animated.View style={[
            styles.button_container,
            this.props.noDefaultStyles ? '' : styles.button, 
            this.props.styles ? this.props.styles.button : '',
            {
                backgroundColor: colorAnimation
            },
            ]}>
            { 
                this.state.is_loading && 
                <Image
                  style={styles.loader}
                  source={require('../img/ajax-loader.gif')}
                />
            }
            <Text style={this.props.styles.label}>
            { this.state.is_loading ? 'loading...' : this.props.label}
            </Text>
        </Animated.View>
    </TouchableWithoutFeedback>
);

按下按钮后,在执行动画之前,它首先执行通过道具传递的功能。

onPress() {
    this.props.onPress();
    this.changeColor();
}

changeColor()函数负责更新状态并设置按钮的背景色动画。 在这里,我们假设该过程将花费3,000毫秒(3秒)。 但是在现实世界中,您并不总是知道一个过程将花费多长时间。 您可以做的是让动画在较短的时间内执行,然后递归调用changeColor()函数,直到处理完成。

changeColor() {
  this.setState({
    is_loading: true
  });

  this.colorValue.setValue(0);
  Animated.timing(this.colorValue, {
    toValue: 100,
    duration: 3000
  }).start(() => {
    this.setState({
        is_loading: false
    });
  });  
}

添加样式:

const styles = StyleSheet.create({
    button_container: {
        flexDirection: 'row',
        alignItems: 'center',
        backgroundColor: '#2196f3'
    },
    button: {
        alignItems: 'center',
        justifyContent: 'center',
        padding: 20,
        borderWidth: 1,
        borderColor: '#eee',
        margin: 20
    },
    loader: {
        width: 16,
        height: 16,
        marginRight: 10
    }
});

返回“按钮”页面:创建组件,渲染三种按钮并添加其样式。

export default class ButtonsPage extends Component {
    press() {
        //do anything you want
    }

    render() {
        return (
            <View style={styles.container}>
                <Button 
                    underlayColor={'#ccc'} 
                    label="Ordinary Button" 
                    onPress={this.press.bind(this)}
                    styles={{button: styles.ordinary_button, label: styles.button_label}} />

                <ScalingButton 
                    label="Scaling Button" 
                    onPress={this.press.bind(this)}
                    styles={{button: styles.animated_button, label: styles.button_label}} />

                <StatefulButton 
                    label="Stateful Button" 
                    onPress={this.press.bind(this)}
                    styles={{button: styles.stateful_button, label: styles.button_label}} />
            </View>
        );
    }
}
 
const styles = StyleSheet.create({
    container: {
        flex: 1,
        flexDirection: 'column',
        padding: 30
    },
    ordinary_button: {
        backgroundColor: '#4caf50',
    },
    animated_button: {
        backgroundColor: '#ff5722'  
    },
    button_label: {
        color: '#fff',
        fontSize: 20,
        fontWeight: 'bold'
    }
});

进度页

进度页面( src/pages/ProgressPage.js )在长时间运行的过程中向用户显示进度动画。 我们将实现自己的方法而不是使用内置组件,因为React Native还没有统一的方式来实现进度条动画。 如果您有兴趣,这里是两个内置进度条组件的链接:

要构建“进度”页面,请先导入所需的组件:

import React, { Component } from 'react';

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

我们正在使用Dimensions来获取设备宽度。 由此,我们可以计算出进度条可用的宽度。 为此,我们减去将添加到容器中的左右填充的总和,以及减去将添加到进度条容器中的左右边框的总和。

var { width } = Dimensions.get('window');
var available_width = width - 40 - 12;

为了使以上公式有意义,让我们直接跳至样式:

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
    justifyContent: 'center'
  },
  progress_container: {
    borderWidth: 6,
    borderColor: '#333',
    backgroundColor: '#ccc'
  },
  progress_status: {
    color: '#333',
    fontSize: 20,
    fontWeight: 'bold',
    alignSelf: 'center'
  }
});

container每一边都有20的padding -因此我们从available_width减去40。 progress_container两侧各有6个边框,因此我们再次将其加倍,然后从进度条宽度中减去12。

export default class ProgressPage extends Component {
  
  constructor(props) {
    super(props);
    this.progress = new Animated.Value(0);
    this.state = {
      progress: 0
    };
  }

}

创建组件,并在构造函数内部创建动画值,以存储进度条的当前动画值。

我说“值”是因为这次我们将使用此单个动画值来为进度条的宽度和背景色设置动画。 稍后您将看到此操作。

除此之外,还需要初始化状态中的当前进度。

export default class ProgressPage extends Component {
  
  constructor(props) {
    super(props);
    this.progress = new Animated.Value(0);
    this.state = {
      progress: 0
    };
  }

}

render()函数内部, progress_container用作进度条的容器,而其中的<Animated.View>是实际进度条,其宽度和背景颜色将根据当前进度而变化。 在它下面,我们还将以文本形式(0%到100%)呈现当前进度。

render() {
    return (
        <View style={styles.container}>
          <View style={styles.progress_container}>
            <Animated.View
              style={[this.getProgressStyles.call(this)]}
            > 
            </Animated.View>
          </View>
          <Text style={styles.progress_status}>
          { this.state.progress }
          </Text>
        </View>
    );
}

进度条的样式由getProgressStyles()函数返回。 在这里,我们使用较早的动画值来计算宽度和背景色。 这样做是为了代替为每个动画创建单独的动画值,因为无论如何我们都插值相同的值。 如果我们使用两个单独的值,则需要并行制作两个动画,这会降低效率。

getProgressStyles() {
  var animated_width = this.progress.interpolate({
    inputRange: [0, 50, 100],
    outputRange: [0, available_width / 2, available_width]
  });
  //red -> orange -> green
  const color_animation = this.progress.interpolate({
    inputRange: [0, 50, 100],
    outputRange: ['rgb(199, 45, 50)', 'rgb(224, 150, 39)', 'rgb(101, 203, 25)']
  });

  return {
    width: animated_width,
    height: 50, //height of the progress bar
    backgroundColor: color_animation
  }
}

一旦安装了组件,动画将立即执行。 首先设置初始进度值,然后将侦听器添加到当前进度值。 这使我们可以在每次进度值更改时更新状态。 我们正在使用parseInt() ,因此将进度值转换为整数。 之后,我们以7,000毫秒(7秒)的持续时间开始动画。 完成后,我们将进度文本更改为完成!

componentDidMount() {
    this.progress.setValue(0);
    this.progress.addListener((progress) => {
      this.setState({
        progress: parseInt(progress.value) + '%'
      });
    });

    Animated.timing(this.progress, {
      duration: 7000,
      toValue: 100
    }).start(() => {
      this.setState({
        progress: 'done!'
      })
    });
}

展开页面

展开页面( src/pages/ExpandPage.js )显示了如何使用展开和收缩动作直观地连接过渡状态。 向用户展示特定元素的形成方式非常重要。 它回答了有关元素来自何处以及在当前上下文中其作用的问题。 与往常一样,首先导入我们需要的东西:

import React, { Component } from 'react';

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

import Icon from 'react-native-vector-icons/FontAwesome';
import ScalingButton from '../components/ScalingButton';

在builder constructor()内部,创建一个动画值,该值将存储菜单的当前y位置。 这个想法是要有一个足以容纳所有菜单项的大盒子。

最初,该框的bottom位置将为负值。 这意味着默认情况下仅显示整个方框的尖端。 一旦用户点击菜单,整个框看起来就好像被展开了,实际上,我们只是在改变bottom位置,以便显示所有内容。

您可能想知道为什么我们使用这种方法而不是仅缩放框以容纳其所有子级。 那是因为我们只需要缩放height属性。 想一想当您仅调整图像的高度或宽度时,图像会发生什么—它们看起来很拉伸。 框内的元素也会发生同样的事情。

回到constructor() ,我们还添加了一个状态标志,指示菜单当前是否处于展开状态。 我们之所以需要这样做,是因为如果菜单已经被展开,我们需要隐藏用于展开菜单的按钮。

export default class ExpandPage extends Component {

  constructor(props) {
    super(props);
    this.y_translate = new Animated.Value(0);
    this.state = {
      menu_expanded: false
    };
  }

  ...
}

render()函数中,指定如何平移bottom位置。 inputRange01 ,而outputRange0-300 。 因此,如果y_translate的值为0 ,则不会发生任何事情,因为outputRange等效项为0 。 但是,如果该值变为1 ,则菜单的bottom位置将从其原始位置转换为-300

注意负号,因为如果只有300 ,则该框将进一步下降。 如果为负数,则相反。

render() {
    const menu_moveY = this.y_translate.interpolate({
        inputRange: [0, 1],
        outputRange: [0, -300]
    });
    
    ...
}

为了使这个更有意义,让我们跳到样式:

const styles = StyleSheet.create({
  container: {
    flex: 10,
    flexDirection: 'column'
  },
  body: {
    flex: 10,
    backgroundColor: '#ccc'
  },
  footer_menu: {
    position: 'absolute',
    width: 600,
    height: 350, 
    bottom: -300, 
    backgroundColor: '#1fa67a',
    alignItems: 'center'
  },
  tip_menu: {
    flexDirection: 'row'
  },
  button: {
    backgroundColor: '#fff'
  },
  button_label: {
    fontSize: 20,
    fontWeight: 'bold'
  }
});

注意footer_menu样式。 其总height设置为350bottom位置为-300 ,这意味着默认情况下仅显示顶部50 。 执行平移动画以展开菜单时, bottom位置最终的值为0 。 为什么? 因为如果您还记得减负数时的规则,则两个减号将变为正数。 因此(-300) - (-300)变为(-300) + 300

我们都知道在添加正数和负数时会发生什么:它们彼此抵消。 因此, bottom位置最终变为0 ,并显示整个菜单。

回到render()函数,我们具有主要内容( body )和页脚菜单,该菜单将被展开和缩小。 translateY Y变换用于平移其在Y轴上的位置。 因为整个container flex: 10body也为flex: 10 ,所以起点实际上是在屏幕的最底部。

return (
  <View style={styles.container}>
    <View style={styles.body}></View>
    <Animated.View 
      style={[ 
        styles.footer_menu,
        {
          transform: [
            {
              translateY: menu_moveY
            }
          ]
        }
      ]}
    >
      
      ...

    </Animated.View>
  </View>
);

<Animated.View>内部是tip_menu和完整菜单。 如果菜单已展开,我们不希望显示提示菜单,因此仅在menu_expanded设置为false时才呈现它。

{
  !this.state.menu_expanded &&
  <View style={styles.tip_menu}>
    <ScalingButton onPress={this.openMenu.bind(this)} noDefaultStyles={true}>
      <Icon name="ellipsis-h" size={50} color="#fff" />
    </ScalingButton>
  </View>
}

另一方面,如果menu_expanded设置为true ,我们只想显示完整菜单。 每个按钮会将菜单缩小到其原始位置。

{
  !this.state.menu_expanded &&
  <View style={styles.tip_menu}>
    <ScalingButton onPress={this.openMenu.bind(this)} noDefaultStyles={true}>
      <Icon name="ellipsis-h" size={50} color="#fff" />
    </ScalingButton>
  </View>
}

打开菜单时,首先需要更新状态,以便呈现隐藏的菜单。 只有完成后才能执行翻译动画。 与Animated.timing相比,它使用了Animated.spring来给Animated.timing添加一些趣味性。 您提供给friction的值越高,则反弹越少。 切记不要过度制作动画,因为动画可能会给用户带来麻烦,而不是帮助用户。

openMenu() {
  this.setState({
    menu_expanded: true
  }, () => {
    this.y_translate.setValue(0);
    Animated.spring(
      this.y_translate,
      {
        toValue: 1,
        friction: 3
      }
    ).start();
  });
}

hideMenu()功能与showMenu()的相反,因此我们只需将其功能反转即可:

hideMenu() {
  this.setState({
    menu_expanded: false
  }, () => {
    this.y_translate.setValue(1);
    Animated.spring(
      this.y_translate,
      {
        toValue: 0,
        friction: 4
      }
    ).start();
  });
}

AttentionSeeker页面

最后但并非最不重要的是attentionseeker页面( src/pages/AttentionSeekerPage.js )。 我知道本教程已经很长了,为了使内容更短,让我们使用react-native-animatable包来实现此页面的动画。

import React, { Component } from 'react';

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

import * as Animatable from 'react-native-animatable';
import ScalingButton from '../components/ScalingButton';

创建一个包含动画类型和每个框要使用的背景色的数组:

var animations = [
  ['bounce', '#62B42C'],
  ['flash', '#316BA7'],
  ['jello', '#A0A0A0'],
  ['pulse', '#FFC600'],
  ['rotate', '#1A7984'],
  ['rubberBand', '#435056'],
  ['shake', '#FF6800'],
  ['swing', '#B4354F'],
  ['tada', '#333333']
];

创建组件:

export default class AttentionSeekerPage extends Component {
   
   ... 
}

render()函数使用renderBoxes()函数创建三行,每行将渲染三个框。

render() {
  return (
    <View style={styles.container}>
      <View style={styles.row}>
        { this.renderBoxes(0) }
      </View>

      <View style={styles.row}>
        { this.renderBoxes(3) }
      </View>

      <View style={styles.row}>
        { this.renderBoxes(6) }
      </View>
    </View>
  );
}

renderBoxes()函数呈现动画框。 这使用提供的起始索引作为参数来提取数组的特定部分并分别渲染它们。

在这里,我们使用的是<Animatable.View>组件,而不是<Animated.View> 。 这接受了animationiterationCount作为道具。 animation指定要执行的animation的类型, iterationCount指定要执行动画的次数。 在这种情况下,我们只想打扰用户,直到他们按一下框。

renderBoxes(start) {
    var selected_animations = animations.slice(start, start + 3);
    return selected_animations.map((animation, index) => {
      return (

        <ScalingButton 
          key={index}
          onPress={this.stopAnimation.bind(this, animation[0])} 
          noDefaultStyles={true}
        >
          <Animatable.View 
            ref={animation[0]}
            style={[styles.box, { backgroundColor: animation[1] }]}
            animation={animation[0]} 
            iterationCount={"infinite"}>
            <Text style={styles.box_text}>{ animation[0] }</Text>
          </Animatable.View>
        </ScalingButton>

      );
    });
}

stopAnimation()阻止动画框。 这使用“引用”来唯一标识每个框,以便可以分别停止它们。

stopAnimation(animation) {
    this.refs[animation].stopAnimation();
}

最后,添加样式:

const styles = StyleSheet.create({
  container: {
    flex: 1,
    flexDirection: 'column',
    padding: 20
  },
  row: {
    flex: 1,
    flexDirection: 'row',
    justifyContent: 'space-between'
  },
  box: {
    alignItems: 'center',
    justifyContent: 'center',
    height: 100,
    width: 100,
    backgroundColor: '#ccc'
  },
  box_text: {
    color: '#FFF'
  }
});

结论

在本教程中,您学习了如何实现移动应用程序中常用的一些动画。 具体来说,您已经学习了如何实现提供视觉反馈,显示当前系统状态,以视觉方式连接过渡状态并吸引用户注意力的动画。

与往常一样,在动画方面还有很多东西要学习。 例如,我们仍然没有涉及以下领域:

翻译自: https://code.tutsplus.com/tutorials/practical-animations-in-react-native--cms-27567

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值