Animated

Animated库旨在使动画变得流畅,强大并易于构建和维护。Animated侧重于输入和输出之间的声明性关系,以及两者之间的可配置变换,此外还提供了简单的 start/stop方法来控制基于时间的动画执行。

创建动画最基本的工作流程是先创建一个 Animated.Value ,将它连接到动画组件的一个或多个样式属性,然后使用Animated.timing()通过动画效果展示数据的变化:

请不要直接修改动画值!你可以用useRef Hook来返回一个可修改的 ref 引用。ref 对象的current属性在初始化时被赋予给定的动画值,且在组件的生命周期内保存不被销毁。

Animated提供了两种类型的值:

  • Animated.Value()用于单个值
  • Animated.ValueXY()用于矢量值
  • Animated.Value可以绑定到样式或是其他属性上,也可以进行插值运算。单个Animated.Value可以用在任意多个属性上。

配置动画

Animated提供了三种动画类型。每种动画类型都提供了特定的函数曲线,用于控制动画值从初始值变化到最终值的变化过程:

  • Animated.decay()以指定的初始速度开始变化,然后变化速度越来越慢直至停下。
  • Animated.spring()提供了一个基础的弹簧物理模型.
  • Animated.timing()使用easing 函数让数值随时间动起来。

大多数情况下你应该使用timing()。默认情况下,它使用对称的 easeInOut 曲线,将对象逐渐加速到全速,然后通过逐渐减速停止结束。

使用动画

通过在动画上调用start()来启动动画。 start()可以传入一个回调函数,以便在动画完成时得到通知调用。如果动画运行正常,则完成回调收到的值为{finished:true}。如果动画是因为调用了stop()而结束(例如,因为它被手势或其他动画中断),则它会收到{finished:false}。

Animated.timing({}).start(({finished}) => {
  /* 动画完成的回调函数 */
});

启用原生动画驱动

使用原生动画,我们会在开始动画之前将所有关于动画的内容发送到原生代码,从而使用原生代码在 UI 线程上执行动画,而不是通过对每一帧的桥接去执行动画。一旦动画开始,JS 线程就可以在不影响动画效果的情况下阻塞(去执行其他任务)掉了。

您可以通过在动画配置中指定useNativeDriver:true 来使用原生动画驱动。你可以在动画文档 中看到更详细的解释。

自定义动画组件

组件必须经过特殊处理才能用于动画。所谓的特殊处理主要是指把动画值绑定到属性上,并且在一帧帧执行动画时避免 react 重新渲染和重新调和的开销。此外还得在组件卸载时做一些清理工作,使得这些组件在使用时是安全的。

createAnimatedComponent()方法正是用来处理组件,使其可以用于动画。
Animated中默认导出了以下这些可以直接使用的动画组件,当然它们都是通过使用上面这个方法进行了封装:

Animated.Image
Animated.ScrollView
Animated.Text
Animated.View
Animated.FlatList
Animated.SectionList

组合动画

动画还可以使用组合函数以复杂的方式进行组合:

  • Animated.delay()在给定延迟后开始动画。
  • Animated.parallel()同时启动多个动画。
  • Animated.sequence()按顺序启动动画,等待每一个动画完成后再开始下一个动画。
  • Animated.stagger()按照给定的延时间隔,顺序并行的启动动画。

动画也可以通过将toValue设置为另一个动画的Animated.Value来简单的链接在一起。请参阅动画指南中的跟踪动态值值。

默认情况下,如果一个动画停止或中断,则组合中的所有其他动画也会停止。

合成动画值

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

Animated.add()
Animated.subtract()
Animated.divide()
Animated.modulo()
Animated.multiply()

插值

interpolate()函数允许输入范围映射到不同的输出范围。默认情况下,它将推断超出给定范围的曲线,但也可以限制输出值。它默认使用线性插值,但也支持缓动功能。

interpolate()

你可以在动画文档中了解到更多。

处理手势和其他事件

手势,如平移或滚动,以及其他事件可以使用Animated.event()直接映射到动画值。这是通过结构化映射语法完成的,以便可以从复杂的事件对象中提取值。第一层参数是一个数组,你可以在其中指定多个参数映射,这种映射可以是嵌套的对象。

Animated.event()

例如,在使用水平滚动手势时,为了将event.nativeEvent.contentOffset.x映射到scrollX(Animated.Value),您需要执行以下操作:

 onScroll={Animated.event(
   // scrollX = e.nativeEvent.contentOffset.x
   [{ nativeEvent: {
        contentOffset: {
          x: scrollX
        }
      }
    }]
 )}

实际案例演示:


import React, {useRef} from 'react';
import {Animated, PanResponder, StyleSheet, View, Dimensions} from 'react-native';

const {width: SCREEN_WIDTH, height: SCREEN_HEIGHT} = Dimensions.get('window');

const DraggableView = () => {
  const pan = useRef(new Animated.ValueXY()).current;
  const scale = useRef(new Animated.Value(1)).current;
  const backgroundColor = useRef(new Animated.Value(0)).current;

  const panResponder = PanResponder.create({
    onStartShouldSetPanResponder: () => true,
    onPanResponderGrant: () => {
      Animated.parallel([
        Animated.spring(scale, {
          toValue: 1.2,
          useNativeDriver: true,
        }),
        Animated.timing(backgroundColor, {
          toValue: 1,
          duration: 200,
          useNativeDriver: false,
        }),
      ]).start();
    },
    onPanResponderMove: Animated.event([
      null,
      {
        dx: pan.x,
        dy: pan.y,
      },
    ], {useNativeDriver: false}),
    onPanResponderRelease: (e, gestureState) => {
      // Boundary check
      const isOutOfBounds = 
        Math.abs(gestureState.dx) > SCREEN_WIDTH / 2 - 40 || 
        Math.abs(gestureState.dy) > SCREEN_HEIGHT / 2 - 40;

      if (isOutOfBounds) {
        Animated.parallel([
          Animated.spring(pan, {
            toValue: {x: 0, y: 0},
            useNativeDriver: true,
          }),
          Animated.spring(scale, {
            toValue: 1,
            useNativeDriver: true,
          }),
          Animated.timing(backgroundColor, {
            toValue: 0,
            duration: 200,
            useNativeDriver: false,
          }),
        ]).start();
      } else {
        Animated.parallel([
          Animated.spring(scale, {
            toValue: 1,
            useNativeDriver: true,
          }),
          Animated.timing(backgroundColor, {
            toValue: 0,
            duration: 200,
            useNativeDriver: false,
          }),
        ]).start();
      }
    },
  });

  const bgColorInterpolation = backgroundColor.interpolate({
    inputRange: [0, 1],
    outputRange: ['#61dafb', '#ff6b6b'],
  });

  return (
    <View style={styles.container}>
      <Animated.View
        {...panResponder.panHandlers}
        style={[
          pan.getLayout(),
          styles.box,
          {
            transform: [{scale}],
            backgroundColor: bgColorInterpolation,
            shadowColor: '#000',
            shadowOffset: {width: 0, height: 2},
            shadowOpacity: 0.3,
            shadowRadius: 4,
            elevation: 5,
          },
        ]}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#f5f5f5',
  },
  box: {
    width: 80,
    height: 80,
    borderRadius: 40,
  },
});

export default DraggableView;

这段React Native代码实现了一个具有丰富视觉反馈的可拖拽圆形视图组件。其核心原理基于React Native的Animated动画系统和PanResponder手势响应系统,通过多个动画值的协同工作来创建流畅的交互体验。

代码首先通过Dimensions API获取屏幕的宽度和高度,用于后续的边界检测。组件内部创建了三个关键的动画值引用:pan用于管理视图的位置坐标,采用ValueXY类型来同时处理x和y轴的位移;scale用于控制视图的缩放比例;backgroundColor则用于管理背景颜色的过渡状态。

在这里插入图片描述

PanResponder是整个交互逻辑的核心,它定义了四个关键的生命周期方法。当用户开始触摸视图时,onPanResponderGrant会被触发,这里使用Animated.parallel同时执行两个动画:scale通过spring动画放大到1.2倍,backgroundColor通过timing动画在200毫秒内从0过渡到1。这种并行执行确保了视觉反馈的即时性和一致性。

在用户拖拽过程中,onPanResponderMove通过Animated.event直接将手势的位移量映射到pan动画值上,实现了视图的实时跟随。这里值得注意的是useNativeDriver的配置差异:transform相关的动画可以使用原生驱动以获得更好的性能,而背景色动画由于涉及颜色插值,只能使用JavaScript驱动。

当用户释放触摸时,onPanResponderRelease执行边界检测逻辑。这个检测基于简单的数学计算,判断视图是否被拖拽到了屏幕边缘区域(距离屏幕中心超过半屏宽度或高度减去40像素)。如果超出边界,视图会通过动画返回到初始位置,同时恢复原始大小和颜色;如果在安全区域内,则只恢复大小和颜色,但保持当前位置。

颜色过渡是通过interpolate方法实现的,它将backgroundColor的数值范围[0,1]映射到具体的颜色值[‘#61dafb’, ‘#ff6b6b’],即从天蓝色过渡到红色。这种插值机制使得动画值可以平滑地驱动视觉属性的变化。

最终的渲染使用了Animated.View组件,它将pan.getLayout()返回的变换样式、缩放变换、背景色插值以及阴影效果组合在一起。阴影的配置既包含了iOS平台的shadow属性,也包含了Android平台的elevation属性,确保了跨平台的一致性。


打包

接下来通过打包命令npn run harmony将reactNative的代码打包成为bundle,这样可以进行在开源鸿蒙OpenHarmony中进行使用。

在这里插入图片描述
最后运行效果图如下显示:

请添加图片描述

欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐