React Native使用Hooks实现按钮边框双线条无限循环流动动画
React Native Hooks实现按钮边框循环流动线条动画
核心实现思路
- 采用两个独立的
Animated.Value作为动画进度驱动,初始相位错开半个周期,保证两条线条不会重合 - 用
Animated.loop包裹线性变化的进度动画,实现无限循环播放 - 通过
interpolate插值方法,将0~1的进度值映射为线条的位置、宽高样式,匹配边框四个边的移动路径 - 所有动画值用
useRef持有,避免不必要的组件重渲染
完整可运行代码
import React, { useEffect, useRef } from 'react'; import { View, Animated, Easing, TouchableOpacity, Text, StyleSheet } from 'react-native'; // 常量配置,可自行调整 const BUTTON_WIDTH = 220; const BUTTON_HEIGHT = 60; const LINE_LENGTH = 80; // 流动线条的长度 const LINE_COLOR = '#2196F3'; const ANIMATION_DURATION = 4000; // 单次循环的时长 const App = () => { // 两个独立的动画进度值,第二个初始进度为0.5,错开相位 const progress1 = useRef(new Animated.Value(0)).current; const progress2 = useRef(new Animated.Value(0.5)).current; // 启动无限循环动画的方法 const startLoopAnimation = (progress) => { return Animated.loop( Animated.timing(progress, { toValue: 1, duration: ANIMATION_DURATION, easing: Easing.linear, // 线性运动保证速度均匀 useNativeDriver: false, }) ).start(); }; useEffect(() => { // 组件挂载后启动两个线条的动画 startLoopAnimation(progress1); startLoopAnimation(progress2); }, []); // 生成线条样式的公共方法 const getLineStyle = (progress) => { // 插值映射X轴偏移 const translateX = progress.interpolate({ inputRange: [0, 0.25, 0.25, 0.5, 0.5, 0.75, 0.75, 1], outputRange: [ 0, BUTTON_WIDTH - LINE_LENGTH, // 顶部:从左往右移动 BUTTON_WIDTH - 2, BUTTON_WIDTH - 2, // 右侧:X固定 BUTTON_WIDTH - LINE_LENGTH, 0, // 底部:从右往左移动 0, 0 // 左侧:X固定 ] }); // 插值映射Y轴偏移 const translateY = progress.interpolate({ inputRange: [0, 0.25, 0.25, 0.5, 0.5, 0.75, 0.75, 1], outputRange: [ 0, 0, // 顶部:Y固定 0, BUTTON_HEIGHT - LINE_LENGTH, // 右侧:从上往下移动 BUTTON_HEIGHT - 2, BUTTON_HEIGHT - 2, // 底部:Y固定 BUTTON_HEIGHT - LINE_LENGTH, 0 // 左侧:从下往上移动 ] }); // 插值映射线条宽度 const lineWidth = progress.interpolate({ inputRange: [0, 0.25, 0.25, 0.5, 0.5, 0.75, 0.75, 1], outputRange: [LINE_LENGTH, LINE_LENGTH, 2, 2, LINE_LENGTH, LINE_LENGTH, 2, 2] }); // 插值映射线条高度 const lineHeight = progress.interpolate({ inputRange: [0, 0.25, 0.25, 0.5, 0.5, 0.75, 0.75, 1], outputRange: [2, 2, LINE_LENGTH, LINE_LENGTH, 2, 2, LINE_LENGTH, LINE_LENGTH] }); return { position: 'absolute', backgroundColor: LINE_COLOR, width: lineWidth, height: lineHeight, transform: [{ translateX }, { translateY }] }; }; return ( <View style={styles.container}> {/* 按钮容器,相对定位 */} <View style={{ width: BUTTON_WIDTH, height: BUTTON_HEIGHT, position: 'relative' }}> {/* 两条流动线条 */} <Animated.View style={getLineStyle(progress1)} /> <Animated.View style={getLineStyle(progress2)} /> {/* 实际的按钮内容 */} <TouchableOpacity style={styles.button}> <Text style={styles.buttonText}>点击按钮</Text> </TouchableOpacity> </View> </View> ); }; const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#f5f5f5' }, button: { width: '100%', height: '100%', justifyContent: 'center', alignItems: 'center', backgroundColor: 'white', borderRadius: 4, borderWidth: 1, borderColor: '#eee' }, buttonText: { fontSize: 16, color: '#333' } }); export default App;
关键说明
- 可以修改常量配置里的参数,调整线条长度、颜色、动画速度,也可以给两条线条设置不同的颜色区分
- 如果要优化性能开启原生驱动,可以将宽高的动态切换改成
transform: [{scaleX}]和transform: [{scaleY}]实现,不需要修改布局属性即可开启useNativeDriver: true - 原有按钮的点击逻辑、样式都可以正常加在内部的
TouchableOpacity上,不会和动画冲突
内容的提问来源于stack exchange,提问作者Jamal Butt
相关产品推荐
相关产品推荐

