开启生长之旅!这是我参与「日新方案 12 月更文应战」的第七天,点击检查活动概况

相关技术

react , hooks , ts

功用描述

依据用户的接触,对卡片进行一个圆形的旋转翻滚。

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

运用react hooks 封装的圆形翻滚组件

运用

引进 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>

组件代码讲解

完成逻辑图

运用react hooks 封装的圆形翻滚组件

组件初始化

需求先获取 可翻滚区域高度,卡片高度,圆的半径,卡片间的视点和可翻滚区域占的度数的信息。

这儿需求运用到 高中知识 ,通过 三角函数视点弧度 的转化。

  • 弧度 = 弧长 / 半径 = 视点 * / 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)

运用react hooks 封装的圆形翻滚组件
useEffect(() => {
/** 获取card的信息 */
const getCardH = async () => {
  const cWrapH = document.querySelector(`.comScrollCircleWrap`)?.clientHeight ?? 0
  info.current.circleWrapHeight = cWrapH
  const cInfo = document.querySelector(`.comScrollCircle-cardWrap`)
  info.current.cardH = cInfo?.clientHeight ?? 0
  const cW = cInfo?.clientWidth ?? 0
  info.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 * index
  const 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 = true
  touchInfo.current.startY = e.clientY
  touchInfo.current.startDeg = rotateDeg
  touchInfo.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.startY
  const 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() - time
  let deg = rotateDeg
  // 接触的始末间隔大于卡片高度的一半,而且接触时刻小于300ms,则接触间隔和时刻旋转更多
  if((Math.abs(_y) > info.current.cardH / 2) && (_time < 300)) {
    // 添加视点变化 
    const v = _time / 300
    const changeDeg = info.current.scrollViewDeg * (_y / info.current.circleWrapHeight) / v
    deg = 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>
  )
}

完好代码

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

运用react hooks 封装的圆形翻滚组件