主页 > 软件开发  > 

react写一个简单的3d滚轮picker组件

react写一个简单的3d滚轮picker组件

1. TreeDPicker.tsx文件

原理就不想赘述了, 想了解的话, 网址在:

使用vue写一个picker插件,使用3d滚轮的原理_vue3中支持3d picker选择器插件-CSDN博客

import React, { useEffect, useRef, Ref, useState } from "react"; import Animate from "../utils/animate"; import _ from "lodash"; import "./Picker.scss"; import * as ReactDOM from "react-dom"; import MyTransition from "./MyTransition"; interface IProps { selected?: number | string; cuIdx: number; pickerArr: string[]|number[]; isShow: boolean; setIsShow: (arg1: boolean) => void; setSelectedValue: (arg1: number|string) => void; } interface IFinger { startY: number; startTime: number; currentMove: number; prevMove: number; } type ICurrentIndex = number; const a = -0.003; // 加速度 let radius = 2000; // 半径--console.log(Math.PI*2*radius/LINE_HEIGHT)=>估算最多可能多少条,有较大误差,半径2000应该够用了,不够就4000 const LINE_HEIGHT = 36; // 文字行高 const FRESH_TIME = 1000 / 60; // 动画帧刷新的频率大概是1000 / 60 // 反正切=>得到弧度再转换为度数,这个度数是单行文字所占有的。 let singleDeg = 2 * ((Math.atan(LINE_HEIGHT / 2 / radius) * 180) / Math.PI); const REM_UNIT = 37.5; // px转化为rem需要的除数 const SCROLL_CONTENT_HEIGHT = 300; // 有效滑动内容高度 const TreeDPicker = (props: IProps) => { const pxToRem = (pxNumber) => { return Number(pxNumber / REM_UNIT) + "rem"; }; const heightRem = pxToRem(LINE_HEIGHT); // picker的每一行的高度--单位rem const lineHeightRem = pxToRem(LINE_HEIGHT); // picker的每一行的文字行高--单位rem const radiusRem = pxToRem(radius); // 半径--单位rem const { cuIdx, pickerArr, isShow, setIsShow, setSelectedValue } = props; // 解构props, 得到需要使用来自父页面传入的数据 const[pickerIsShow, setPickerIsShow] = useState(props.isShow) useEffect(() => { setPickerIsShow(isShow) }, [isShow]) // 存储手指滑动的数据 const finger0 = useRef<IFinger>({ startY: 0, startTime: 0, // 开始滑动时间(单位:毫秒) currentMove: 0, prevMove: 0, }); const finger = _.get(finger0, "current") || {}; const currentIndex = useRef<ICurrentIndex>(0); const pickerContainer = useRef() as Ref<any>; const wheel = useRef() as Ref<any>; let isInertial = useRef<boolean>(false); // 是否正在惯性滑动 // col-wrapper的父元素, 限制滚动区域的高度,内容正常显示(col-wrapper多余的部分截掉不显示) const getWrapperFatherStyle = () => { return { height: pxToRem(SCROLL_CONTENT_HEIGHT), }; }; // class为col-wrapper的style样式: 滚轮的外包装理想样式--展示半径的内容可见,另外的半径隐藏 const getWrapperStyle = () => ({ height: pxToRem(2 * radius), // 居中: 1/2直径 - 1/2父页面高度 transform: `translateY(-${pxToRem(radius - SCROLL_CONTENT_HEIGHT / 2)})`, }); // 当父元素(class为col-wrapper), 定位是relative, 高度是直径: 2 * radius, 子页面想要居中, top: (1/2直径)-(1/2*一行文字高度) const circleTop = pxToRem(radius - LINE_HEIGHT / 2); // 很重要!!! // col-wrapper的子元素 => 3d滚轮的内容区域样式--useRef=wheel的元素样式 const getListTop = () => ({ top: circleTop, height: pxToRem(LINE_HEIGHT), }); // col-wrapper的子元素 => 参照一般居中的做法,[50%*父页面的高度(整个圆的最大高度是直径)]-居中内容块(文本的行高)的一半高度 const getCoverStyle = () => { return { backgroundSize: `100% ${circleTop}`, }; }; // col-wrapper的子元素 => 应该也是参照居中的做法(注意减去两条边框线) const getDividerStyle = () => ({ top: `calc(${circleTop} - ${pxToRem(0)})`, height: pxToRem(LINE_HEIGHT), }); const animate = new Animate(); function initWheelItemDeg(index) { // 初始化时转到父页面传递的下标所对应的选中的值 // 滑到父页面传的当前选中的下标cuIdx处 const num = -1 * index + Number(cuIdx); const transform = getInitWheelItemTransform(num); // 当前的下标 return { transform: transform, height: heightRem, lineHeight: lineHeightRem, }; } /** * 1、translate3d 在浏览器中,y轴正方向垂直向下,x轴正方向水平向右,z轴正方向指向外面。 z轴越大离我们越近,即看到的物体越大。z轴说物体到屏幕的距离。 * */ function getInitWheelItemTransform(indexNum) { // 初始化时转到父页面传递的下标所对应的选中的值 // 滑动的角度: 该行文字下标 * 一行文字对应的角度 const rotate3dValue = getMoveWheelItemTransform(indexNum * LINE_HEIGHT); return `${rotate3dValue} translateZ(calc(${radiusRem} / 1))`; } function getMoveWheelItemTransform(move) { // 初始化时转到父页面传递的下标所对应的选中的值 const indexNum = Math.round(move / LINE_HEIGHT); // 滑动的角度: 该行文字下标 * 一行文字对应的角度 const wheelItemDeg = indexNum * singleDeg; return `rotateX(${wheelItemDeg}deg)`; } function listenerTouchStart(ev) { ev.stopPropagation(); isInertial.current = false; // 初始状态没有惯性滚动 finger.startY = ev.targetTouches[0].pageY; // 获取手指开始点击的位置 finger.prevMove = finger.currentMove; // 保存手指上一次的滑动距离 finger.startTime = Date.now(); // 保存手指开始滑动的时间 } function listenerTouchMove(ev) { ev.stopPropagation(); // startY: 开始滑动的touch目标的pageY: ev.targetTouches[0].pageY减去 const nowStartY = ev.targetTouches[0].pageY; // 获取当前手指的位置 // finger.startY - nowStart为此次滑动的距离, 再加上上一次滑动的距离finger.prevMove, 路程总长: (finger.startY - nowStartY) + finger.prevMove finger.currentMove = finger.startY - nowStartY + finger.prevMove; let wheelDom = _.get(wheel, "current") || document.getElementsByClassName("wheel-list")[0]; if (wheelDom) { wheelDom.style.transform = getMoveWheelItemTransform(finger.currentMove); } } function listenerTouchEnd(ev) { ev.stopPropagation(); const _endY = ev.changedTouches[0].pageY; // 获取结束时手指的位置 const _entTime = Date.now(); // 获取结束时间 const v = (finger.startY - _endY) / (_entTime - finger.startTime); // 滚动完毕求移动速度 v = (s初始-s结束) / t const absV = Math.abs(v); isInertial.current = true; // 最好惯性滚动,才不会死板 animate.start(() => inertia({ start: absV, position: Math.round(absV / v), target: 0 })); // Math.round(absV / v)=>+/-1 } /**用户结束滑动,应该慢慢放慢,最终停止。从而需要 a(加速度) * @param start 开始速度(注意是正数) @param position 速度方向,值: 正负1--向上是+1,向下是-1 @param target 结束速度 */ function inertia({ start, position, target }) { if (start <= target || !isInertial.current) { animate.stop(); finger.prevMove = finger.currentMove; getSelectValue(finger.currentMove); // 得到选中的当前下标 return; } // 这段时间走的位移 S = (+/-)vt + 1/2at^2 + s1; const move = position * start * FRESH_TIME + 0.5 * a * Math.pow(FRESH_TIME, 2) + finger.currentMove; const newStart = position * start + a * FRESH_TIME; // 根据求末速度公式: v末 = (+/-)v初 + at let actualMove = move; // 最后的滚动距离 let wheelDom = _.get(wheel, "current") || document.getElementsByClassName("wheel-list")[0]; // 已经到达目标 // 当滑到第一个或者最后一个picker数据的时候, 不要滑出边界 // 因为在开始的时候加了父页面传递的下标,这里需要减去才能够正常使用 const minIdx = 0 - cuIdx; const maxIdx = pickerArr.length - 1 - cuIdx; if (Math.abs(newStart) >= Math.abs(target)) { if (Math.round(move / LINE_HEIGHT) < minIdx) { // 让滚动在文字区域内,超出区域的滚回到边缘的第一个文本处 actualMove = minIdx * LINE_HEIGHT; } else if (Math.round(move / LINE_HEIGHT) >= maxIdx) { // 让滚动在文字区域内,超出区域的滚回到边缘的最后一个文本处 actualMove = maxIdx * LINE_HEIGHT; } if (wheelDom) wheelDom.style.transition = "transform 700ms cubic-bezier(0.19, 1, 0.22, 1)"; } // finger.currentMove赋值是为了点击确认的时候可以使用=>获取选中的值 finger.currentMove = actualMove; if (wheelDom) wheelDom.style.transform = getMoveWheelItemTransform(actualMove); animate.stop(); // animate.start(() => inertia.bind({ start: newStart, position, target })); } // 滚动时及时获取最新的当前下标--因为在初始化的时候减去了,所以要加上cuIdx,否则下标会不准确 function getSelectValue(move) { const idx = Math.round(move / LINE_HEIGHT) + Number(cuIdx); currentIndex.current = idx; return idx; } function sure() { // 点击确认按钮 getSelectValue(finger.currentMove); setSelectedValue(pickerArr[currentIndex.current]); setTimeout(() => { close(); }, 0); } function close() { setTimeout(() => { setPickerIsShow(false); // 延迟关闭, 因为MyTransition需要这段事件差执行动画效果 setTimeout(() => { setIsShow(false) }, 500); }, 0); } // 点击取消按钮 useEffect(() => { const dom = _.get(pickerContainer, "current") || document.getElementsByClassName("picker-container")[0]; try { dom.addEventListener("touchstart", listenerTouchStart, false); dom.addEventListener("touchmove", listenerTouchMove, false); dom.addEventListener("touchend", listenerTouchEnd, false); } catch (error) { console.log(error); } return () => { const dom = _.get(pickerContainer, "current") || document.getElementsByClassName("picker-container")[0]; dom.removeEventListener("touchstart", listenerTouchStart, false); dom.removeEventListener("touchmove", listenerTouchMove, false); dom.removeEventListener("touchend", listenerTouchEnd, false); }; }, [_.get(pickerContainer, "current")]); return ReactDOM.createPortal( <div className="picker-container"> <div ref={pickerContainer}> {isShow+''} <MyTransition name="myPopup" transitionShow={pickerIsShow}> {isShow && ( <section className="pop-cover" onClick={close}></section> )} </MyTransition> <MyTransition name="myOpacity" transitionShow={pickerIsShow}> {isShow && ( <section> <div className="btn-box"> <button onClick={close}>取消</button> <button onClick={sure}>确认</button> </div> <div className="col-wrapper-father" style={getWrapperFatherStyle()} > <div className="col-wrapper" style={getWrapperStyle()}> <ul className="wheel-list" style={getListTop()} ref={wheel}> {_.map(pickerArr, (item, index) => { return ( <li className="wheel-item" style={initWheelItemDeg(index)} key={"wheel-list-"+index} > {item+''} </li> ); })} </ul> <div className="cover" style={getCoverStyle()}></div> <div className="divider" style={getDividerStyle()}></div> </div> </div> </section> )} </MyTransition> </div> </div>, document.body ); }; export default TreeDPicker;

2. scss文件:

@import "./common.scss"; .picker-container { position: fixed; bottom: 0; left: 0; right: 0; // transition动画部分 .myOpacity-enter, .myOpacity-leave-to { opacity: 0; // 因为picker滚动区域有过transform, 这里也写transform的话会导致本不该滚动的地方滚动了 } .myOpacity-enter-active, .myOpacity-leave-active { opacity: 1; transition: all 0.5s ease; } .myPopup-enter, .myPopup-leave-to { transform: translateY(100px); } .myPopup-enter-active, .myPopup-leave-active { transition: all 0.5s ease; } // 透明遮罩 .pop-cover { position: fixed; top: 0; left: 0; right: 0; height: 100vh; background: rgba(0, 0, 0, 0.5); z-index: -1; } // 确认 取消按钮box .btn-box { height: pxToRem(40px); background: rgb(112, 167, 99); display: flex; justify-content: space-between; font-size: pxToRem(16px); & button { background-color: rgba(0, 0, 0, 0); border: none; color: #fff; } } .col-wrapper-father { overflow: hidden; } //overflow: hidden=>截掉多余的部分,显示弹窗内容部分 ul, li { list-style: none; padding: 0; margin: 0; } // 为了方便掌握重点样式,简单的就直接一行展示,其他的换行展示,方便理解 .col-wrapper { position: relative; border: 1px solid #ccc; text-align: center; background: #fff; &>.wheel-list { position: absolute; width: 100%; transform-style: preserve-3d; transform: rotate3d(1, 0, 0, 0deg); .wheel-item { backface-visibility: hidden; position: absolute; left: 0; top: 0; width: 100%; border: 1px solid #eee; font-size: pxToRem(16px); } } &>.cover { position: absolute; left: 0; top: 0; right: 0; bottom: 0; background: linear-gradient(0deg, rgba(white, 0.6), rgba(white, 0.6)), linear-gradient(0deg, rgba(white, 0.6), rgba(white, 0.6)); background-position: top, bottom; background-repeat: no-repeat; } &>.divider { position: absolute; width: 100%; left: 0; border-top: 1px solid #ccc; border-bottom: 1px solid #ccc; } } }

3. transition组件(之前写了一篇文章有提到):

react简单写一个transition动画组件然后在modal组件中应用-CSDN博客

标签:

react写一个简单的3d滚轮picker组件由讯客互联软件开发栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“react写一个简单的3d滚轮picker组件