使用react hooks 封装的圆形滚动组件

news/2025/1/20 2:41:18/

相关技术

react , hooks , ts

功能描述

根据用户的触摸,对卡片进行一个圆形的旋转滚动。

码上掘金 引入组件好像不支持ts类型会报错,所以功能函数就丢到一个文件里面了

使用

引入 ScrollRotate 组件,在需要使用的数据列表外包裹一层,传入 list 和该区域的高度 height ;内部的卡片需要使用 ScrollRotate.Item 包裹,并传入每个卡片的索引值。

<ScrollRotate list={list} height={`calc(100vh - 100px)`}>{list?.map((item,i) => (<ScrollRotate.Item key={item._id} index={i}><View className={`card`}><View className="cardTitle">{item.title}</View></View></ScrollRotate.Item>))}
</ScrollRotate> 

组件代码讲解

实现逻辑图

组件初始化

需要先获取 可滚动区域高度,卡片高度,圆的半径,卡片间的角度和可滚动区域占的度数的信息。

这里需要运用到 高中知识 ,通过 三角函数角度弧度 的转化。

  • 弧度 = 弧长 / 半径 = 角度 * π / 180; 弧长 = (角度 / 360) * 周长
  • 求sin 例: const sin30 = Math.sin(30 * Math.PI / 180) // 0.5 sin30度
  • 求角度 例: const deg_30 = 180 * Math.asin(1 / 2) / Math.PI // 30度

例:比如这里要计算两个卡片间的角度

代入求角度的公式就是: a的度数 = 180 * Math.atan((w / 2) / (r - h / 2)) / Math.PI

这里和实际组件中的代码的宽和高写反了(w和h)

useEffect(() => { /** 获取card的信息 */ const getCardH = async () => {const cWrapH = document.querySelector(`.comScrollCircleWrap`)?.clientHeight ?? 0info.current.circleWrapHeight = cWrapHconst cInfo = document.querySelector(`.comScrollCircle-cardWrap`)info.current.cardH = cInfo?.clientHeight ?? 0const cW = cInfo?.clientWidth ?? 0info.current.circleR = Math.round(systemInfo.screenHeight)// 卡片间的角度cardDeg.current = 2 * 180 * Math.atan(((info.current.cardH ?? 0) / 2) / (info.current.circleR - cW / 2)) / Math.PI + cardAddDeg// 屏幕高度对应的圆的角度info.current.scrollViewDeg = getLineAngle(info.current.circleWrapHeight, info.current.circleR)console.log(`可滚动区域高度: ${info.current.circleWrapHeight};\n卡片高度: ${info.current.cardH};\n圆的半径: ${info.current.circleR};\n卡片间的角度: ${cardDeg.current}度;\n可滚动区域占的度数: ${info.current.scrollViewDeg}度;`);setRotateDeg(cardDeg.current * initCartNum) } if(list?.length) {setTimeout(() => {getCardH()}, 10); } }, [list, cardAddDeg]) ```

给每个卡片设置初始样式

由于我这里每个卡片是一开始直接定位到一个圆上的,所以组件初始化后,需要计算出每个卡片的 top leftrotate,这里其实也是一些三角函数的处理了。

const cardStyle = useMemo(() => {const deg = 90 + cardDeg * indexconst top = circleR * (1 - Math.cos(deg * Math.PI / 180))const left = circleR * (1 - Math.sin(deg * Math.PI / 180))const rotate = 90 - deg// console.log(top, left, rotate);return {top: `${top}px`, left: `${left}px`, transform: `translate(-50%, -50%) rotate(${rotate}deg)`}
}, [circleR, cardDeg]) 

由于这里两个组件需要共享数据,item需要使用list的数据,所以这里使用 useContext

组件间共享数据的封装说明(精简)

首先需要创建一个上下文对象。

const ScrollCircleCtx = React.createContext({circleR: 0,cardDeg: 0
}) 

然后最外层组件 使用上下文对象的 Provider 包裹。

const ScrollCircle = () => {return (<ScrollCircleCtx.Provider value={{circleR: info.current.circleR,cardDeg: cardDeg.current}}>{children}</ScrollCircleCtx.Provider>)
} 

item 组件使用 useContext() 获取上下文数据。

const ScrollRotateItem = () => {const {circleR, cardDeg} = useContext(ScrollCircleCtx)return (<div>{children}</div>)
} 

触摸旋转滚动

监听鼠标的事件 onMouseDown , onMouseMove , onMouseUponMouseLeave 事件。如果是移动端的话改成 onTouchStart , onTouchMoveonTouchEnd 即可。

<div className="comScrollcircle"onMouseDown={onTouchStart}onMouseMove={onTouchMove}onMouseUp={onTouchEnd}onMouseLeave={onTouchEnd}style={{width: `${info.current.circleR * 2}px`,height: `${info.current.circleR * 2}px`,transform: `translate(calc(-50% + ${systemInfo.screenWidth / 2}px), -50%) rotate(${rotateDeg}deg)`}}
>{children}
</div> 
onTouchStart

记录鼠标点击初始化的信息

const onTouchStart = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {touchInfo.current.isTouch = truetouchInfo.current.startY = e.clientYtouchInfo.current.startDeg = rotateDegtouchInfo.current.time = Date.now()
} 
onTouchMove

根据触摸移动的距离,计算出应该旋转的角度,我这里的计算公式为:

初始位置的角度 + (触摸距离 / 可触摸的整个区域高度) * 触摸区域高度所占的角度

我这里的惯性滚动效果是采用 transition 里面的 ease-out 来简单实现的。

const onTouchMove = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {if(!touchInfo.current.isTouch) {return}const y = e.clientY - touchInfo.current.startYconst deg = Math.round(touchInfo.current.startDeg - info.current.scrollViewDeg * (y / info.current.circleWrapHeight))setRotateDeg(deg)
} 
onTouchEnd

当触摸结束时,该次触摸如果小于300ms,且触摸距离大于卡片高度一半的话,则表示用户的该次触摸是快速滚动,则需要旋转更多的角度,这里的计算和上面 move 的同理。

const onTouchEnd = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {const {startY, startDeg, time } = touchInfo.current// 移动的距离const _y = e.clientY - startY// 触摸的时间const _time = Date.now() - timelet deg = rotateDeg// 触摸的始末距离大于卡片高度的一半,并且触摸时间小于300ms,则触摸距离和时间旋转更多if((Math.abs(_y) > info.current.cardH / 2) && (_time < 300)) {// 增加角度变化 const v = _time / 300const changeDeg = info.current.scrollViewDeg * (_y / info.current.circleWrapHeight) / vdeg = Math.round(startDeg - changeDeg)}// 处理转动的角度为:卡片的角度的倍数 (_y > 0 表示向上滑动)const _deg = cardDeg.current * Math[_y > 0 ? 'floor' : 'ceil'](deg / cardDeg.current)setRotateDeg(_deg)touchInfo.current.isTouch = false
} 

完整使用样例

import { useState, useEffect } from 'react';
import './index.scss';
import ScrollRotate from './scrollRotate';export default () => {const [list, setList] = useState<any[]>([])useEffect(() => {init()}, [])/** 初始化获取数据 */const init = async () => {setTimeout(() => {const newList = new Array(23).fill('Tops').map((a,i) => ({_id: 'id' + i, title: a + i}))setList(newList)}, 300);}return (<div className='page-categories-test-1'><div className="top" style={{height: '50px', background: '#458cfe'}}></div><ScrollRotate list={list} height={`calc(100vh - 100px)`}>{list?.map((item,i) => (<ScrollRotate.Item key={item._id} index={i}><div className={`card`}><div className="cardTitle">{item.title}</div></div></ScrollRotate.Item>))}</ScrollRotate><div className="navWrap" onClick={()=>{}}><div className='navItem'>T</div><div className='navItem'>C</div><div className='navItem'>B</div></div><div className="bottom" style={{height: '50px', background: '#458cfe'}}></div></div>)
} 

完整代码

组件代码比较长,就保存到 码上掘金 了。

最后

最近还整理一份JavaScript与ES的笔记,一共25个重要的知识点,对每个知识点都进行了讲解和分析。能帮你快速掌握JavaScript与ES的相关知识,提升工作效率。



有需要的小伙伴,可以点击下方卡片领取,无偿分享


http://www.ppmy.cn/news/26521.html

相关文章

水文监测场景的数据通信规约解析和落地实践

[小 迪 导 读]&#xff1a;江苏云上需要通过云平台接入水文设备来实现水文数据的采集、存储、显示、控制、报警及传输等综合功能。企业介绍江苏云上智联物联科技有限公司是专业从事物联网相关产品与解决方案服务的高科技公司&#xff0c;总部位于美丽的江苏无锡。公司遵循“智联…

多If函数封装的策略

在工作中我们经常遇到有多个if的判读函数&#xff0c;这是一件很正常的事情&#xff0c;如下&#xff1a; let order function (orderType, isPay, count) {if (orderType 1) {// 充值 500if (isPay true) {// 充值成功console.log(中奖100元)} else {if (count > 0) {c…

ISCC认证的必备条件建议收藏

【ISCC认证的必备条件建议收藏】作为国际公认的可持续认证体系之一&#xff0c;ISCC认证因其体系健全和适用范围广等特点得到了广泛应用。随着ISCC体系的不断完善及体系用户和认证机构的反馈&#xff0c;ISCC的体系文件内容和审核要求也在持续更新。ISCC体系有两个核心的要求&a…

慕了没?3年经验,3轮技术面+1轮HR面,拿下字节30k*16薪offer

前段时间有个朋友出去面试&#xff0c;这次他面试目标比较清晰&#xff0c;面的都是业务量大、业务比较核心的部门。前前后后去了不少公司&#xff0c;几家大厂里&#xff0c;他说给他印象最深的是字节3轮技术面1轮HR面&#xff0c;他最终拿到了30k*16薪的offer。第一轮主要考察…

测试2:编写测试用例的方法

目录1.编写测试用例的方法1.1 测试用例的描述&#xff1a;1.2 测试用例设计方法&#xff08;1&#xff09;基于需求&#xff1a;依据需求来写测试点&#xff08;2&#xff09;等价类--分类&#xff08;3&#xff09;边界值&#xff1a;--黑盒测试方法&#xff08;4&#xff09;…

家政服务小程序实战开发教程015-填充用户信息

我们上一篇讲解了立即预约功能&#xff0c;存在的问题是&#xff0c;每次都需要用户填写联系信息。在我们前述篇章中已经介绍了用户注册的功能&#xff0c;在立即预约的时候我们需要把已经填写的用户信息提取出来&#xff0c;显示到表单对应的字段中。本篇我们就讲解一下如何提…

Robot Framework自动化测试---元素定位

不要误认为Robot framework 只是个web UI测试工具&#xff0c;更正确的理解Robot framework是个测试框架&#xff0c;之所以可以拿来做web UI层的自动化是国为我们加入了selenium2的API。比如笔者所处工作中&#xff0c;更多的是拿Robot framework来做数据库的接口测试&#xf…

每日英语学习(11)大英复习单词和翻译

2023.2.20 单词 1.contemplate 思考、沉思 2.spark 激起 3.venture 冒险 4.stunning 极好的 5.dictate 影响 6.diplomatic 外交的 7.vicious 恶性的 8.premier 首要的 9.endeavor 努力 10.bypass 绕过 11.handicaps 不利因素 12.vulnerable 脆弱的 13.temperament 气质、性格…