实现一个横向的picker

Picker 选择器显示一个或多个选项集合的可滚动列表,相比于原生 picker,实现了 iOS 与 Android 端体验的一致性。

要实现横向 picker,其实跟纵向 picker差不多,都支持滚动时停留在指定位置,并且支持滚动到边界支持反弹效果。

在这里插入图片描述

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>picker</title>
    <style>
      * {
        margin: 0;
        padding: 0;
      }
      .scroller-component {
        display: block;
        position: relative;
        height: 34px;
        overflow: hidden;
        width: 100%;
      }

      .scroller-content {
        position: absolute;
        left: 0;
        top: 0;
        z-index: 1;
        white-space: nowrap;
        line-height: 0;
        font-size: 0;
      }

      .scroller-mask {
        position: absolute;
        left: 0;
        top: 0;
        height: 100%;
        margin: 0 auto;
        width: 100%;
        z-index: 3;
        transform: translateZ(0px);
        background-image: linear-gradient(
            to right,
            rgba(255, 255, 255, 0.95),
            rgba(255, 255, 255, 0.6)
          ),
          linear-gradient(
            to left,
            rgba(255, 255, 255, 0.95),
            rgba(255, 255, 255, 0.6)
          );
        background-position: left, right;
        background-size: 102px 100%;
        background-repeat: no-repeat;
      }

      .scroller-item {
        text-align: center;
        font-size: 16px;
        height: 34px;
        width: 50px;
        line-height: 34px;
        color: #000;
        display: inline-block;
        box-sizing: border-box;
      }

      .scroller-indicator {
        box-sizing: border-box;
        width: 50px;
        height: 34px;
        position: absolute;
        transform: translate3d(-50%, 0, 0);
        left: 50%;
        top: 0;
        z-index: 3;
        border: 1px solid red;
      }

      .scroller-item {
        line-clamp: 1;
        -webkit-line-clamp: 1;
        overflow: hidden;
        text-overflow: ellipsis;
      }
    </style>
  </head>

  <body>
    <div class="scroller-component" data-role="component">
      <div class="scroller-mask" data-role="mask"></div>
      <div class="scroller-indicator" data-role="indicator"></div>
      <div class="scroller-content" data-role="content">
        <div class="scroller-item" data-value="1">1</div>
        <div class="scroller-item" data-value="2">2</div>
        <div class="scroller-item" data-value="3">3</div>
        <div class="scroller-item" data-value="4">4</div>
        <div class="scroller-item" data-value="5">5</div>
        <div class="scroller-item" data-value="6">6</div>
        <div class="scroller-item" data-value="7">7</div>
        <div class="scroller-item" data-value="8">8</div>
        <div class="scroller-item" data-value="9">9</div>
        <div class="scroller-item" data-value="10">10</div>
        <div class="scroller-item" data-value="11">11</div>
        <div class="scroller-item" data-value="12">12</div>
        <div class="scroller-item" data-value="13">13</div>
        <div class="scroller-item" data-value="14">14</div>
        <div class="scroller-item" data-value="15">15</div>
        <div class="scroller-item" data-value="16">16</div>
        <div class="scroller-item" data-value="17">17</div>
        <div class="scroller-item" data-value="18">18</div>
        <div class="scroller-item" data-value="19">19</div>
        <div class="scroller-item" data-value="20">20</div>
      </div>
    </div>
    <script>
      let running = {}; // 运行
      let counter = 1; // 计时器
      let desiredFrames = 60; // 每秒多少帧
      let millisecondsPerSecond = 1000; // 每秒的毫秒数

      const Animate = {
        // 停止动画
        stop(id) {
          var cleared = running[id] != null;
          if (cleared) {
            running[id] = null;
          }
          return cleared;
        },

        // 判断给定的动画是否还在运行
        isRunning(id) {
          return running[id] != null;
        },
        start(
          stepCallback,
          verifyCallback,
          completedCallback,
          duration,
          easingMethod,
          root
        ) {
          let start = Date.now();
          let percent = 0; // 百分比
          let id = counter++;
          let dropCounter = 0;

          let step = function () {
            let now = Date.now();

            if (!running[id] || (verifyCallback && !verifyCallback(id))) {
              running[id] = null;
              completedCallback &&
                completedCallback(
                  desiredFrames -
                    dropCounter / ((now - start) / millisecondsPerSecond),
                  id,
                  false
                );
              return;
            }

            if (duration) {
              percent = (now - start) / duration;
              if (percent > 1) {
                percent = 1;
              }
            }
            let value = easingMethod ? easingMethod(percent) : percent;
            if (percent !== 1 && (!verifyCallback || verifyCallback(id))) {
              stepCallback(value);
              window.requestAnimationFrame(step);
            }
          };

          running[id] = true;
          window.requestAnimationFrame(step);
          return id;
        },
      };
    </script>
    <script>
      let component = document.querySelector("[data-role=component]"); // 插件容器
      let content = component.querySelector("[data-role=content]"); // 内容容器
      let indicator = component.querySelector("[data-role=indicator]"); // 正确位置实线
      let __startTouchTop = 0;
      let __scrollTop = 0;
      
      let __isAnimating = false; // 是否开启动画

      let __lastTouchMove = 0; // 最后滚动时间记录
      let __positions = []; // 记录位置和时间

      let __deceleratingMove = 0; // 减速运动速度
      let __isDecelerating = false; // 是否开启减速状态

      let __itemHeight = parseFloat(window.getComputedStyle(indicator).width);
      let __maxScrollTop = __itemHeight / 2; // 滚动最大值
      let __minScrollTop = -(content.offsetWidth) + __maxScrollTop; // 滚动最小值
      
      content.style.left = component.clientWidth / 2 - __itemHeight / 2 + 'px';

      // 开始快后来慢的渐变曲线
      let easeOutCubic = (pos) => {
        return Math.pow(pos - 1, 3) + 1;
      };
      // 以满足开始和结束的动画
      let easeInOutCubic = (pos) => {
        if ((pos /= 0.5) < 1) {
          return 0.5 * Math.pow(pos, 3);
        }
        return 0.5 * (Math.pow(pos - 2, 3) + 2);
      };
      let __callback = (top) => {
        const distance = top;
        content.style.transform = "translate3d(" + distance + "px, 0, 0)";
      };
      let __publish = (top, animationDuration) => {
        if (animationDuration) {
          let oldTop = __scrollTop;
          let diffTop = top - oldTop;
          let wasAnimating = __isAnimating;

          let step = function (percent) {
            __scrollTop = oldTop + diffTop * percent;
            __callback(__scrollTop);
          };
          let verify = function (id) {
            return __isAnimating === id;
          };
          let completed = function (
            renderedFramesPerSecond,
            animationId,
            wasFinished
          ) {
            if (animationId === __isAnimating) {
              __isAnimating = false;
            }
          };
          __isAnimating = Animate.start(
            step,
            verify,
            completed,
            animationDuration,
            wasAnimating ? easeOutCubic : easeInOutCubic
          );
        } else {
          __scrollTop = top;
          __callback(top);
        }
      };
      // 滚动到正确位置的方法
      let __scrollTo = (top) => {
        top = Math.round((top / __itemHeight).toFixed(5)) * __itemHeight;
        let newTop = Math.max(Math.min(__maxScrollTop, top), __minScrollTop);
        if (top !== newTop) {
          if (newTop >= __maxScrollTop) {
            top = newTop - __itemHeight / 2;
          } else {
            top = newTop + __itemHeight / 2;
          }
        }
        __publish(top, 250);
      };
      // 开始减速动画
      let __startDeceleration = () => {
        let step = () => {
          let scrollTop = __scrollTop + __deceleratingMove;
          let scrollTopFixed = Math.max(
            Math.min(__maxScrollTop, scrollTop),
            __minScrollTop
          ); // 不小于最小值,不大于最大值
          if (scrollTopFixed !== scrollTop) {
            scrollTop = scrollTopFixed;
            __deceleratingMove = 0;
          }
          if (Math.abs(__deceleratingMove) <= 1) {
            if (Math.abs(scrollTop % __itemHeight) < 1) {
              __deceleratingMove = 0;
            }
          } else {
            __deceleratingMove *= 0.95;
          }
          __publish(scrollTop);
        };
        let minVelocityToKeepDecelerating = 0.5;
        let verify = () => {
          // 保持减速运行需要多少速度
          let shouldContinue =
            Math.abs(__deceleratingMove) >= minVelocityToKeepDecelerating;
          return shouldContinue;
        };
        let completed = function (
          renderedFramesPerSecond,
          animationId,
          wasFinished
        ) {
          __isDecelerating = false;
          if (__scrollTop <= __minScrollTop || __scrollTop >= __maxScrollTop) {
            __scrollTo(__scrollTop);
            return;
          }
        };
        __isDecelerating = Animate.start(step, verify, completed);
      };
      let touchStartHandler = (e) => {
        e.preventDefault();
        const target = e.touches ? e.touches[0] : e;
        __startTouchTop = target.pageX;
      };
      let touchMoveHandler = (e) => {
        const target = e.touches ? e.touches[0] : e;
        let currentTouchTop = target.pageX;
        let moveY = currentTouchTop - __startTouchTop;
        let scrollTop = __scrollTop;
        scrollTop = scrollTop + moveY;
        if (scrollTop > __maxScrollTop || scrollTop < __minScrollTop) {
          if (scrollTop > __maxScrollTop) {
            scrollTop = __maxScrollTop;
          } else {
            scrollTop = __minScrollTop;
          }
        }
        if (__positions.length > 40) {
          __positions.splice(0, 20);
        }
        __positions.push(scrollTop, e.timeStamp);

        __publish(scrollTop);

        __startTouchTop = currentTouchTop;
        __lastTouchMove = e.timeStamp;
      };
      let touchEndHandler = (e) => {
        if (e.timeStamp - __lastTouchMove < 100) {
          // 如果抬起时间和最后移动时间小于 100 证明快速滚动过
          let positions = __positions;
          let endPos = positions.length - 1;
          let startPos = endPos;
          // 由于保存的时候位置跟时间都保存了, 所以 i -= 2
          // positions[i] > (self.__lastTouchMove - 100) 判断是从什么时候开始的快速滑动
          for (
            let i = endPos;
            i > 0 && positions[i] > __lastTouchMove - 100;
            i -= 2
          ) {
            startPos = i;
          }
          if (startPos !== endPos) {
            // 计算这两点之间的相对运动
            let timeOffset = positions[endPos] - positions[startPos]; // 快速开始时间 - 结束滚动时间
            let movedTop = __scrollTop - positions[startPos - 1]; // 最终距离 - 快速开始距离
            // 基于50ms计算每个渲染步骤的移动
            __deceleratingMove = (movedTop / timeOffset) * (1000 / 60); // 移动距离是用分钟来计算的

            let minVelocityToStartDeceleration = 4; // 开始减速的最小速度
            // 只有速度大于最小加速速度时才会出现下面的动画
            if (Math.abs(__deceleratingMove) > minVelocityToStartDeceleration) {
              __startDeceleration();
            }
          }
        }
        if (!__isDecelerating) {
          __scrollTo(__scrollTop);
        }

        __positions.length = 0;
      };

      component.addEventListener("touchstart", touchStartHandler);

      component.addEventListener("touchmove", touchMoveHandler);

      component.addEventListener("touchend", touchEndHandler);
    </script>
  </body>
</html>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/463545.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C语言中,基本数据类型介绍

C语言当中各种数据类型的大小&#xff0c;首先要了解有哪些数据类型。 一 字符型&#xff1a; 整数&#xff08;字符&#xff09;类型存储大小值范围char1 字节-128 到 127 或 0 到 255&#xff08;2的8次方&#xff09;unsigned char1 字节0 到 255&#xff08;&#xff09;s…

C/C++火柴棍等式

有n根(n<24)火柴棍&#xff0c;你可以拼出多少个形如“ABC"的等式?等式中的A、B、C是用火柴棍拼出的整数(若该数非零&#xff0c;则最高位不能是0)。用火柴棍拼数字0-9的拼法如图所示: 依次需要用到的火柴棍数目为6 2 5 5 4 5 6 3 7 6 。 如果是初学者可能会这么写。 …

shallowReactive浅层式响应对象

一、 reactive 和ref 都是深层响应式对象: 就是不管对象有多少层&#xff0c;修改此对象任一属性都会响应式处理 shallowReactive 和shallowRef 浅层响应式对象: 只会修改第一层对象&#xff0c;修改此对象第一层属性&#xff0c;视图会有同步变化&#xff0c;非第一层&#xf…

JVM学习-底层字节码的执行过程

目录 1.一个简单的程序分析 2. a&#xff0c;a&#xff0c;a--在JVM中的执行过程 3. 一个好玩的xx 4.方法调用的字节码分析、多态的实现、对象头 5. try-catch-finally的字节码分析 5.1 try-catch 5.2 try-catch-finally 5.3特殊情况 5.3.1 try和finally块中都出现了re…

面向对象技术(第二周)

目录 前言 ⚽回顾 &#x1f3d0;类的层次 定义 层次关系的实现 &#x1f3c0;继承 &#x1f94e;编程方法 非面向对象编程 根本思想 特点 例子&#xff08;设计一个画板系统Panel&#xff09; 第一步&#xff1a;整体设计 第二步&#xff1a;模块具体设计 缺点分…

Linux 常用命令100+

Linux 运维/开发/测试 常用命令100(v1.1) 帮助命令(2个) 命令功能说明示例man 命令查看普通命令帮助&#xff0c;命令的词典&#xff0c;更复杂的还有info&#xff0c;但不常用。rootbrLinux ~]#man lshelp 命令查看Linux内置命令的帮助&#xff0c;比如cd命令。[rootbrLinux…

Apache zookeeper kafka 开启SASL安全认证

背景&#xff1a;我之前安装的kafka没有开启安全鉴权&#xff0c;在没有任何凭证的情况下都可以访问kafka。搜了一圈资料&#xff0c;发现有关于sasl、acl相关的&#xff0c;准备试试。 简介 Kafka是一个高吞吐量、分布式的发布-订阅消息系统。Kafka核心模块使用Scala语言开发…

拼多多商品详情接口数据采集

拼多多商品详情接口数据采集是一个相对专业的任务&#xff0c;通常涉及到使用API接口或第三方采集工具等技术手段。以下是一些基本步骤和注意事项&#xff0c;供您参考&#xff1a; 请求示例&#xff0c;API接口接入Anzexi58 申请开发者账号&#xff1a;如果您打算使用API接口…

基于java+springboot+vue实现的停车场车位预约系统(文末源码+Lw+ppt)23-442

摘 要 本系统为用户而设计制作合庆镇停车场车位预约系统&#xff0c;旨在实现合庆镇停车场车位预约智能化、现代化管理。本合庆镇停车场车位预约管理自动化系统的开发和研制的最终目的是将合庆镇停车场车位预约的运作模式从手工记录数据转变为网络信息查询管理&#xff0c;…

高光谱数据应用于植被监测与分析与数据获

1. 常用高光谱数据 (1) 航空成像光谱仪系统 国内系统&#xff1a;MAIS、OMIS-1、OMIS-2、PHI、WHI、LASIS 国外系统&#xff1a;AIS、AVIRIS、TRWIS、GERIS、HYDICEAISA、DAIS、CASI、HYMAP (2) 航天成像光谱仪 Hyperion/EO-1 环境与减灾小卫星星座&#xff08;HJ-1B&…

ChatGPT GPT4科研应用、数据分析与机器学习、论文高效写作、AI绘图技术

原文链接&#xff1a;ChatGPT GPT4科研应用、数据分析与机器学习、论文高效写作、AI绘图技术https://mp.weixin.qq.com/s?__bizMzUzNTczMDMxMg&mid2247596849&idx3&sn111d68286f9752008bca95a5ec575bb3&chksmfa823ad6cdf5b3c0c446eceb5cf29cccc3161d746bdd9f2…

IPy,一个超级有用的Python库

更多内容在 Python 的 IPy 库是一个专门用于处理 IP 地址和网络相关操作的第三方库。它提供了丰富的功能&#xff0c;可以帮助用户轻松地进行 IP 地址的解析、格式化、比较和范围计算等操作。以下是 IPy 库的一些主要特性和功能&#xff1a; IP 地址解析与表示&#xff1a;IPy …

展开说说:Android之SharedPreferences

SharedPreferences 是一种轻量级的数据持久化存储机制。以key/value键值对形式存储在xml文件&#xff0c;用于保存一些应用程序数据。保存在 /data/data/PACKAGE_NAME/shared_prefs/xxx.Xml文件。 SharedPreferences 只能存储string&#xff0c;int&#xff0c;float&#xff…

实战!wsl 与主机网络通信,在 wsl 中搭建服务器。学了计算机网络,但只能刷刷面试题?那也太无聊了!这篇文章可以让你检测你的计网知识!

前言&#xff08;碎碎念&#xff09;&#xff1a;每次发布文章时&#xff0c;我都是一个纠结的过程。因为我给自己写笔记时&#xff0c;只需要记录自己不清晰或者易忘的知识点就可以了&#xff0c;但一旦想要作为文章发布&#xff0c;那么我就得考虑到很多人是纯新手&#xff0…

【Git】本地仓库关联远程仓库

Git 本地项目关联远程仓库 本地 本地已有项目 ● 项目 07.GitLocalTest 包含有一个js ○ test.js 远程仓库 ● 远程仓库地址 ○ https://github.com/Sonnenlicht77/gitTest.git ○ 仓库只有一个 readme.md 关联 1.本地 1.1 本地仓库 ● git init ● git add . ● gi…

[Node.js] Node.js 版本控制工具 nvm/n/fnm

文章目录 Node.jsNode.js 版本控制工具介绍nvm (Node Version Manager)nfnm (Fast Node Manager) Node.js Node.js 是一个开源与跨平台的 JavaScript 运行环境&#xff0c;它允许 JavaScript 运行&#xff0c; Node.js 使用事件驱动、非阻塞的 I/O 模型&#xff0c;使其轻量且…

HSE化工应急安全生产管理平台:衢州某巨大型化工企业的成功应用

在化工行业中&#xff0c;安全生产一直是至关重要的议题。为了提高生产安全性、降低成本并提升企业形象&#xff0c;衢州某巨大型化工企业引入了HSE化工应急安全生产管理平台&#xff0c;取得了显著的改善和获益。 该平台的核心功能包括风险管理和应急预案制定。通过对化工生产…

JVM学习-垃圾回收专题

目录 1.如何判断对象可以回收 1.1引用计数法 1.2可达性分析算法 1.3五种引用 1.4拓展&#xff1a;直接内存 2.垃圾回收算法 2.1标记清除算法 2.2标记整理算法 2.3复制 3.分代垃圾回收 3.垃圾回收器 3.1串行垃圾回收器 3.2吞吐量优先垃圾回收器 3.3响应时间优先垃圾回收器…

51单片机-AT24C02(I2C总线)

目录 一&#xff0c;介绍及元件工作原理 7.时序结构&#xff08;重要&#xff09; 8.i2C总线数据帧&#xff08;重要&#xff09; 二&#xff0c;应用 一&#xff0c;介绍及元件工作原理 1.元件介绍 2.存储器 3.地址总线和数据总线 地址总线只能一次选中一行 4.引脚及应用…

python之自动化(django)

1、安装 我用的是pip install Django 在命令行中安装 然后django-admin startproject autotext&#xff08;在命令行中&#xff09; 这句话是创建一个django 项目 然后切换到你所创建项目的目录下 输入&#xff1a; python manage.py runserver 当你出现以下错误时 You…