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的每一行的高度--单位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 = 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.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 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><divclassName="col-wrapper-father"style={getWrapperFatherStyle()}><div className="col-wrapper" style={getWrapperStyle()}><ul className="wheel-list" style={getListTop()} ref={wheel}>{_.map(pickerArr, (item, index) => {return (<liclassName="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组件
1. TreeDPicker.tsx文件 原理就不想赘述了, 想了解的话, 网址在: 使用vue写一个picker插件,使用3d滚轮的原理_vue3中支持3d picker选择器插件-CSDN博客 import React, { useEffect, useRef, Ref, useState } from "react"; import Animate from "../utils/an…...
Compose竖向列表LazyColumn
基础列表一 LazyColumn组件中用items加载数据,rememberLazyListState()结合rememberCoroutineScope()实现返回顶部。 /*** 基础列表一*/ Composable fun Items() {Box(modifier Modifier.fillMaxSize()) {val context LocalContext.currentval dataList arrayLi…...
6.自定义相机控制器
愿你出走半生,归来仍是少年! Cesium For Unity自带的Dynamic Camera,拥有优秀的动态展示效果,但是其对于场景的交互方式用起来不是很舒服。 通过模仿Cesium JS 的交互方式,实现在Unity中的交互: 通过鼠标左键拖拽实现场景平移通过…...
一文带你GO语言入门
什么是go语言? Go语言(又称Golang)是Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。Go语言的主要特点包括:- 简洁和简单 - 语法简单明快,易于学习和使用 特点 高效 编译速度快,执行效率高 并发支持 原生支持并发,利用goroutine实现高效的并发…...
前后端小项目链接
1.vue的创建 vue的项目创建 1.1 vue create vue_name 1.2 Babel Router(路由) CSS Pre-processors 路由可通过:npm i vue-router3.5.2 -S 下载 1.3less 1.4 In dedicated config files 1.5 启动命令:npm run serve 端口号在vue.config。js中配置 devS…...
编辑器功能:用一个快捷键来【锁定】或【解开】Inspector面板
一、需求 我有一个脚本,上面暴露了许多参数,我要在场景中拖物体给它进行配置。 如果不锁定Inspector面板的话,每次点击物体后,Inspector的内容就是刚点击的物体的内容,而不是挂载脚本的参数面板。 二、 解决 &…...
Vue 网络处理 - axios 异步请求的使用,请求响应拦截器(最佳实践)
目录 一、axiox 1.1、axios 简介 1.2、axios 基本使用 1.2.1、下载核心 js 文件. 1.2.2、发送 GET 异步请求 1.2.3、发送 POST 异步请求 1.2.4、发送 GET、POST 请求最佳实践 1.3、请求响应拦截器 1.3.1、拦截器解释 1.3.2、请求拦截器的使用 1.3.3、响应拦截器的使…...
关于W5500网卡使用过程的部分问题记录
某个项目中用到了W5500这种自带网络协议栈的网卡芯片,由于该项目开发时间很紧,就临时网上买了一些模块拼凑到了一套系统,经过验证果真这种拼积木的方法只能用在学生实验开发中,真不能拿来做工程应用,硬件太不稳定很容易…...
Unity DOTS World Entity ArchType Component EntityManager System概述
最近DOTS终于发布了正式的版本, 我们来分享以下DOTS里面地几个关键概念,方便大家上手学习掌握Unity DOTS开发。 Unity DOTS 中所有的Entities 都是被放到World世界中。每个Entity在它所在的World里面有唯一不同的ID号来区分。DOTS项目中可以同时有多个World。每个W…...
最详细STM32,cubeMX 点亮 led
这篇文章将详细介绍 如何在 stm32103 板子上点亮一个LED. 文章目录 前言一、开发环境搭建。二、LED 原理图解读三、什么是 GPIO四、cubeMX 配置工程五、解读 cubeMX 生成的代码六、延时函数七、控制引脚状态函数点亮 LED 八、GPIO 的工作模式九、为什么使用推挽输出驱动 LED总结…...
论文阅读:Image-to-Lidar Self-Supervised Distillation for Autonomous Driving Data
目录 摘要 Motivation 整体架构流程 技术细节 雷达和图像数据的同步 小结 论文地址: [2203.16258] Image-to-Lidar Self-Supervised Distillation for Autonomous Driving Data (arxiv.org) 论文代码:GitHub - valeoai/SLidR: Official PyTorch implementati…...
前端版本控制工具,常见的Git 和SVN
目录 前言GitGit简介Git的优势Git常用指令常见的Git服务 SVN (Subversion)SVN简介SVN的优势SVN常用指令SVN与Git的区别 👍 点赞,你的认可是我创作的动力! ⭐️ 收藏,你的青睐是我努力的方向! ✏️ 评论,你…...
C++ —— Tinyxml2在Vs2017下相关使用2(较文1更复杂,附源码)
相关链接 C —— Tinyxml2在Vs2017下相关使用1(附源码) tinyxml2简介 TinyXML2是一个简单,小巧,高效,CXML解析器,可以很容易地集成到其他程序中。TinyXML-2解析一个XML文档,并从中构建一个 可以…...
阿里内推强推的并发编程学习笔记,原理+实战+面试题,面面俱到!
并发编程 谈到并发编程,可能很多人都有过经验,甚至比我了解的更多。 那么并发与并行的区别又是什么? 并发编程是编程中的核心问题,实践中,当人们希望利用计算机处理一些现实世界问题,以及希望同时处理多…...
域名注册查询流程
域名注册查询怎么查域名是否被注册?域名注册查询如何查域名的过期时间和注册商?域名注册查询用什么工具?下面是关于域名注册查询流程介绍。 1、域名注册查询可以用什么工具? 这里可以使用聚查工具,聚查包括:whois 查询、建站历史查询、反链查询、P…...
【Linux学习笔记】代码编辑工具vim
1. vim工具基本模式的转换2. vim命令模式下的各种编辑命令2.1. 光标行定位2.2. 光标自由定位2.3. 复制粘贴2.4. 删除2.5. 文本的大小写替换2.6. 文本的替换2.7. 文本的前删后删2.8. 撤销操作 3. vim底行模式下的命令3.1. 设置行号与取消设置行号3.2. 分屏操作3.3. 在不退出vim的…...
Android Boring SSL
前期设置 SSLContext.getInstance(“TLS”)SSLContext.init()SSLContext.getSocketFactory()SSLSocketFactory.createSocket()NativeSsl.newInstance()BioWrapper 的创建ConscryptEngineSocket.startHandshake() TLS协商 state_start_connect(TLS)state_enter_early_data(TLS)s…...
中国人民大学与加拿大女王大学金融硕士项目:开启你的金融精英之路
在全球化的今天,金融行业的发展日新月异,对金融人才的需求也日益增长。为了满足这一需求,中国人民大学与加拿大女王大学联合推出了金融硕士项目,旨在培养具有国际视野、专业素养和创新能力的金融精英。 这一开创性的项目将两大世…...
HashSet编程小案例,控制生日和姓名。重写HashCode
Java编程: 定义员工Employee类,该类包含:private成员属性name,sal,birthday(MyDate类型), 其中birthday为MyDate类型(属性包括:year,month,day), 要求&…...
虚幻阴影整理
虚拟阴影贴图(VSM)是一种全新的阴影贴图方法,可以提供稳定的高分辨率阴影。通过与虚幻引擎5的Nanite虚拟几何体、Lumen全局光照和反射以及世界分区功能结合使用,它能够实现电影级的品质效果,为大型开放场景提供光照。 …...
TDengine 快速体验(Docker 镜像方式)
简介 TDengine 可以通过安装包、Docker 镜像 及云服务快速体验 TDengine 的功能,本节首先介绍如何通过 Docker 快速体验 TDengine,然后介绍如何在 Docker 环境下体验 TDengine 的写入和查询功能。如果你不熟悉 Docker,请使用 安装包的方式快…...
.Net框架,除了EF还有很多很多......
文章目录 1. 引言2. Dapper2.1 概述与设计原理2.2 核心功能与代码示例基本查询多映射查询存储过程调用 2.3 性能优化原理2.4 适用场景 3. NHibernate3.1 概述与架构设计3.2 映射配置示例Fluent映射XML映射 3.3 查询示例HQL查询Criteria APILINQ提供程序 3.4 高级特性3.5 适用场…...
CMake基础:构建流程详解
目录 1.CMake构建过程的基本流程 2.CMake构建的具体步骤 2.1.创建构建目录 2.2.使用 CMake 生成构建文件 2.3.编译和构建 2.4.清理构建文件 2.5.重新配置和构建 3.跨平台构建示例 4.工具链与交叉编译 5.CMake构建后的项目结构解析 5.1.CMake构建后的目录结构 5.2.构…...
UDP(Echoserver)
网络命令 Ping 命令 检测网络是否连通 使用方法: ping -c 次数 网址ping -c 3 www.baidu.comnetstat 命令 netstat 是一个用来查看网络状态的重要工具. 语法:netstat [选项] 功能:查看网络状态 常用选项: n 拒绝显示别名&#…...
高频面试之3Zookeeper
高频面试之3Zookeeper 文章目录 高频面试之3Zookeeper3.1 常用命令3.2 选举机制3.3 Zookeeper符合法则中哪两个?3.4 Zookeeper脑裂3.5 Zookeeper用来干嘛了 3.1 常用命令 ls、get、create、delete、deleteall3.2 选举机制 半数机制(过半机制࿰…...
实现弹窗随键盘上移居中
实现弹窗随键盘上移的核心思路 在Android中,可以通过监听键盘的显示和隐藏事件,动态调整弹窗的位置。关键点在于获取键盘高度,并计算剩余屏幕空间以重新定位弹窗。 // 在Activity或Fragment中设置键盘监听 val rootView findViewById<V…...
【JavaWeb】Docker项目部署
引言 之前学习了Linux操作系统的常见命令,在Linux上安装软件,以及如何在Linux上部署一个单体项目,大多数同学都会有相同的感受,那就是麻烦。 核心体现在三点: 命令太多了,记不住 软件安装包名字复杂&…...
Spring AI与Spring Modulith核心技术解析
Spring AI核心架构解析 Spring AI(https://spring.io/projects/spring-ai)作为Spring生态中的AI集成框架,其核心设计理念是通过模块化架构降低AI应用的开发复杂度。与Python生态中的LangChain/LlamaIndex等工具类似,但特别为多语…...
重启Eureka集群中的节点,对已经注册的服务有什么影响
先看答案,如果正确地操作,重启Eureka集群中的节点,对已经注册的服务影响非常小,甚至可以做到无感知。 但如果操作不当,可能会引发短暂的服务发现问题。 下面我们从Eureka的核心工作原理来详细分析这个问题。 Eureka的…...
python爬虫——气象数据爬取
一、导入库与全局配置 python 运行 import json import datetime import time import requests from sqlalchemy import create_engine import csv import pandas as pd作用: 引入数据解析、网络请求、时间处理、数据库操作等所需库。requests:发送 …...
