当前位置: 首页 > ops >正文

【React Native】自定义轮盘(大转盘)组件Wheel

一、功能概述

本组件是一个自定义轮盘(大转盘)组件

  • 旋转动画:通过 Animated 实现平滑旋转效果,支持自定义旋转圈数、单圈时长和缓动曲线。
  • 自定义渲染:支持自定义奖品项(renderItem)和旋转按钮(renderRunButton)。
  • 精准控制:提供 scrollToIndex 方法,可编程滚动到指定奖品位置。
  • 状态回调:支持旋转开始(onRotationStart)和结束(onRotationEnd)的回调事件。
  • 视觉定制:支持自定义奖品底盘颜色(dataBgColor),适配不同设计需求。

二、组件 Props 说明

Prop 名称类型说明默认值
styleViewStyle转盘容器的外层样式{}
dataT[]奖品数据数组(必传)[]
rotationCountnumber旋转圈数(如 3 表示旋转 3 圈后停止)3
rotationOneTimenumber单圈旋转时长(单位:ms)2000
renderItem(item: T, index: number) => React.ReactNode自定义奖品项的渲染函数必传
renderRunButton() => React.ReactNode自定义旋转按钮的渲染函数(可选)undefined
keyExtractor(item: T, index: number) => string奖品项的唯一键提取函数(必传)必传
clickRunButton() => void点击旋转按钮的回调(触发旋转逻辑)必传
onRotationStart() => void旋转开始时的回调undefined
onRotationEnd(item: T, index: number) => void旋转结束时的回调(返回最终奖品和索引)undefined
dataBgColorColorValue[]扇区背景色数组(循环使用)['#FFD700', '#FFA500', '#008C00']

三、使用示例

import React, { useRef } from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
import Wheel from './Wheel';const App = () => {const wheelRef = useRef(null);const prizes = ['iPhone 15', 'iPad Pro', 'MacBook Air', 'AirPods Max', 'Apple Watch'];// 点击按钮触发旋转(滚动到随机位置)const handleSpin = () => {const randomIndex = Math.floor(Math.random() * prizes.length);wheelRef.current?.scrollToIndex(randomIndex);};// 旋转结束回调const handleRotationEnd = (item: string, index: number) => {console.log(`抽中:${item}(索引 ${index}`);};return (<View style={styles.container}><Wheelref={wheelRef}data={prizes}rotationCount={3}rotationOneTime={2000}renderItem={(item, index) => (<View style={styles.item}><Text style={styles.itemText}>{item}</Text></View>)}renderRunButton={() => (<Button title="开始抽奖" onPress={handleSpin} />)}keyExtractor={(item, index) => index.toString()}onRotationStart={() => console.log('旋转开始...')}onRotationEnd={handleRotationEnd}dataBgColor={['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEEAD']}/></View>);
};const styles = StyleSheet.create({container: {flex: 1,justifyContent: 'center',alignItems: 'center',backgroundColor: '#F7FFF7',},item: {width: 80,height: 40,justifyContent: 'center',alignItems: 'center',backgroundColor: 'rgba(255, 255, 255, 0.9)',borderRadius: 20,},itemText: {color: '#333',fontSize: 14,fontWeight: 'bold',},
});export default App;

四、源码

import React, {type Ref, useImperativeHandle, useRef, useState} from 'react';
import {Animated,ColorValue,Easing,StyleSheet,TouchableOpacity,View,ViewStyle,
} from 'react-native';interface WheelProps<T> {style?: ViewStyle;// 奖品数据data: T[];// 旋转圈数rotationCount?: number;// 一圈旋转时间rotationOneTime?: number;// 渲染奖品renderItem: (item: T, index: number) => React.ReactNode;// 渲染奖品底盘颜色dataBgColor?: ColorValue[];// 渲染旋转按钮,默认没有renderRunButton?: () => React.ReactNode;// 键keyExtractor: (item: T, index: number) => string;// 点击旋转按钮回调clickRunButton: () => void;// 旋转开始回调onRotationStart?: () => void;// 旋转结束回调onRotationEnd?: (item: T, index: number) => void;
}export interface WheelHandles {scrollToIndex: (targetIndex: number) => void; // 滚动到指定下标的方法
}const Wheel = <T,>(props: WheelProps<T>,ref: Ref<WheelHandles>,
): JSX.Element => {const {style,data,rotationCount = 3,rotationOneTime = 2000,dataBgColor = ['#FFD700', '#FFA500', '#008C00'],clickRunButton,renderItem,renderRunButton,keyExtractor,onRotationStart,onRotationEnd,} = props;const [wheelWidth, setWheelWidth] = useState(0);const [wheelHeight, setWheelHeight] = useState(0);const [itemWidth, setItemWidth] = useState(0);const [itemHeight, setItemHeight] = useState(0);const rotateAnim = useRef(new Animated.Value(0)).current;const isRunning = useRef(false);// 暴露方法给父组件useImperativeHandle(ref, () => ({scrollToIndex,}));// 计算每个奖品扇区的角度const getSectorAngle = () => 360 / data.length;const renderPrizeItems = () => {const sectorAngle = getSectorAngle();return data.map((prize, index) => {const rotate = index * sectorAngle - 90;return renderItems(prize, index, rotate);});};const renderItems = (data: T,index: number,rotate: number,isMeasure = false,) => {return (<Viewkey={keyExtractor(data, index)}style={[styles.item,{transform: [{translateX: wheelWidth / 2 - itemWidth / 2,},{translateY: wheelHeight / 2 - itemHeight / 2,},{rotate: `${rotate}deg`},{translateX: wheelHeight / 2 - itemHeight / 2,},{rotate: `${90}deg`},],},]}onLayout={event => {if (isMeasure) {setItemWidth(event.nativeEvent.layout.width);setItemHeight(event.nativeEvent.layout.height);}}}>{renderItem(data, index)}</View>);};const renderItemBg = () => {let sectorAngle = getSectorAngle();return data.map((prize, index) => {return (<Viewkey={keyExtractor?.(prize, index)}style={{position: 'absolute',width: wheelWidth,height: wheelHeight,borderRadius: 1000,overflow: 'hidden',}}>{sectorAngle >= 90? sectorAngle === 360? renderBgItem360(): renderBgItemGt90(index, sectorAngle): renderBgItemLt90(index, sectorAngle)}</View>);});};const renderBgItemLt90 = (index: number, rotate: number) => {const rotateOut = 180 + index * rotate + rotate / 2;return (<Viewstyle={{width: wheelWidth,height: wheelHeight,overflow: 'hidden',transform: [{rotate: `${rotateOut}deg`},{translateX: wheelWidth / 2},],}}><Viewstyle={{width: wheelWidth,height: wheelHeight,position: 'absolute',left: 0,top: 0,backgroundColor: dataBgColor[index % dataBgColor.length],transform: [{translateX: -wheelWidth / 2},{rotate: `${90 - rotate}deg`},{translateX: wheelWidth / 2},{translateY: wheelWidth / 2},],}}/></View>);};const renderBgItemGt90 = (index: number, rotate: number) => {const rotateOut = 180 + index * rotate - rotate / 2;return (<Viewstyle={{width: wheelWidth,height: wheelHeight,overflow: 'hidden',transform: [{rotate: `${rotateOut}deg`}],}}><Viewstyle={{width: wheelWidth,height: wheelHeight,position: 'absolute',left: 0,top: 0,backgroundColor: dataBgColor[index % dataBgColor.length],transform: [{rotate: `${90}deg`},{translateX: wheelWidth / 2},{translateY: wheelWidth / 2},],}}/><Viewstyle={{width: wheelWidth,height: wheelHeight,position: 'absolute',left: 0,top: 0,backgroundColor: dataBgColor[index % dataBgColor.length],transform: [{rotate: `${rotate}deg`},{translateX: wheelWidth / 2},{translateY: wheelWidth / 2},],}}/></View>);};const renderBgItem360 = () => {return (<Viewstyle={{width: wheelWidth,height: wheelHeight,overflow: 'hidden',backgroundColor: dataBgColor[0],}}/>);};// 在 Wheel 组件内部添加const scrollToIndex = (targetIndex: number) => {if (isRunning.current) {return;}if (data.length === 0 || targetIndex < 0 || targetIndex >= data.length) {return;}const sectorAngle = getSectorAngle(); // 扇区角度(360°/数据长度)// 计算目标项的原始旋转角度(未滚动时的角度)const targetItemOriginalRotate = targetIndex * sectorAngle;// 转盘需要旋转的角度(反向抵消原始角度,使目标项到顶部)let targetRotation = -targetItemOriginalRotate - 360 * rotationCount;isRunning.current = true;onRotationStart?.();// 执行动画Animated.timing(rotateAnim, {toValue: targetRotation,duration: rotationOneTime * rotationCount, // 动画时长easing: Easing.bezier(0.42, 0, 0.58, 1), // 动画曲线useNativeDriver: true, // 使用原生驱动提升性能}).start(() => {isRunning.current = false;rotateAnim.setValue(-targetItemOriginalRotate);onRotationEnd?.(data?.[targetIndex], targetIndex);});};return (<Viewstyle={[styles.wheelContainer, style]}onLayout={event => {setWheelWidth(event.nativeEvent.layout.width);setWheelHeight(event.nativeEvent.layout.height);}}><View style={{opacity: 0, position: 'absolute', left: 0, top: 0}}>{renderItems(data?.[0], 0, 0, true)}</View><View><Animated.Viewstyle={[styles.wheel,{width: wheelWidth,height: wheelHeight,transform: [{rotate: rotateAnim.interpolate({inputRange: [0, 360],outputRange: ['0deg', '360deg'],}),},],},]}><View style={{position: 'absolute', left: 0, top: 0}}>{renderItemBg()}</View><>{renderPrizeItems()}</></Animated.View></View>{renderRunButton && (<TouchableOpacityactiveOpacity={1}onPress={clickRunButton}style={{position: 'absolute',}}>{renderRunButton()}</TouchableOpacity>)}</View>);
};const styles = StyleSheet.create({wheelContainer: {position: 'relative',alignItems: 'center',justifyContent: 'center', // 水平居中(主轴)},wheel: {overflow: 'hidden',borderRadius: 150,position: 'relative',},item: {position: 'absolute',alignItems: 'center',padding: 0,margin: 0,},
});export default React.forwardRef<WheelHandles, WheelProps<any>>(Wheel);
http://www.xdnf.cn/news/18506.html

相关文章:

  • 【KO】前端面试题四
  • 今日科技热点 | 量子计算突破、AI芯片与5G加速行业变革
  • PLECS 中使用 C-Script 来模拟 NTC 热敏电阻(如 NTC3950B)
  • 【K8s】整体认识K8s之Docker篇
  • 百度面试题:赛马问题
  • 嵌入式LINUX-------------数据库
  • 循环中的阻塞风险与异步线程解法
  • 搜索体验优化:ABP vNext 的查询改写(Query Rewrite)与同义词治理
  • 前端安全之XSS和CSRF
  • 鸿蒙异步处理从入门到实战:Promise、async/await、并发池、超时重试全套攻略
  • 互联网大厂Java面试实战:核心技术栈与场景化提问解析(含Spring Boot、微服务、测试框架等)
  • 量子计算驱动的Python医疗诊断编程前沿展望(中)
  • RabbitMQ面试精讲 Day 28:Docker与Kubernetes部署实践
  • Git checkout 与 Git reset 核心区别解析(分支与版本关联逻辑)
  • 如何在 Spring Boot 中安全读取账号密码等
  • 技术演进中的开发沉思-75 Linux系列:中断和与windows中断的区分
  • 【python与生活】如何自动总结视频并输出一段总结视频?
  • 基于 FastAPI 和 OpenFeature 使用 Feature Flag 控制业务功能
  • Js逆向 拼夕夕anti_content
  • 【读代码】SQLBot:开源自然语言转SQL智能助手原理与实践
  • 怎样避免游戏检测到云手机?
  • 深入浅出:图解 glibc —— 系统与应用的沉默基石
  • 【知识】Elsevier论文接收后的后续流程
  • 可预约体验 | 一句话生成全栈应用,网易CodeWave智能开发能力全新升级!
  • TDengine IDMP 应用场景:工业锅炉监控
  • 资深产品经理个人能力提升方向:如何系统化进阶与考证规划
  • Maven快速入门
  • Day26 树的层序遍历 哈希表 排序算法 内核链表
  • 数据库服务语句应用
  • 【机器学习深度学习】多模态典型任务与应用全景