实现一个横向的picker

news2024/10/6 18:30:12

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.coloradmin.cn/o/1523258.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

代码随想录 贪心算法-难度题目-其他题目

目录 53.最大子数组和 134.加油站 968.监控二叉树 53.最大子数组和 53. 最大子数组和 中等 给你一个整数数组 nums &#xff0c;请你找出一个具有最大和的连续子数组&#xff08;子数组最少包含一个元素&#xff09;&#xff0c;返回其最大和。 子数组 是数组中的一个…

HJ212协议C#代码解析实现

HJ212协议C#代码解析实现 HJ212协议是环保中一个非常重要的标准协议&#xff08;字符串协议&#xff09;&#xff0c;之前写了两篇C HJ212协议解析的相关博文&#xff1a; 环保 HJ212协议解析基于Qt5.14.2的HJ212 TCP服务端接收解析入库程序 最近在学习C#&#xff0c;所以打算…

T1.数据库MySQL

二.SQL分类 2.1 DDL 2.1.1数据库操作 1). 查询所有数据库 show databases ; 2). 查询当前数据库 select database(); 3)创建数据库 create database [if not exists] 数据库名 [default charset 字符集] [collate 排序规则] ; 4&#xff09;删除数据库 drop database …

【你也能从零基础学会网站开发】Web建站之jQuery进阶篇 jQuery常见属性和方法概述与使用

&#x1f680; 个人主页 极客小俊 ✍&#x1f3fb; 作者简介&#xff1a;程序猿、设计师、技术分享 &#x1f40b; 希望大家多多支持, 我们一起学习和进步&#xff01; &#x1f3c5; 欢迎评论 ❤️点赞&#x1f4ac;评论 &#x1f4c2;收藏 &#x1f4c2;加关注 jQuery创建新的…

Docker 哲学 - 容器操作 (二)

命令行启动 参数键值之间可以使 " " 或者 空格 卷的挂载是在容器创建时指定的&#xff0c;不能在容器运行时再添加 当加上 --network-alias 设置同一网络下别名参数后 &#xff0c;inspect 该容器发现 会同步到 容器信息中 2、给容器打日志 docker logs 【-…

PHP+golang开源办公系统CRM管理系统

基于ThinkPHP6 Layui MySQL的企业办公系统。集成系统设置、人事管理、消息管理、审批管理、日常办公、客户管理、合同管理、项目管理、财务管理、电销接口集成、在线签章等模块。系统简约&#xff0c;易于功能扩展&#xff0c;方便二次开发。 服务器运行环境要求 PHP > 7.…

静态库与动态库的制作和使用

个人主页&#xff1a;Lei宝啊 愿所有美好如期而遇 目录 前言 库&#xff1f; 为什么要使用库 静态库 静态库的制作和使用 动态库 动态库的制作和使用 四种方法&#xff1a; 直接将库拷贝(安装)到系统路径中 配置环境变量 软链接 添加配置文件 动态库和静态库同时…

jvm 内存泄露、内存溢出、栈溢出区别

JVM&#xff08;Java虚拟机&#xff09;是负责执行Java程序的运行环境。以下是对内存泄露、内存溢出和栈溢出这几个概念的解释&#xff1a; 内存泄露&#xff08;Memory Leak&#xff09;&#xff1a; 内存泄露指的是程序中分配的内存空间在不再被使用时没有被释放的情况。这可…

【python】集合

前言 简洁整理&#xff0c;无废话 集合概念 含义&#xff1a;跟数学中的基本一样 形式&#xff1a;{1,a,(1,2)} 性质&#xff1a;不重复性&#xff0c;集合中每个元素不会有重复&#xff1b;集合中必须是不可变元素&#xff0c;不能有列表可以有元组 创建&#xff1a;{}或…

2核4g服务器可以带多少用户?

腾讯云轻量应用服务器2核4G5M配置性能测评&#xff0c;腾讯云轻量2核4G5M带宽服务器支持多少人在线访问&#xff1f;并发数10&#xff0c;支持每天5000IP人数访问&#xff0c;腾讯云百科txybk.com整理2核4G服务器支持多少人同时在线&#xff1f;并发数测试、CPU性能、内存性能、…

Unity2019.2.x 导出apk 安装到安卓Android12+及以上的系统版本 安装出现-108 安装包似乎无效的解决办法

Unity2019.2.x 导出apk 安装到安卓Android12及以上的系统版本 安装出现-108 安装包似乎无效的解决办法 导出AndroidStudio工程后 需要设置 build.gradle文件 // GENERATED BY UNITY. REMOVE THIS COMMENT TO PREVENT OVERWRITING WHEN EXPORTING AGAINbuildscript {repositor…

Vue3+TypeScript 学习回顾,温故而知新

文章简介&#xff1a; &#xff08;1&#xff09;简介&#xff1a; 在 Vue3 中编码规范如下&#xff1a; 编码语言: JavaScript代码风格: 组合式API选项式、API简写形式: setup语法糖 &#xff08;2&#xff09;复习内容&#xff1a; 1.核心: ref、reactive、computed、w…

亚马逊云科技Glue

Glue 最重要的部分&#xff0c; ETL&#xff1a;用于从 A 点&#xff08;我们的源数据&#xff09;提取、转换和加载数据到 B 点&#xff08;目标文件或数据存储库&#xff09;。 AWS Glue 会为您执行大量此类工作。 转换通常是更繁重的工作&#xff0c;需要从各种来源进行组合…

springboot整合swagger,postman,接口规范

一、postman介绍 1.1概述 工具下载 Postman&#xff08;发送 http 请求的工具&#xff09; 官网&#xff08;下载速度比较慢&#xff09;&#xff1a;Download Postman | Get Started for Free 网盘下载&#xff1a;百度网盘 请输入提取码 1.2Http 请求格式 请求地址请求方法状…

算法刷题笔记

1.力扣-1337.矩阵中战斗力最弱的K行 给你一个大小为 m * n 的矩阵 mat&#xff0c;矩阵由若干军人和平民组成&#xff0c;分别用 1 和 0 表示。 请你返回矩阵中战斗力最弱的 k 行的索引&#xff0c;按从最弱到最强排序。 如果第 i 行的军人数量少于第 j 行&#xff0c;或者两行…

C语言分析基础排序算法——归并排序

目录 归并排序 递归版本 非递归版本 非递归版本的问题 归并排序小优化 归并排序 归并排序&#xff0c;分为分治以及合并&#xff0c;分治部分可以使用递归或者非递归完成&#xff0c;归并排序的基本思路是&#xff1a;将已有序的子序列合并&#xff0c;得到完全有序的序列…

探索递归函数:C语言中的使用方法

递归函数是一种在程序设计中常见且强大的工具&#xff0c;它可以将一个问题分解成更小的子问题&#xff0c;并通过反复调用自身来解决这些子问题。在C语言中&#xff0c;递归函数的运用极大地增强了程序的灵活性和可读性。本文将探讨C语言中如何使用递归函数&#xff0c;以及递…

Python之Web开发中级教程----搭建Web框架二

Python之Web开发中级教程----搭建Web框架二 搭建虚拟环境 虚拟环境的作用 虚拟环境可以搭建独立的python运行环境, 使得单个项目的运行环境与其它项目互不影响. 搭建虚拟环境 &#xff08;1&#xff09;安装 sudo pip install virtualenv sudo pip install virtualenvwra…

JUC之AQS

AQS抽象的队列同步器 public abstract class AbstractQueuedSynchronizerextends AbstractOwnableSynchronizerimplements java.io.Serializable {AbstractQueuedSynchronizer 是用来实现锁或者其他同步器组件的公共基础部分的抽象实现&#xff0c;是重量级基础框架及整个JUC体…

60 个深度学习教程:包含论文、实现和注释 | 开源日报 No.202

labmlai/annotated_deep_learning_paper_implementations Stars: 44.0k License: MIT annotated_deep_learning_paper_implementations 是一个包含深度学习论文的 60 个实现/教程&#xff0c;附带并排注释&#xff1b;包括 transformers&#xff08;原始、xl、switch、feedbac…