vue 2 仿IOS 滾輪選擇器 從入門到精通 (一)


大家好,由於最近從事的是微信公眾號和APP內嵌 H5開發,避免不了開發一些和native相同的操作功能,就如接下來說的 仿IOS滾輪選擇器。github源碼鏈接 https://github.com/zhangKunUserGit/vue-component大家可以下載運行

先來個截圖:

接下來具體介紹如何實現的。能力有限避免不了錯誤請指出,有問題QQ郵箱 1766597067@qq.com

 

先來屢一下需求:

1.移動端用戶手上下滑動,內容上下移動,用戶手離開數字按照慣性移動一段距離。

2.當停止移動后,選中一個文字並且文字高亮,上面的值會變成你選中的文字。

3.可以連續滾動。

 

好了我們知道需求了,開始寫吧。

寫之前,想來一句 “上海天真藍,可我在寫代碼”。

說起滾動,不得不提css3的  transform-style: preserve-3d;  backface-visibility: hidden;

(1)transform-style 屬性規定如何在 3D 空間中呈現被嵌套的元素。值如下圖:

我們使用preserve-3d 是讓我們的值列表呈現3d效果,他是寫在列表父級;

(2)backface-visivility 屬性定義當元素不面向屏幕時是否可見。

我們使用hidden是背面不可見的,他是寫在列表上

不過只有他們是無法完成這個艱巨界面的。只是這兩個比較少見並少用,在此記錄一下。

 

結合上面的知識點那我們怎么實現滾筒呢?

我實現的方法如下:(transform-style為什么子元素需要定位?

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
  <style>
    *{
      padding: 0;
      margin: 0;
      list-style: none;
    }
    .wrapper{
      margin: 200px auto;
      width: 200px;
      position: relative;
    }
    ul{
      width: 100%;
      transform-style: preserve-3d;
      position: absolute;
      top: 40%;
      left:0;
      height: 34px;
    }
    li{
      backface-visibility: hidden;
      position: absolute;
      left: 0;
      top: 0;
      height: 34px;
      text-align: center;
      width: 100%;
    }
  </style>
</head>
<body>
<div class="wrapper">
  <ul>
    <li style="transform: rotate3d(1, 0, 0, 80deg) translate3d(0, 0, 100px)">27</li>
    <li style="transform: rotate3d(1, 0, 0, 60deg) translate3d(0, 0, 100px)">28</li>
    <li style="transform: rotate3d(1, 0, 0, 40deg) translate3d(0, 0, 100px)">29</li>
    <li style="transform: rotate3d(1, 0, 0, 20deg) translate3d(0, 0, 100px)">30</li>
    <li style="transform: rotate3d(1, 0, 0, 0deg) translate3d(0, 0, 100px)">1</li>
    <li style="transform: rotate3d(1, 0, 0, -20deg) translate3d(0, 0, 100px)">2</li>
    <li style="transform: rotate3d(1, 0, 0, -40deg) translate3d(0, 0, 100px)">3</li>
    <li style="transform: rotate3d(1, 0, 0, -60deg) translate3d(0, 0, 100px)">4</li>
    <li style="transform: rotate3d(1, 0, 0, -80deg) translate3d(0, 0, 100px)">5</li>
  </ul>
</div>
</body>
</html>

可以看到我是用到了定位,rotate3d 和 translate3d, 可能你會問為什么要用到translate3d 並且第三個參數寫100px?

  我主要是用到的定位,都定位到一起了,也就是一個黑點了,哈哈。。。 然后用 transform 的 rotate3d  統一 沿Y軸旋轉元素 到一定的角度,然而我們要做滾筒,滾筒需要半徑,所以我用translate3d 拉伸 Z 軸 (垂直屏幕)100px,

這樣元素就沿着我拉伸前的原點旋轉,半徑是 100px; 大家可以復制代碼運行一下,看看效果,如何有其他方法分享出來吧,共同學習進步。

 

說了這么多,跟vue有什么關系呢? 哈哈。。。你猜?

 

滾動用什么呢? 我之前用過 scroll  ios 需要加上  -webkit-overflow-scrolling: touch; 才能觸發onscroll, 但是那種做法我試了一下,太麻煩,有滾動條,太垃圾。

這里我們用 touchstart / touchmove / touchend

 mounted() {
      this.$el.addEventListener('touchstart', this.listenerTouchStart, false);
      this.$el.addEventListener('touchmove', this.listenerTouchMove, false);
      this.$el.addEventListener('touchend', this.listenerTouchEnd, false);
 },
 methods: {
    listenerTouchStart(ev) {
        ev.stopPropagation();
        ev.preventDefault();
        isInertial = false;
        this.finger.startY = ev.targetTouches[0].pageY;
        this.finger.prevMove = this.finger.currentMove;
        this.finger.startTime = Date.now();
      },
      listenerTouchMove(ev) {
        ev.stopPropagation();
        ev.preventDefault();
        const move = (this.finger.startY - ev.targetTouches[0].pageY) + this.finger.prevMove;
        this.finger.currentMove = move;
        this.$refs.wheel.style.transform = `rotate3d(1, 0, 0, ${(move / lineHeight) * singleDeg}deg)`;
        this.updateRange(Math.round(move / lineHeight));
      },
      listenerTouchEnd(ev) {
        ev.stopPropagation();
        ev.preventDefault();
        this.finger.endY = ev.changedTouches[0].pageY;
        this.finger.endTime = Date.now();
        this.getInertiaDistance();
      },
}

我們在 start 時,緩存手觸摸的的Y軸坐標 ,startTime 是為了后面touchend時,計算初速度  (一定距離 時間越短 速度越大,慣性滑動越長)

/**
       * 求移動速度(v = s / t),判斷用戶操作快慢,從而得到慣性的滑動距離
       */
      getInertiaDistance() {
        // 移動距離
        const s = this.finger.startY - this.finger.endY;
        // 移動時間
        const t = this.finger.endTime - this.finger.startTime;
        // 移動速度
        const v = s / t;
        const absV = Math.abs(v);
        isInertial = true;
        this.inertia(absV, Math.floor(absV / v), 0);
      },
      /**
       * 用戶結束滑動,應該慢慢放慢,最終停止。從而需要 a(加速度)
       * @param start 開始速度
       * @param position 速度方向,值: 正負1
       * @param target 結束速度
       */
      inertia(start, position, target) {
        if (start <= target || !isInertial) {
          this.animate.stop();
          this.finger.prevMove = this.finger.currentMove;
          this.updateRange(Math.round(this.finger.currentMove / lineHeight));
          this.getSelectValue(this.finger.currentMove);
          return;
        }
        // 這段時間走的位移 S = vt + 1/2at^2;
        const move = (position * start * (1000 / 60)) + (0.5 * a * (1000 / 60) * (1000 / 60)) + this.finger.currentMove;
        // 根據求末速度公式: v末 = v初 + at
        const newStart = (position * start) + (a * (1000 / 60));
        let moveDeg = (move / lineHeight) * singleDeg;
        let actualMove = move;
        // 已經到達目標
        if (newStart <= target) {
          moveDeg = Math.round(move / lineHeight) * singleDeg;
          actualMove = Math.round(move / lineHeight) * lineHeight;
          this.$refs.wheel.style.transition = 'transform 700ms cubic-bezier(0.19, 1, 0.22, 1)';
        } else {
          this.$refs.wheel.style.transition = '';
        }
        this.finger.currentMove = actualMove;
        this.$refs.wheel.style.transform = `rotate3d(1, 0, 0, ${moveDeg}deg)`;
        this.updateRange(Math.round(this.finger.currentMove / lineHeight));
        this.animate.start(this.inertia.bind(this, newStart, position, target));
      }

這里動畫是 requestAnimationFrame  我稍微封裝了一下,里面用到的公式我已經標注;

我們需要連續滾動,所以界面需要連續刷新,不斷更新數字(可能有更好的方法吧)

computed: {
      scrollValues() {
        const result = [];
        for (let i = this.range.start; i <= this.range.end; i += 1) {
          result.push({
            value: this.getRangeData(i),
            index: i, // 這里是旋轉參數
          });
        }
        return result;
      },
      getListTop() {
        return {
          top: `${radius - Math.round(lineHeight / 2)}px`,
          height: `${lineHeight}px`
        };
      },
      getWrapperHeight() {
        return {
          height: `${2 * radius}px`,
        };
      },
      getCoverStyle() {
        return {
          backgroundSize: `100% ${radius - Math.round(lineHeight / 2)}px`,
        };
      },
      getDividerStyle() {
        return {
          top: `${radius - Math.round(lineHeight / 2)}px`,
          height: `${lineHeight - 2}px`,
        };
      },
      animate() {
        return new Animate();
      }
    },

 最后我把所有的變量提取出來,到時候能根據用戶要求顯示不同情況

 const a = -0.003; // 加速度
  const radius = 100; // 半徑
  const lineHeight = 36; // 文字行高
  let isInertial = false; // 是否正在慣性滑動
  // 根據三角形余弦公式
  // 反余弦得到弧度再轉換為度數,這個度數是單行文字所占有的。
  let deg = Math.round((Math.acos((((radius * radius) + (radius * radius)) - (lineHeight * lineHeight)) / (2 * radius * radius)) * 180) / Math.PI);
  // deg這個值須360能整除,因為當滾動列占滿一周后可以再次均勻的覆蓋在上一周文字上;滾動時不會出現錯位
  while (360 % deg !== 0 && deg <= 360) {
    deg += 1;
  }
  const singleDeg = deg;
  // 半圓下的內容條數
  const space = Math.floor((360 / singleDeg) / 2);

最后附上github源碼鏈接 https://github.com/zhangKunUserGit/vue-component大家可以下載運行


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM