react native高级应用之手势动画
你将掌握的知识点:
Animated动画库使用方式
布局与定位能力
如何借用ScrollView的滚动能力
PanResponder手势系统
Touchable系列组件
点击事件 & 手势冲突如何解决
效果展示:
完整代码: react-native-radius-view
功能分析
- 需要支持点击切换的能力
- 需要支持滑动滚动的能力
- 当滚动时需要依次滚动到上一个组件的位置。
- 可以跨多个组件滚动 当一侧没有内容时 不可以在滚动。
- 整体的布局可以分为中心点 然后右侧区域 左侧区域 交替填充数据。
- 数据的内容并不是无限的范围【1,9】
整体排列如图:
需要解决的问题
- 如何实现点击和手势滑动逻辑
- 如何确定将要滚动到那个位置 例如用户向左滑动一次 0->2的位置 2->4 4->6 6->8 8已经在最边缘了保持不变。右侧同理 依次滚动到前面的空位。
- 左右滑动的边界在哪里
- 滚动的动画如何实现
问题1:如何实现点击和手势滑动
通过Touchable & scrollview组合
当基于滚动行为做一些逻辑或者动画时 我们首先想到的肯定是基于react native官方提供的已有的组件。在这个场景下是scrollview。那么他是否可以实现我们的需求呢?
scrollview滚动需要满足内容大于视口的条件。这一点我们的场景是不满足的。但是我们可以通过填充空白元素 撑起scrollview的内容 使用组件的滚动能力。然后通过绝对定位 将元素定位到页面的第一屏。可以保证同时响应滚动和点击的能力。
如图所示:
问题: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(中心位置永远不变)。
【start, end】形成一个窗口 当左右滑动时 窗口左右移动
当左右窗口边界位于中心点时即到达滚动边缘 不可以再继续向对应方向滚动。
对于边界也要做相应的处理 当已经处于边界外围并且移动的下一步依然在边界外面 我们不需要移动当前视图。
问题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 库通常分为以下几个步骤:
- 初始化动画值,使用 Animated.Value 或 Animated.ValueXY 初始化动画的状态值
- 将动画值应用到组件的样式属性上
- 通过动画api配置并开始动画属性的变化。
注意
需要将Animated动画值声明为不可变.
const fadeAnim = new Animated.Value(0);
在这种情况下,每当组件重新渲染时,fadeAnim 将被重新实例化,从而导致:
- 动画值每次渲染都重置,使得无法维持连贯的动画效果。
- 动画相关的操作(如开始、停止)需要重新配置,因为 fadeAnim 每次都是新的实例。
这种做法不仅影响动画的正常执行,还可能引入性能问题,因为频繁的实例化和垃圾回收会消耗额外的资源。
具体实现
- 我们将滑动分为左右两个方向 分别实现。
- 当向左滑动时 首先判断当前end是否大于center点 只有大于中心点才能移动。
- 窗口的end位置依次向左移动 找到下一个视图对应的位置 并获取坐标。计算差值
- 然后开始计算动画属性变化 生成动画对象数组。
- 将【start end】边界做调整,修改中心位置的对象。
- 通过动画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;
};
参考
转载自:https://juejin.cn/post/7371019286372450319