ipad网站开发,化妆品网站优化,惠州城乡建设部网站,滁州做网站的1. TreeDPicker.tsx文件
原理就不想赘述了, 想了解的话, 网址在:
使用vue写一个picker插件,使用3d滚轮的原理_vue3中支持3d picker选择器插件-CSDN博客
import React, { useEffect, useRef, Ref, useState } from react;
import Animate from ../utils/an…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的每一行的高度--单位remconst lineHeightRem pxToRem(LINE_HEIGHT); // picker的每一行的文字行高--单位remconst radiusRem pxToRem(radius); // 半径--单位remconst { cuIdx, pickerArr, isShow, setIsShow, setSelectedValue } props; // 解构props, 得到需要使用来自父页面传入的数据const[pickerIsShow, setPickerIsShow] useState(props.isShow)useEffect(() {setPickerIsShow(isShow)
}, [isShow])// 存储手指滑动的数据const finger0 useRefIFinger({startY: 0,startTime: 0, // 开始滑动时间单位毫秒currentMove: 0,prevMove: 0,});const finger _.get(finger0, current) || {};const currentIndex useRefICurrentIndex(0);const pickerContainer useRef() as Refany;const wheel useRef() as Refany;let isInertial useRefboolean(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滚轮的内容区域样式--useRefwheel的元素样式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.prevMovefinger.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结束) / tconst 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初 atlet 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 classNamepicker-containerdiv ref{pickerContainer}{isShow}MyTransition namemyPopup transitionShow{pickerIsShow}{isShow (section classNamepop-cover onClick{close}/section)}/MyTransitionMyTransition namemyOpacity transitionShow{pickerIsShow}{isShow (sectiondiv classNamebtn-boxbutton onClick{close}取消/buttonbutton onClick{sure}确认/button/divdivclassNamecol-wrapper-fatherstyle{getWrapperFatherStyle()}div classNamecol-wrapper style{getWrapperStyle()}ul classNamewheel-list style{getListTop()} ref{wheel}{_.map(pickerArr, (item, index) {return (liclassNamewheel-itemstyle{initWheelItemDeg(index)}key{wheel-list-index}{item}/li);})}/uldiv classNamecover style{getCoverStyle()}/divdiv classNamedivider 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博客