大家好,由於最近從事的是微信公眾號和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大家可以下載運行