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博客文章来源地址https://www.toymoban.com/news/detail-720707.html

到了这里,关于react写一个简单的3d滚轮picker组件的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 面试题-React(六):React组件和生命周期

    一、React组件 React组件简介: React组件是构建用户界面的基本单元。它们将界面拆分成独立、可重用的部分,使得代码更加模块化、可维护性更高。React组件可以是函数组件或类组件,它们接收输入的数据(称为props)并返回表示用户界面的React元素。 创建React组件: 在React中

    2024年02月11日
    浏览(34)
  • 面试题-React(三):什么是JSX?它与常规JavaScript有什么不同?

    在React的世界中,JSX是一项引人注目的技术,它允许开发者在JavaScript中嵌套类似HTML的标签,用于描述UI组件的结构。本篇博客将通过丰富的代码示例,深入探索JSX语法,解析其在React中的用法和优势。 一、JSX基础语法 在React项目中,你会经常看到类似HTML的代码块,这就是JS

    2024年02月12日
    浏览(36)
  • 手写一个 React 图片预览组件

    原文链接: 手写一个 React 图片预览组件 前几天打算给博客添加一个图片预览的效果,可在网上找了半天也没找到合适的库,于是自己干脆自己手写了个。 最终实现效果如下: 当鼠标点击图片时生成一个半透明遮罩,并添加一个与点击图片位置大小都相同的图片,之后通过

    2024年02月08日
    浏览(39)
  • React超级简单易懂全面的有关问题回答(面试)

    目录 React事件机制: 2、React的事件和普通的HTML有什么不同: - 事件命名的规则不同,原生事件采用全小写,react事件采用小驼峰 3、React组件中怎么做事件代理?他的原理是什么? 4、React高阶组件、Render props、Hook有什么区别,为什么要不断迭代? ✨HOC(高阶组件): ✨Render

    2024年02月02日
    浏览(41)
  • 面试中问:React中函数组件和class组件的区别,hooks模拟生命周期

    React 的函数组件和类组件在很多方面都相似,但它们也有一些关键的差异。以下是这两种组件之间的主要区别: 定义方式 : 函数组件 : 是简单的 JavaScript 函数,接受 props 为参数,并返回 React 元素。 类组件 : 是 ES6 的类,继承的时候要用到 extends React.Component,至少包含一个名

    2024年02月11日
    浏览(46)
  • 搭建一个简单的react工程

    首先,需要确保安装了 Node.js 和 npm。可以在命令行中输入 node -v 和 npm -v 命令来检查版本。 创建一个新的项目目录,并进入该目录。 在命令行中运行以下命令来初始化 npm 项目: 这将创建一个名为 package.json 的文件,其中包含了项目的依赖和配置信息。 安装 React 和 ReactDOM

    2023年04月22日
    浏览(40)
  • react使用hook封装一个tab组件

    2024年02月09日
    浏览(47)
  • 前端(十五)——开源一个用react封装的图片预览组件

    👵博主:小猫娃来啦 👵文章核心:开源一个react封装的图片预览组件 Gitee:点此跳转下载 CSDN:点此跳转下载 装依赖 运行 打开 创建一个React函数组件并命名为 ImageGallery 。 在组件内部,使用useState钩子来定义状态变量,并初始化为合适的初始值。 selectedImageUrl 来追踪当前选

    2024年02月10日
    浏览(56)
  • JavaScript+canvas实现一个旋转的3D球动画效果

    1. 获取Canvas元素和设置初始参数 这部分代码主要是获取Canvas元素,并根据设备的DPI进行缩放。然后,定义了一些全局变量,包括Canvas的宽度、高度、球体旋转的角度和存储所有点的数组。 2. 定义一些常量 这部分代码定义了一些常量,如点的数量、点的半径、球半径等。 3.定

    2024年01月18日
    浏览(57)
  • react Hook+antd封装一个优雅的弹窗组件

    前言 在之前学vue2的时候封装过一个全局的弹窗组件,可以全局任意地方通过this调用,这次大创项目是用react技术栈,看了一下项目需求,突然发现弹窗还是比较多的,主要分为基础的弹窗以及form表单式的弹窗,如果只是无脑的去写代码,那些项目也没啥必要了。正好react和

    2024年02月13日
    浏览(41)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包