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

【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.dtcms.com/a/346554.html

相关文章:

  • Krea Video:Krea AI推出的AI视频生成工具
  • JAVA国际版东郊到家同城按摩服务美容美发私教到店服务系统源码支持Android+IOS+H5
  • 大白话聊一聊,数据结构的基石:数组和链表
  • 【Kubernetes知识点】Pod调度和ConfigMaps
  • Maven快速入门
  • 【python】get_dummies()用法
  • 云原生、容器及数据中心网络相关名词记录
  • 量子计算驱动的Python医疗诊断编程前沿展望(上)
  • 【JVM内存结构系列】一、入门:先搞懂整体框架,再学细节——避免从一开始就混淆概念
  • leetcode 69 x的平方根
  • 【UnityAS】Unity Android Studio 联合开发快速入门:环境配置、AAR 集成与双向调用教程
  • 一个适用于 Word(Mac/Win 通用) 的 VBA 宏:把所有“上角标格式的 0‑9”以及 “Unicode 上角标数字 ⁰‑⁹” 批量删除。
  • 前端安全之XSS和CSRF
  • Mac相册重复照片终结指南:技术流清理方案
  • Bartender 5 Mac 多功能菜单栏管理
  • Android Studio下载gradle文件很慢的捷径之路
  • MySQL深分页的处理方案
  • Java项目:基于SpringBoot和Vue的图书个性化推荐系统(源码+数据库+文档)
  • 小米集团总裁卢伟冰:小米汽车不会参与任何价格战,下半年有望开始盈利
  • WSL Ubuntu数据迁移
  • 把 AI 塞进「电梯按钮」——基于毫米波手势识别的零接触控制面板
  • 网站速度慢?安全防护弱?EdgeOne免费套餐一次性解决两大痛点
  • C语言文件操作精讲:从格式化读写到随机访问
  • 控制建模matlab练习16:线性状态反馈控制器-⑤轨迹追踪
  • 后台管理系统-15-vue3之登录页的实现
  • 谷歌浏览器重定向url,谷歌浏览器浏览网页修改url到本地
  • 批量归一化:不将参数上传到中心服务器,那服务器怎么进行聚合?
  • 基于JSqlParser的SQL语句分析与处理
  • ASCOMP PDF Conversa:高效精准的PDF转换工具
  • 【机器学习深度学习】多模态学习