likes
comments
collection
share

react native高级应用之手势动画

作者站长头像
站长
· 阅读数 50

你将掌握的知识点:

  1. Animated动画库使用方式
  2. 布局与定位能力
  3. 如何借用ScrollView的滚动能力
  4. PanResponder手势系统
  5. Touchable系列组件
  6. 点击事件 & 手势冲突如何解决

效果展示:

react native高级应用之手势动画

react native高级应用之手势动画

完整代码: react-native-radius-view

功能分析

  1. 需要支持点击切换的能力
  2. 需要支持滑动滚动的能力
  3. 当滚动时需要依次滚动到上一个组件的位置。
  4. 可以跨多个组件滚动 当一侧没有内容时 不可以在滚动。
  5. 整体的布局可以分为中心点 然后右侧区域 左侧区域 交替填充数据。
  6. 数据的内容并不是无限的范围【1,9】

整体排列如图: react native高级应用之手势动画

需要解决的问题

  1. 如何实现点击和手势滑动逻辑
  2. 如何确定将要滚动到那个位置 例如用户向左滑动一次 0->2的位置 2->4 4->6 6->8 8已经在最边缘了保持不变。右侧同理 依次滚动到前面的空位。
  3. 左右滑动的边界在哪里
  4. 滚动的动画如何实现

问题1:如何实现点击和手势滑动

通过Touchable & scrollview组合

当基于滚动行为做一些逻辑或者动画时 我们首先想到的肯定是基于react native官方提供的已有的组件。在这个场景下是scrollview。那么他是否可以实现我们的需求呢?

scrollview滚动需要满足内容大于视口的条件。这一点我们的场景是不满足的。但是我们可以通过填充空白元素 撑起scrollview的内容 使用组件的滚动能力。然后通过绝对定位 将元素定位到页面的第一屏。可以保证同时响应滚动和点击的能力。

如图所示: react native高级应用之手势动画

问题:scrollview的内容即使设置了绝对定位 依然会跟随页面滚动。后发现文档里有stickyHeaderIndices属性可以设置不随滚动移动 然鹅... 不支持与属性horizontal={true}一起使用。。。

通过Touchable & PanResponder一起使用

PanResponder

在 React Native 中,PanResponder 是用来处理用户手势的系统。它提供了一系列的 API,通过这些 API,你可以定义如何响应用户的拖拽、滑动等手势操作。 核心代码如下:

    const panResponder = PanResponder.create({
        // 其他东西想要成为响应者。这个视图应该释放响应者吗
        onPanResponderTerminationRequest: () => true,
        // 设置成为响应者
        onStartShouldSetPanResponder: () => false,
        onMoveShouldSetPanResponder(evt, gestureState) {
            // 取消的情况gestureState数据会被清空 这里保存状态
            if (cycleList.length > 1 && Math.abs(gestureState.dx) > 15) {
                panDx.current = { ...gestureState };
                return true;
            }
            return false;
        },
        onStartShouldSetPanResponderCapture: () => false,
        // 抬起
        onPanResponderRelease(evt, gestureState) {
            onTouchEnd(gestureState);
        },
        // 取消
        onPanResponderTerminate(evt, gestureState) {
            onTouchEnd(panDx.current || gestureState);
        },
    });

gestureState 对象

每个 PanResponder 回调中都会接受到一个 gestureState 对象,它有以下关键属性:

  • dx 和 dy: 从触摸操作开始到当前事件触发点的累积距离。
  • moveX 和 moveY: 最近一次移动事件发生时屏幕上的触点位置。
  • x0 和 y0: 用户在屏幕上初始触点的位置。
  • vx 和 vy: 在 onPanResponderMove 时的手指移动速度。
  • numberActiveTouches: 屏幕上触点的数量。

注意

如果手势被取消 当onPanResponderTerminate被执行时 gestureState对象会被清空 因此如果想基于gestureState在onPanResponderTerminate中执行逻辑时 需要手动保存下这个对象。

点击与手势冲突

通过设置开始不响应并且在滚动时拦截 可以解决点击和手势事件冲突的问题。

onStartShouldSetPanResponder: () => false,
onMoveShouldSetPanResponder(evt, gestureState) {
            if (cycleList.length > 1 && Math.abs(gestureState.dx) > 15) {
                panDx.current = { ...gestureState };
                return true;
            }
            return false;
        },

问题2:如何确定将要滚动到那个位置

滚动动画的本质是从屏幕的(x,y)移动到另一个(x,y)。那么我们如何获取到对应的坐标呢?

我们可以通过onLayout获取每个视图的坐标点,并基于此计算出视图的中心坐标。然后保存在ref中。

   const layoutListRef = useRef<LayoutItem[]>([]);
    const onItemLayout = (e, index) => {
        layoutListRef.current[index] = {
            centerX: e.nativeEvent.layout.x + e.nativeEvent.layout.width / 2,
            centerY: e.nativeEvent.layout.y + e.nativeEvent.layout.height / 2,
        };
    };
 <Animated.View
                        style={stylesList}
                        onLayout={e => onItemLayout(e, index)}
                        key={index}
                    >
                        <TouchableWithoutFeedback onPress={() => onClick(index)}>
                            {renderItem(data, ani, index)}
                        </TouchableWithoutFeedback>
                    </Animated.View>

问题3:左右滑动的边界在哪里

因为数组长度为【1,9】我们可以通过这个长度计算出【start, end】. 例如长度为4的情况start=3 end=6 center=4(中心位置永远不变)。 react native高级应用之手势动画

【start, end】形成一个窗口 当左右滑动时 窗口左右移动 当左右窗口边界位于中心点时即到达滚动边缘 不可以再继续向对应方向滚动。 react native高级应用之手势动画

对于边界也要做相应的处理 当已经处于边界外围并且移动的下一步依然在边界外面 我们不需要移动当前视图。 react native高级应用之手势动画

问题4:滚动的动画如何实现

先看下最基本的Animated动画是如何实现的

import React, { useEffect, useRef } from 'react';
import { Animated, Text, View } from 'react-native';
const FadeInView = (props) => {
  const fadeAnim = useRef(new Animated.Value(0)).current; // 1:初始化动画属性

  useEffect(() => {
    // 3:通过api开始动画属性的变化
    Animated.timing(
      fadeAnim,
      {
        toValue: 1,  // 目标透明度
        duration: 5000,  // 动画时长
        useNativeDriver: true,  // 启用原生动画驱动
      }
    ).start();
  }, [fadeAnim])

  return (
    <Animated.View                 // 使用 Animated.View
      style={{
        ...props.style,
        opacity: fadeAnim,         //  动画属性和动画组件的样式关联
      }}
    >
      {props.children}
    </Animated.View>
  );
}
// 使用
<FadeInView style={{width: 200, height: 50, backgroundColor: 'powderblue'}}>
  <Text style={{fontSize: 28, textAlign: 'center', margin: 10}}>Fading in</Text>
</FadeInView>

使用 Animated 库通常分为以下几个步骤:

  1. 初始化动画值,使用 Animated.Value 或 Animated.ValueXY 初始化动画的状态值
  2. 将动画值应用到组件的样式属性上
  3. 通过动画api配置并开始动画属性的变化。

注意

需要将Animated动画值声明为不可变.

const fadeAnim = new Animated.Value(0);

在这种情况下,每当组件重新渲染时,fadeAnim 将被重新实例化,从而导致:

  • 动画值每次渲染都重置,使得无法维持连贯的动画效果。
  • 动画相关的操作(如开始、停止)需要重新配置,因为 fadeAnim 每次都是新的实例。

这种做法不仅影响动画的正常执行,还可能引入性能问题,因为频繁的实例化和垃圾回收会消耗额外的资源。

具体实现

  1. 我们将滑动分为左右两个方向 分别实现。
  2. 当向左滑动时 首先判断当前end是否大于center点 只有大于中心点才能移动。
  3. 窗口的end位置依次向左移动 找到下一个视图对应的位置 并获取坐标。计算差值
  4. 然后开始计算动画属性变化 生成动画对象数组。
  5. 将【start end】边界做调整,修改中心位置的对象。
  6. 通过动画api并行的运行收集的动画对象。
const leftXY = (step = 1) => {
        const aniListXY = [];
        [...infoList.current].reverse().forEach((info, i) => {
            let s = 1;
            let o = 0;
            const item = cycleList[info.pos];
            let index = end - i; // 当前的元素位置
            if (index < 0) {
                return;
            }
            const nextIndex = Math.max(index - step, 0); // 将要移动到的下一个元素位置
            if (index >= LocationNums.length && nextIndex >= LocationNums.length) { // 处理超出边界的case
                return;
            }
            index = Math.min(end - i, LocationNums.length - 1);

            const x =
                layoutListRef.current[LocationNums[nextIndex]].centerX -
                layoutListRef.current[LocationNums[index]].centerX;
            const y =
                layoutListRef.current[LocationNums[nextIndex]].centerY -
                layoutListRef.current[LocationNums[index]].centerY;
            if (LocationNums[nextIndex] === 0) {
                s = 1.5;
                o = 1;
            }

            info.offsetX = info.offsetX + x;
            info.offsetY = info.offsetY + y;
            generateAniList(aniListXY, item, { x: info.offsetX, y: info.offsetY, s, o });
        });

        changeCenterIndex(true, step);
        setStart(start - step);
        setEnd(end - step);
        return aniListXY;
    };

参考

  1. docs.swmansion.com/react-nativ…
  2. reactnative.dev/
  3. reactnative.dev/docs/0.73/h…
转载自:https://juejin.cn/post/7371019286372450319
评论
请登录