picker左右滑动

横向picker

主要运用在移动端项目中,通常的组件是通过纵向的滑动来选择,使用transform的问题是页面虽然旋转过来了,但是让他左右移动时的动作要与旋转前一样,不符合操作逻辑,因此造了这个轮子。有需要的可以点赞收藏。

效果如下

在这里插入图片描述

vue代码

需要传入pickerData数组数据,返回事件result,代码中已添加滑动变化时震动
震动代码为:

//跳过边界震动
if (!(y > option.maxX || y < -option.minX) && item !== option.item) {
	navigator.vibrate(20);
    option.item = item;
}

横向完整代码

<template>
  <div>
    <div class="picker-group">
      <div
        class="picker-row"
        @touchstart="touchStart"
        @touchmove="touchMove"
        @touchend="touchEnd"
      >
        <ul
          class="picker-content"
          :style="
            'transform: translate3d(' +
            option.translateX +
            'px, 0px, 0px);display: flex;transition:' +
            option.pointerdown +
            ' ease 0s'
          "
        >
          <li
            v-for="(i, o) in state.pickerData"
            :key="o"
            :class="option.activeItem == o ? 'active' : ''"
          >
            {{ i }}
          </li>
        </ul>
      </div>
    </div>
  </div>
</template>

<script>
import { reactive } from "@vue/reactivity";
import { onMounted } from "@vue/runtime-core";
export default {
  props: {
    pickerData: {
      typeof: Array,
      default: () => {
        return [];
      },
    },
  },
  setup(props, { emit }) {
    const state = reactive({
      pickerData: props.pickerData,
    });
    const option = reactive({
      isPointerdown: false,
      pointerdown: "transform 300ms",
      ul: null,
      pickerRow: null,
      itemWidth: 120, // 列表项宽度
      pseudoWidth: 0, //伪元素宽度
      maxX: 0, //初始位置也是最大的位置
      minX: 0, //最小的位置
      lastX: 0,
      diffX: 0,
      translateX: 0, // 当前位置
      friction: 0.95, // 摩擦系数
      distanceX: 0, // 滑动距离
      activeItem: 0,
      result: state.pickerData[0],
      item: 0, //监听变化震动
    });
    /**
     * @description: 初始化
     * @return {*}
     */
    const render = () => {
      option.ul = document.querySelector(".picker-content");
      option.pickerRow = document.querySelector(".picker-row");
      option.itemWidth = option.ul.querySelector("li").offsetWidth; // 列表项宽度
      option.pseudoWidth = window.getComputedStyle(
        option.pickerRow,
        "after"
      ).width; //伪元素宽度
      option.maxX = option.pseudoWidth.replace(/[a-zA-Z]+/g, ""); //初始位置也是最大的位置
      option.minX =
        option.itemWidth * (state.pickerData.length - 1) - option.maxX; //最小的位置
      option.translateX = option.maxX;
      emit("result", option.result);
    };

    /**
     * @description: 点击
     * @param {*} e
     * @return {*}
     */
    const touchStart = (e) => {
      option.isPointerdown = true;
      option.lastX = e.touches[0].clientX;
      option.diffX = 0;
      option.distanceX = 0;
      getTransform();
    };
    /**
     * @description: 移动
     * @param {*} e
     * @return {*}
     */
    const touchMove = (e) => {
      if (option.isPointerdown) {
        option.diffX = e.touches[0].clientX - option.lastX;
        option.translateX += option.diffX;
        option.lastX = e.touches[0].clientX;

        //震动操作
        let y = option.translateX + option.distanceX;
        let item = Math.round(
          (option.translateX - option.maxX) / option.itemWidth
        );
        //跳过边界震动
        if (!(y > option.maxX || y < -option.minX) && item !== option.item) {
          option.activeItem = Math.abs(item);
          navigator.vibrate(20);
          option.item = item;
        }
      }
    };
    /**
     * @description: 结束
     * @param {*} e
     * @return {*}
     */
    const touchEnd = () => {
      if (option.isPointerdown) {
        option.isPointerdown = false;
        getTranslateX();

        // 滑动距离与时长成正比且最短时长为300ms
        const duration = Math.max(Math.abs(option.distanceX) * 1.5, 300);
        option.ul.style.transition = "transform " + duration + "ms ease";
        option.pointerdown = "transform 300ms";
      }
    };

    /**
     * @description: 设置位置及返回数据
     * @return {*}
     */
    const getTranslateX = () => {
      let speed = option.diffX;
      while (Math.abs(speed) > 1) {
        speed *= option.friction;
        option.distanceX += speed;
      }
      // 边界判断
      let y = option.translateX + option.distanceX;
      if (y > option.maxX) {
        option.translateX = option.maxX;
        option.distanceX = option.maxX - option.translateX;
      } else if (y < -option.minX) {
        option.translateX = -option.minX;
        option.distanceX = option.minX - option.translateX;
      } else {
        option.translateX = y;
      }

      // 计算停止位置使其为itemWidth的整数倍
      let i = Math.round((option.translateX - option.maxX) / option.itemWidth);
      option.translateX = Number(option.maxX) + Number(i * option.itemWidth);
      option.activeItem = Math.abs(i);
      option.result = state.pickerData[Math.abs(i)];

      emit("result", option.result);
    };
    /**
     * @description: 设点击时初始位置
     * @return {*}
     */
    const getTransform = () => {
      const transform = window
        .getComputedStyle(option.ul)
        .getPropertyValue("transform");
      option.translateX = parseFloat(transform.split(",")[4]);
      option.pointerdown = "none 0s";
    };

    onMounted(() => {
      render();
    });

    return { state, option, touchStart, touchMove, touchEnd };
  },
};
</script>

<style lang="less" scoped>
.picker-group {
  display: flex;
}

.picker-row {
  position: relative;
  flex: 1;
  margin: auto 0;
  overflow: hidden;
  touch-action: none;
  display: flex;
}

.picker-row::before {
  content: "";
  position: absolute;
  display: inline;
  top: 0;
  left: 0;
  bottom: 0;
  z-index: 1;
  width: calc(~"50% - 60px");
  border-right: 1px solid #ebebeb;
  background: linear-gradient(
    to bottom,
    rgba(255, 255, 255, 0.6),
    rgba(255, 255, 255, 0.9)
  );
}

.picker-row::after {
  content: "";
  position: absolute;
  display: inline;
  bottom: 0;
  right: 0;
  top: 0;
  z-index: 1;
  width: calc(~"50% - 60px");
  border-left: 1px solid #ebebeb;
  background: linear-gradient(
    to bottom,
    rgba(255, 255, 255, 0.9),
    rgba(255, 255, 255, 0.6)
  );
}

li {
  list-style: none;
  font-size: 14px;
  width: 120px;
  line-height: 20px;
  text-align: center;
}
.active {
  font-size: 16px;
  font-weight: 700;
}

.btn-sure {
  display: block;
  margin: 15px auto 0;
}
</style>

3D滚动代码

<template>
  <div>
    <div class="picker-group">
      <div class="picker-row">
        <ul
          @touchstart="touchStart"
          @touchmove="touchMove"
          @touchend="touchEnd"
          class="picker-content"
          :style="
            'transform: rotateY(' +
            option.rotateY +
            'deg);transition:' +
            option.pointerdown +
            ' ease 0s'
          "
        >
          <li
            v-for="(i, o) in state.pickerData"
            :key="o"
            :style="'transform: rotateY(' + o * 45 + 'deg) translateZ(160px); '"
            :class="option.activeItem == o ? 'active' : ''"
          >
            {{ i }}
          </li>
          <li
            :style="
              'transform: rotateY(' +
              Number(option.minX + 45) +
              'deg) translateZ(160px)'
            "
          >
            &nbsp;
          </li>
          <li
            v-if="state.pickerData.length == 6"
            :style="
              'transform: rotateY(' +
              Number(option.minX + 90) +
              'deg) translateZ(160px)'
            "
          >
            &nbsp;
          </li>
        </ul>
      </div>
    </div>
  </div>
</template>

<script>
import { reactive } from "@vue/reactivity";
import { onMounted } from "@vue/runtime-core";
export default {
  props: {
    pickerData: {
      typeof: Array,
      default: () => {
        return [];
      },
    },
  },
  setup(props, { emit }) {
    const state = reactive({
      pickerData: props.pickerData,
    });
    const option = reactive({
      isPointerdown: false,
      pointerdown: "transform 300ms",
      degPx: 0, // 多少px为1度
      maxX: 0, //初始位置也是最大的位置
      minX: 0, //最小的位置
      lastX: 0,
      diffX: 0, // 移动过程中的微小距离变化
      rotateY: 0, //旋转角度
      friction: 0.95, // 摩擦系数
      distanceX: 0, // 滑动距离
      activeItem: 0,
      result: state.pickerData[0],
      item: 0, //监听变化震动
      dataLength: 0,
    });
    /**
     * @description: 初始化
     * @return {*}
     */
    const render = () => {
      option.degPx = 3; // 多少px为1度
      option.dataLength = state.pickerData.length;
      option.maxX = 0; //初始位置也是最大的位置
      option.minX = (option.dataLength - 1) * 45; //最小的位置
      emit("result", option.result);
    };

    /**
     * @description: 点击
     * @param {*} e
     * @return {*}
     */
    const touchStart = (e) => {
      option.isPointerdown = true;
      option.lastX = e.touches[0].clientX;
      option.diffX = 0;
      option.distanceX = 0;
      option.pointerdown = "none 0s";
    };
    /**
     * @description: 移动
     * @param {*} e
     * @return {*}
     */
    const touchMove = (e) => {
      if (option.isPointerdown) {
        option.diffX = e.touches[0].clientX - option.lastX;
        option.lastX = e.touches[0].clientX;
        option.rotateY += option.diffX / option.degPx;
        if (option.rotateY > option.maxX) {
          option.rotateY = 0;
        } else if (option.rotateY < -option.minX) {
          option.rotateY = -option.minX;
        }

        //震动
        let y = option.rotateY + option.distanceX / option.degPx;
        let item = Math.round((option.rotateY - 0) / 45);
        //跳过边界震动
        if (!(y > option.maxX || y < -option.minX) && item !== option.item) {
          if (navigator.vibrate) navigator.vibrate(20);
          option.item = item;
          option.activeItem = Math.abs(item);
        }
      }
    };
    /**
     * @description: 结束
     * @param {*} e
     * @return {*}
     */
    const touchEnd = () => {
      if (option.isPointerdown) {
        option.isPointerdown = false;
        getTranslateX();
        // 滑动距离与时长成正比且最短时长为300ms
        const duration = Math.max(Math.abs(option.distanceX), 300);
        option.pointerdown = "transform " + duration + "ms";
      }
    };

    /**
     * @description: 设置位置及返回数据
     * @return {*}
     */
    const getTranslateX = () => {
      let speed = option.diffX;
      while (Math.abs(speed) > 1) {
        speed *= option.friction;
        option.distanceX += speed;
      }
      // 边界判断
      let y = option.rotateY + option.distanceX / option.degPx; // 原始位置与变化位置之和
      if (y > 0) {
        option.rotateY = 0;
      } else if (y < -option.minX - 45) {
        option.rotateY = -option.minX;
      } else {
        option.rotateY = y;
      }
      // 计算停止位置使其为itemWidth的整数倍
      let i = Math.round((option.rotateY - 0) / 45);
      if (Math.abs(i) >= option.dataLength) {
        i = -option.dataLength + 1;
      }
      option.rotateY = i * 45;
      option.activeItem = Math.abs(i);
      option.result = state.pickerData[Math.abs(i)];
      emit("result", option.result);
    };

    onMounted(() => {
      render();
    });

    return { state, option, touchStart, touchMove, touchEnd };
  },
};
</script>

<style lang="less" scoped>
.picker-group {
  display: flex;
}

.picker-row {
  margin: 0 auto;
  width: 120px;
  height: 30px;
  position: relative;
  .picker-content {
    height: 100%;
    width: 100%;
    position: absolute;
    transform-style: preserve-3d;
  }
}

li {
  display: block;
  position: absolute;
  width: 135px;
  background: #fff;
  font-size: 1em;
  text-align: center;
  color: #000;
}
.active {
  font-size: 16px;
  font-weight: 700;
}

.btn-sure {
  display: block;
  margin: 15px auto 0;
}
</style>

html代码

可以直接运行的html代码。

<!DOCTYPE html>
<!-- saved from url=(0052)http://jsdemo.codeman.top/html/pickerTransition.html -->
<html lang="en">
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />

    <meta
      name="viewport"
      content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
    />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>pickerTransition</title>
    <link
      rel="icon"
      type="image/x-icon"
      href="http://jsdemo.codeman.top/images/favicon.ico"
    />
    <style>
      * {
        margin: 0;
        padding: 0;
      }

      .btn {
        height: 32px;
        padding: 0 15px;
        text-align: center;
        font-size: 14px;
        line-height: 32px;
        color: #fff;
        border: none;
        background: #1890ff;
        border-radius: 2px;
        cursor: pointer;
      }

      .mask {
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        z-index: 999;
        background: rgba(0, 0, 0, 0.6);
        animation: fadeIn 0.3s forwards;
      }

      .slide-box {
        position: fixed;
        left: 0;
        right: 0;
        bottom: 0;
        padding: 15px;
        border-radius: 10px 10px 0 0;
        background: #fff;
        user-select: none;
      }

      h4 {
        height: 24px;
        margin-bottom: 16px;
        font-size: 16px;
        line-height: 24px;
        text-align: center;
      }

      .picker-group {
        display: flex;
      }

      .picker-row {
        position: relative;
        flex: 1;
        height: 200px;
        margin: auto 0;
        overflow: hidden;
        touch-action: none;
        display: flex;
      }

      .picker-row::before {
        content: "";
        position: absolute;
        display: inline;
        top: 0;
        left: 0;
        bottom: 0;
        z-index: 1;
        height: 79px;
        width: calc(50% - 28px);
        border-right: 1px solid #ebebeb;
        background: linear-gradient(
          to bottom,
          rgba(255, 255, 255, 0.6),
          rgba(255, 255, 255, 0.9)
        );
      }

      .picker-row::after {
        content: "";
        position: absolute;
        display: inline;
        bottom: 0;
        right: 0;
        top: 0;
        z-index: 1;
        width: calc(50% - 28px);
        height: 79px;
        border-left: 1px solid #ebebeb;
        background: linear-gradient(
          to bottom,
          rgba(255, 255, 255, 0.9),
          rgba(255, 255, 255, 0.6)
        );
      }

      li {
        list-style: none;
        font-size: 14px;
        width: 56px;
        line-height: 40px;
        text-align: center;
      }

      .btn-sure {
        display: block;
        margin: 15px auto 0;
      }
    </style>
  </head>

  <body>
    <div class="mask">
      <div class="slide-box">
        <h4>时间选择器</h4>
        <div class="picker-group">
          <div class="picker-row">
            <ul
              class="picker-content"
              style="
                transform: translate3d(calc(50% - 28px), 0px, 0px);
                transition: transform 300ms ease 0s;
                display: flex;
              "
            >
              <li>s</li>
            </ul>
          </div>
        </div>
        <button class="btn btn-sure" type="button">确定</button>
      </div>
    </div>
    <script>
      const btnSure = document.querySelector(".btn-sure");
      const slide = document.querySelector(".slide-box");
      btnSure.addEventListener("click", function (e) {
        alert(hourPicker.result);
        e.preventDefault();
      });
      /**
       * 原生javascript实现picker
       */
      class Picker {
        constructor(options) {
          this.options = Object.assign({}, options);
          this.isPointerdown = false;
          const ul = document.querySelector(".picker-content");
          const pickerRow = document.querySelector(".picker-row");
          this.itemWidth = ul.querySelector("li").offsetWidth; // 列表项宽度
          const pseudoWidth = window.getComputedStyle(pickerRow, "after").width; //伪元素宽度
          this.maxX = pseudoWidth.replace(/[a-zA-Z]+/g, ""); //初始位置也是最大的位置
          this.minX =
            this.itemWidth * (this.options.list.length - 1) - this.maxX; //最小的位置
          this.lastX = 0;
          this.diffX = 0;
          this.translateX = 0; // 当前位置
          this.friction = 0.95; // 摩擦系数
          this.distanceX = 0; // 滑动距离
          this.result = this.options.list[0];
          this.render();
          this.bindEventListener();
        }
        render() {
          let html = "";
          for (const item of this.options.list) {
            html += "<li>" + item + "</li>";
          }
          this.options.pickerContent.innerHTML = html;
          this.options.pickerContent.style.transform =
            "translate3d(" + this.maxX + "px, 0px, 0px)";
        }
        handlePointerdown(e) {
          // 如果是鼠标点击,只响应左键
          if (e.pointerType === "mouse" && e.button !== 0) {
            return;
          }
          this.options.pickerColumn.setPointerCapture(e.pointerId);
          this.isPointerdown = true;
          this.lastX = e.clientX;
          this.diffX = 0;
          this.distanceX = 0;
          this.getTransform();
          this.options.pickerContent.style.transform =
            "translate3d(" + this.translateX + "px, 0px, 0px)";
          this.options.pickerContent.style.transition = "none";
        }
        handlePointermove(e) {
          if (this.isPointerdown) {
            this.diffX = e.clientX - this.lastX;
            this.translateX += this.diffX;
            this.lastX = e.clientX;
            this.options.pickerContent.style.transform =
              "translate3d(" + this.translateX + "px, 0px, 0px)";
          }
        }
        handlePointerup(e) {
          if (this.isPointerdown) {
            this.isPointerdown = false;
            this.getTranslateX();
            // 滑动距离与时长成正比且最短时长为300ms
            const duration = Math.max(Math.abs(this.distanceX) * 1.5, 300);
            this.options.pickerContent.style.transition =
              "transform " + duration + "ms ease";
            this.options.pickerContent.style.transform =
              "translate3d(" + this.translateX + "px, 0px, 0px)";
          }
        }
        handlePointercancel(e) {
          if (this.isPointerdown) {
            this.isPointerdown = false;
          }
        }
        bindEventListener() {
          this.handlePointerdown = this.handlePointerdown.bind(this);
          this.handlePointermove = this.handlePointermove.bind(this);
          this.handlePointerup = this.handlePointerup.bind(this);
          this.handlePointercancel = this.handlePointercancel.bind(this);
          this.options.pickerColumn.addEventListener(
            "pointerdown",
            this.handlePointerdown
          );
          this.options.pickerColumn.addEventListener(
            "pointermove",
            this.handlePointermove
          );
          this.options.pickerColumn.addEventListener(
            "pointerup",
            this.handlePointerup
          );
          this.options.pickerColumn.addEventListener(
            "pointercancel",
            this.handlePointercancel
          );
        }
        getTransform() {
          const transform = window
            .getComputedStyle(this.options.pickerContent)
            .getPropertyValue("transform");
          this.translateX = parseFloat(transform.split(",")[4]);
        }
        getTranslateX() {
          let speed = this.diffX;
          while (Math.abs(speed) > 1) {
            speed *= this.friction;
            this.distanceX += speed;
          }
          // 边界判断
          let y = this.translateX + this.distanceX;
          if (y > this.maxX) {
            this.translateX = this.maxX;
            this.distanceX = this.maxX - this.translateX;
          } else if (y < -this.minX) {
            this.translateX = -this.minX;
            this.distanceX = this.minX - this.translateX;
          } else {
            this.translateX = y;
          }

          // 计算停止位置使其为itemWidth的整数倍
          let i = Math.round((this.translateX - this.maxX) / this.itemWidth);
          this.translateX = Number(this.maxX) + Number(i * this.itemWidth);
          this.result = this.options.list[Math.abs(i)];
        }
      }

      // 调用方式
      function createList(start, end) {
        const list = [];
        for (i = start; i < end; i++) {
          list[i] = i < 10 ? "0" + i : "" + i;
        }
        return list;
      }
      const hours = createList(0, 24);
      const pickerColumns = document.querySelectorAll(".picker-row");
      const pickerContents = document.querySelectorAll(".picker-content");
      const hourPicker = new Picker({
        pickerColumn: pickerColumns[0],
        pickerContent: pickerContents[0],
        list: hours,
      });
    </script>
  </body>
</html>

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值