最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • React Native 动画小结

    正文概述 掘金(joking_zhang)   2021-07-13   523

    引言

    三十功名尘与土,八千里路云和月。莫等闲,白了少年头,空悲切。——岳飞《满江红》

    在脉脉做 React Native 也有一小段时间了,大概小半年吧,期间主要负责搜索相关的开发。和之前的 PC 端的开发不同的是,C 端(移动端)会更加注重用户的体验,而流程、有意义的动画对于移动用户的使用体验的提升,是非常重要的。

    接下来,让我们来聊聊如何在 React Native 中,实现各种各样的小动画吧~

    前置知识

    如何完成一个基础的动画

    创建动画,首先需要创建一个 Animated.Value ,将它连接到动画组件的一个或多个样式属性,然后使用Animated.timing()通过动画效果展示数据的变化。

    不要直接修改 Animated.Value 的值,应该使用 useRef Hook 或者 state 返回一个动画值的引用,然后使用 xxx.setValue(val) 来设置那种不需要动画过程的值;使用 Animated.timing 等函数来设置需要过程的动画。更加详细的教程、API解析还请移步官方文档,这里不再赘述。

    可以设置什么样的属性用来呈现动画

    下面是我最近开发的时候碰到的一个报错:

    React Native 动画小结

    大致就是说,在 React Native 中,Native 模块不支持使用 height 属性来实现动画。翻了一下官方的博客,在 Using Native Driver for Animated 中有提到:只能使用 transformopacity 这种非布局属性来实现动画,但是不支持使用 Flexboxposition 这种影响布局的样式属性来实现动画。

    一些常用的示例

    接下来,带大家看几个工作中常见的小动画:

    最简单的动画:反馈提示 Message 组件

    React Native 动画小结

    如上图,我们需要在进入这个页面的时候,在顶部弹出一个提示。下面是相关代码:

    import React, { useEffect, useState, useRef } from 'react';
    import { Animated, Easing, Text } from 'react-native';
    
    function AnimatedMessage() {
      const translateY = useRef(new Animated.Value(0)); // 初始化动画
      const [display, setDisplay] = useState('flex');
      useEffect(() => {
        openAnimation(); // 成功加载组件之后,触发动画
      }, []);
    
      const openAnimation = () => {
        // 触发第一阶段展开提示信息的动画
        Animated.timing(translateY.current, {
          toValue: 36,
          duration: 1500,
          easing: Easing.elastic(0.8),
        }).start(closeAnimation);
      };
    
      const closeAnimation = () => {
        // 触发第二阶段收起提示信息的动画
        setTimeout(() => {
          Animated.timing(translateY.current, {
            toValue: 0,
            duration: 1500,
            easing: Easing.elastic(0.8),
          }).start(() => {
            // 彻底隐藏该元素
            setDisplay('none');
          });
        }, 800);
      };
    
      const wrapperStyles = {
        position: 'absolute',
        top: 0,
        left: 0,
        width: '100%',
        transform: [{ translateY: translateY.current }],
        display,
      };
    
      return (
        <Animated.View style={wrapperStyles}>
          <Text
            style={{
              fontFamily: 'PingFangSC-Regular',
              color: '#724804',
              fontSize: 13,
            }}
            numberOfLines={1}
          >
            会员搜索权益,本次为你找到 999+ 个人脉
          </Text>
        </Animated.View>
      );
    }
    
    export default AnimatedMessage;
    

    我们来分解一些这个过程。

    首先,我们有一个 Animated.View 组件,使用绝对定位,让它位于页面的顶部。我们可以通过修改 transform:translateY 属性来实现元素在 Y 轴的偏移。

    组件完成初始化之后,调用 openAnimation 函数来触发动画的第一阶段。

    在动画的第一阶段,我们使用 Animated.timing修改元素的 translateY 值,0 -> 36,使得整个过程呈一个线性的变化。这样,我们就能看到整个元素从列表顶部慢慢的冒出来了。接下来,我们需要再让组件原路返回,在其 start回调中,触发动画的第二阶段 closeAnimation

    在动画的第二阶段,我们再次使用 Animated.timing修改元素元素的 translateY 值,36 -> 0,这样,我们就可以看到组件慢慢的收起了。最后在其 start回调中,修改 display:none,彻底的隐藏元素。

    组合动画:弹窗 Modal 组件

    在我们日常工作中,弹窗是一个更加常见的使用场景。而官方的 Modal 组件只是一个可以盖在整个视图上的组件,其他的效果还需要开发者自行实现。首先,看下最终效果:

    React Native 动画小结

    如上图,当点击「职位要求」之后,需要出现一个黑色半透明的遮罩,以及一个盖在上面的内容区块。下面是相关代码:

    import React, { useEffect, useState, useRef } from 'react';
    import {
      Modal,
      Dimensions,
      Animated,
      TouchableWithoutFeedback,
      View,
      Easing,
    } from 'react-native';
    
    import _ from 'underscore';
    
    const screenHeight = Dimensions.get('screen').height;
    
    function BasicModal(props) {
      const {
        visible = false,
        // Wrapper
        style = {},
        // Body
        bodyStyle = {},
        // Mask
        maskStyle,
        onBodyLayout,
        onClose,
      } = props;
    
      const [animatedVisible, setAnimatedVisible] = useState(visible);
      // ========================= Anim =========================
      const animRef = useRef({
        backgroundOpacityValue: new Animated.Value(0),
        transformYValue: new Animated.Value(0),
      });
    
      const updateAnimValue = val => {
        animRef.current.backgroundOpacityValue.setValue(val);
        animRef.current.transformYValue.setValue(val);
      };
    
      const showAnimating = () => {
        updateAnimValue(0);
    
        Animated.parallel([
          Animated.timing(animRef.current.backgroundOpacityValue, {
            toValue: 1,
            duration: 300,
            easing: Easing.easeIn,
            useNativeDriver: true,
          }),
          Animated.spring(animRef.current.transformYValue, {
            toValue: 1,
            tension: 50,
            friction: 8,
            useNativeDriver: true,
          }),
        ]).start();
    
        setAnimatedVisible(true);
      };
    
      const hideAnimating = () => {
        Animated.parallel([
          Animated.timing(animRef.current.backgroundOpacityValue, {
            toValue: 0,
            duration: 100,
            easing: Easing.linear,
            useNativeDriver: true,
          }),
    
          Animated.spring(animRef.current.transformYValue, {
            toValue: 0,
            tension: 50,
            friction: 7,
            useNativeDriver: true,
          }),
        ]).start();
    
        setTimeout(() => {
          setAnimatedVisible(false);
        }, 200);
      };
      // ========================= Effect =========================
    
      useEffect(() => {
        if (visible) {
          showAnimating();
        } else {
          hideAnimating();
        }
        return () => {};
        // eslint-disable-next-line react-hooks/exhaustive-deps
      }, [visible]);
      // ========================= Event =========================
      const handleClose = _.debounce(
        () => {
          hideAnimating();
    
          onClose?.();
        },
        100,
        true
      );
    
      // ========================= render =========================
      const renderBackground = () => {
        const bgOpacityAnim = animRef.current.backgroundOpacityValue.interpolate({
          inputRange: [0, 1],
          outputRange: [0, 0.8],
        });
    
        return (
          <TouchableWithoutFeedback onPress={handleClose}>
            <Animated.View
              style={{
                position: 'absolute',
                opacity: bgOpacityAnim,
                left: 0,
                top: 0,
                right: 0,
                bottom: 0,
                backgroundColor: '#333333',
                ...maskStyle,
              }}
            />
          </TouchableWithoutFeedback>
        );
      };
    
      const renderPanelContent = () => {
        const transformYValue = animRef.current.transformYValue.interpolate({
          inputRange: [0, 1],
          outputRange: [screenHeight, 0],
        });
        const contentStyle = {
          ...style,
        };
    
        return (
          <Animated.View
            style={{
              transform: [
                {
                  translateY: transformYValue,
                },
              ],
              position: 'absolute',
              bottom: 30,
              left: 16,
              right: 16,
              flex: 1,
            }}
          >
            <Animated.View style={contentStyle}>
    
              {/* Body */}
              <View style={bodyStyle}>{props.children}</View>
    
            </Animated.View>
          </Animated.View>
        );
      };
    
      const renderPanel = () => {
        return (
          <View style={{ flex: 1 }}>
            {renderBackground()}
            {renderPanelContent()}
          </View>
        );
      };
    
      return (
        <Modal
          visible={animatedVisible}
          animationType={'none'}
          transparent={true}
          presentationStyle={'overFullScreen'}
          onRequestClose={handleClose}
          hardwareAccelerated={true}
        >
          <View
            style={{ top: 0, left: 0, bottom: 0, right: 0, position: 'absolute' }}
            onLayout={onBodyLayout}
          />
          {renderPanel()}
        </Modal>
      );
    }
    
    export default BasicModal;
    

    我们来分解一些整个过程。

    这个组件的实现思路,是通过监听 props.visible 的改变,来触发弹窗「出现」「收起」的动画。

    props.visible = true 的时候,弹窗「出现」的动画 showAnimating 会被触发。这个动画由两部分组成,分别是底部的遮罩出现,透明度 0 -> 0.8;内容从底部弹出,Y轴 transform:translateY 屏幕高度 (Dimensions.get('screen').height)-> 0。

    多个动画可以通过parallel(同时执行)来实现。底部的遮罩的出现是一个线性的过程,使用的 Animated.timing() 函数即可实现;而内容的出现,有一个弹簧的效果,可以使用 Animated.spring() 来实现。

    任何动画的过程,都可以理解为一个 0 -> 1 的过程。因此,当我们在设置 Animated 的动画函数时,都可以将 toValue 设置为 0 或者 1 作为动画开始和结束的值。然后在 render 函数中,调用 interpolate 将这个“标准”的输入,映射成实际的值。

    当我们点击遮罩、底部按钮、右上角关闭按钮的时候,会传入 props.visible = false 。弹窗「收起」的动画 hideAnimating 会被触发。

    差值:跟随手势的 Tag

    当我们在手机上浏览信息时,伴随着我们滑动的手势,在列表进行翻页的同时,也会触发一些动画。

    React Native 动画小结

    如上图所示,当我们向下滑动时,会展开一些Tag;向上滑动时,会收起这些Tag。这里,就需要监听滚动事件,然后触发相关的动画,具体的代码如下:

    import React, { useEffect, useState, useRef } from "react";
    import {
      Modal,
      Dimensions,
      Animated,
      TouchableWithoutFeedback,
      View,
      Easing,
    } from "react-native";
    
    const screenHeight = Dimensions.get("screen").height;
    
    function ScrollList(props) {
      const {
        visible = false,
        // Wrapper
        style = {},
        // Body
        bodyStyle = {},
        // Mask
        maskStyle,
        onBodyLayout,
        onClose,
      } = props;
    
      // ========================= Anim =========================
      const animRef = useRef({
        scrollY: new Animated.Value(0),
        transformYValue: new Animated.Value(0),
        translateY: null,
        translateYListener: null,
      });
    
      // ========================= Effect =========================
    
      useEffect(() => {
        const scrollY = animRef.current.scrollY;
        let tagHeight = 36;
        let tagHeightInput = tagHeight;
        // 根据不同平台,调整动画帧数
        if (Platform.OS === "android") {
          tagHeightInput *= 2;
        } else {
          tagHeightInput *= 1.2;
        }
        const scrollYClamped = diffClamp(scrollY, 0, tagHeightInput);
    
        const translateY = scrollYClamped.interpolate({
          inputRange: [0, tagHeightInput], // 修改动画帧数
          outputRange: [0, -tagHeight],
        });
    
        animRef.current.translateYListener = translateY.addListener(({ value }) => {
          animRef.current.translateYNumber.setValue(value);
        });
    
        animRef.current.translateY = translateY;
    
        return () => {
          animRef.current.translateY.removeListener(
            animRef.current.translateYListener
          );
        };
      }, []);
      // ========================= Event =========================
    
      // ========================= render =========================
      const tagStyles = {};
    
      const listStyles = {
        flex: 1,
        zIndex: -1,
      };
      // 当列表位置发生偏移的时候,需要增加一个负的 margin 来缩小列表的可视区域,否则会出现遮挡
      if (animRef.current.translateYNumber) {
        listStyles.marginBottom = animRef.current.translateYNumber;
      }
      tagStyles.transform = [{ translateY: animRef.current.translateY }];
      listStyles.transform = [{ translateY: animRef.current.translateY }];
    
      return (
        <View style={{ flex: 1 }}>
          <Animated.View style={tagStyles}>{tags}</Animated.View>
          <Animated.ScrollView // <-- 使用可动画化的ScrollView组件
            style={listStyles}
            scrollEventThrottle={1} // <-- 设为1以确保滚动事件的触发频率足够密集
            onScroll={Animated.event(
              [
                {
                  nativeEvent: {
                    contentOffset: { y: animRef.current.scrollY },
                  },
                },
              ],
              { useNativeDriver: true } // <-- 加上这一行
            )}
          >
            {content}
          </Animated.ScrollView>
        </View>
      );
    }
    
    export default ScrollList;
    

    我们来分解一些整个过程。

    可以将整个页面分为2个区域,顶部的Tag,和下面的列表。

    进入页面之后,为列表添加滚动事件的监听。因为列表以及顶部的Tag上下的偏移量有一个最大值,和最小值,如果超过这个值即使触发滚动事件也不会修改偏移量。

    所以,可以使用 diffClamp 得到一个限制在最小、最大值的新的动画值。然后,使用 interpolation 计算出实际的偏移量。

    需要注意的是,如果真机实际体验不佳,设置 interpolation 的时候,可以根据不同平台,调整动画帧数,增加 inputRange 的范围。

    结语

    在学习 React Native 之初,一个问题伴随着我,就是 React Native 都可以实现哪些动效?通过本文的梳理之后,也有了一个大致的答案。

    本文更多的是抛砖引玉,和大家聊聊使用 React Native 实现动画的一些思路,上面例子中的代码,是从工程中脱敏之后总结的? ,并不是可以直接运行的Demo。

    PS:对脉脉感兴趣的小伙伴,欢迎发送简历到 496691544@qq.com ,我可以帮忙内推~

    参考

    • Using Native Driver for Animated · React Native
    • 动画 · React Native 中文网
    • Animated · React Native 中文网

    起源地下载网 » React Native 动画小结

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元