接著前面的行動端效果的研究,這次來看看行動端效果中Picke的實作方法
選擇器的實作原理
行動端效果之Swiper
程式碼看這裡:github
<!-- 说明: 1. 类 行動端效果中Picke的實作方法-3d 是为了提供3d视角,如果不需要可以去掉 2. 类 行動端效果中Picke的實作方法-slot-absolute 在3d视角中需要加上,因为下面相对定位的 行動端效果中Picke的實作方法-items 是要相对父容器进行 transform的,如果不加,就会造成位移不正确 3. DOM中所有的style样式都是在初始化的时候加上的--><p class="行動端效果中Picke的實作方法 行動端效果中Picke的實作方法-3d"> <p class="行動端效果中Picke的實作方法-items"> <p class="行動端效果中Picke的實作方法-slot 行動端效果中Picke的實作方法-slot-absolute" style="flex:1;"> <p class="行動端效果中Picke的實作方法-slot-wrapper" id="wrapper" style="height: 108px;"> <p class="行動端效果中Picke的實作方法-item 行動端效果中Picke的實作方法-selected" style="height:36px;line-height: 36px">1981</p> <!-- ... --> <p class="行動端效果中Picke的實作方法-item" style="height:36px;line-height: 36px">1999</p> </p> </p> </p> <p class="行動端效果中Picke的實作方法-center-highlight" style="height:36px;margin-top:-18px;"></p></p>
由於餓了麼源碼中的行動端效果中Picke的實作方法
是採用v-for
指令產生的DOM
,因此我這裡只是簡單的用javascript
來模擬DOM
的生成。
var el = document.querySelector('#wrapper'); var animationFrameId = null; var currentValue; var itemHeight = 36; var visibleItemCount = 3; var valueIndex = 0; var rotateEffect = true; var datas = ['1981', '1982', '1983', '...', '1999'];// 如果支持3d视角,则给<p class="行動端效果中Picke的實作方法"></p>加上类"行動端效果中Picke的實作方法-3d"// <p class="行動端效果中Picke的實作方法-slot" style="flex:1;">加上类"行動端效果中Picke的實作方法-slot-absolute"if (rotateEffect) { var 行動端效果中Picke的實作方法 = document.querySelector('.行動端效果中Picke的實作方法'); var 行動端效果中Picke的實作方法Slot = document.querySelector('.行動端效果中Picke的實作方法-slot'); 行動端效果中Picke的實作方法.classList.add('行動端效果中Picke的實作方法-3d'); 行動端效果中Picke的實作方法Slot.classList.add('行動端效果中Picke的實作方法-slot-absolute');}// 限定容器高度el.style.height = `${visibleItemCount * itemHeight}px`;// 生成DOMvar html = '';datas.forEach(function(data, index) { html += `<p class="行動端效果中Picke的實作方法-item" style="height:36px;line-height:36px;">${data}</p>`;});el.innerHTML = html;// 激活当前itemvar 行動端效果中Picke的實作方法Items = document.querySelectorAll('.行動端效果中Picke的實作方法-item');行動端效果中Picke的實作方法Items[valueIndex].classList.add('行動端效果中Picke的實作方法-selected');
總體上來說,行動端效果中Picke的實作方法
的事件也包括滑動開始、滑動中、滑動結束。因為畢竟是行動端,滑動不可避免。這次,原始碼中的對滑動事件進行了封裝,相容了PC
#端以及排除了拖曳和選擇造成的影響,具體看一下分析。 `
/** * draggable.js * 只是起到一定的兼容性 * 实质和直接调用 el.addEventListener('touchstart', startFn); 并没有多大差别 */// 滑动开始// touchstart 和 mousedown 可见对PC端的兼容// onselectstart/ondragstart 直接return 可见排除了拖动和选择element.addEventListener(supportTouch ? 'touchstart' : 'mousedown', function(event) { if (isDragging) return; document.onselectstart = function() { return false; }; document.ondragstart = function() { return false; }; // ...});// 滑动结束var endFn = function(event) { // 注销事件 if (!supportTouch) { document.removeEventListener('mousemove', moveFn); document.removeEventListener('mouseup', endFn); } document.onselectstart = null; document.ondragstart = null; isDragging = false; if (options.end) { options.end(supportTouch ? event.changedTouches[0] || event.touches[0] : event); }}
要是DOM
跟隨自己在手機螢幕上的滑動而滑動,方法大同小異,無非就是在開始滑動記錄開始位置,滑動中即時計算位移,滑動結束之後將DOM
滑動應該滑動的位置。這點可以參考前面一篇文章移動端效果之Swiper,這篇文章中有著相同的方法。這裡重點講一下其中的差異
// 滑动开始的执行事件方法start: function(event) { dragState = { range: getDragRange(), // ... startTranslateTop: translateUtil.getElementTranslate(el).top };}
這其中有兩個方法,第一個getDragRange
和第二個getElementTranslate(el )
.
第一個函數的作用是取得行動端效果中Picke的實作方法
能夠滑動的最小和最大的位移,這將會在滑動結束事件中使用到。關於如何計算,這裡簡單提一下,向下滑動,最大不能超過最中間的item
的最上方,這也就是為什麼itemHeight * Math.floor(visibleItemCount / 2)
,而向上滑動,最大不能超過中間item
的最下方,-itemHeight * (valuesLength - Math.ceil(visibleItemCount / 2))
,細細想一下就好了。
第二個函數的作用是取得目前行動端效果中Picke的實作方法
的transform
值,作為下一次滑動計算的依據。其實感覺這樣挺費事,因為在touchend
中最後肯定會計算translate
值,我們只需要每次保存最後滑動的移動值就好了,而不要每次都要在DOM
中取。
/** * translateUtil * 对浏览器对前缀支持的一些判断 * 检测浏览器对3d属性的支持情况 * 获取当前的translate值/清空行動端效果中Picke的實作方法的translate值/移动行動端效果中Picke的實作方法 * 对于浏览器的检测方面,这也算是一个比较好的工具类 */var docStyle = document.documentElement.style;var engine;var translate3d = false;// 浏览器判断if (window.opera && Object.prototype.toString.call(opera) === '[object Opera]') { engine = 'presto';} else if ('MozAppearance' in docStyle) { engine = 'gecko';} else if ('WebkitAppearance' in docStyle) { engine = 'webkit';} else if (typeof navigator.cpuClass === 'string') { engine = 'trident';}// css前缀var cssPrefix = { trident: '-ms-', // IE gecko: '-moz-', // FireFox webkit: '-webkit-', // Chrome/Safari/etc... presto: '-o-' // Opera}[engine];// style前缀var vendorPrefix = { trident: 'ms', gecko: 'Moz', webkit: 'Webkit', presto: 'O'}[engine];var helpElem = document.createElement('p');var perspectiveProperty = vendorPrefix + 'Perspective';var transformProperty = vendorPrefix + 'Transform';var transformStyleName = cssPrefix + 'transform';var transitionProperty = vendorPrefix + 'Transition';var transitionStyleName = cssPrefix + 'transition';var transitionEndProperty = vendorPrefix.toLowerCase() + 'TransitionEnd';if (helpElem.style[perspectiveProperty] !== undefined) { translate3d = true;}// 讲一下这个正则// \s*(-?\d+(\.\d+?)?)px 这是一个单元,匹配这样的 -23.15px, 剩下的应该就好理解了var regexp = /translate\(\s*(-?\d+(\.\d+?)?)px,\s*(-?\d+(\.\d+?)?)px\)\s*translateZ\(0px\)/ig;
接下來看看滑動中
drag: function(event) { // 加上 dragging 类是为了清除过渡效果,在swiper中也有同样的应用 el.classList.add('dragging'); dragState.left = event.pageX; dragState.top = event.pageY; var deltaY = dragState.top - dragState.startTop; // 计算当前的滑动位移 var translate = dragState.startTranslateTop + deltaY; // 滑动元素 translateUtil.translateElement(el, null, translate); velocityTranslate = translate - prevTranslate || translate; prevTranslate = translate; if (rotateEffect) { updateRotate(prevTranslate, 行動端效果中Picke的實作方法Items); }}
看到以上的程式碼中有一個velocityTranslate
,這個值有神馬作用,最開始我也不清楚,後面發現在滑動結束之後用到了,才明白了它代表了一個速率的位移值。什麼是速率?就好比你快速滑動的時候,總希望它能夠慣性滑動一下,這個值乘以一個慣性值就可以得到一個慣性位移。看end
中的程式碼。
end: function() { // 添加过渡 el.classList.remove('dragging'); // 惯性值 var momentumRatio = 7; var currentTranslate = translateUtil.getElementTranslate(el).top; var duration = new Date() - dragState.start; var momentumTranslate; if (duration < 300) { momentumTranslate = currentTranslate + velocityTranslate * momentumRatio; } // 加上惯性速率之后的位移值 console.log('momentumTranslate', momentumTranslate); dragRange = dragState.range; setTimeout(function() { var translate; if (momentumTranslate) { translate = Math.round(momentumTranslate / itemHeight) * itemHeight; } else { translate = Math.round(currentTranslate / itemHeight) * itemHeight; } // 取得最终的位移值, // 必须为itemHeight的倍数 // 在范围的最大值和最小值中取 translate = Math.max(Math.min(translate, dragRange[1]), dragRange[0]); translateUtil.translateElement(el, null, translate); // 计算得出当前位移下应该对应的实际值 currentValue = translate2Value(translate); // 3d效果 if (rotateEffect) { planUpdateRotate(); } }, 10); dragState = {};}
這就是整個行動端效果中Picke的實作方法
的實作流程,撇開3d
效果就可以使用了。下面來看看如何實現的3D
效果。在doOnValuesChange
中有一個最開始的初始化。
[].forEach.call(items, function(item, index) { translateUtil.translateElement(item, null, itemHeight * index);});
給每一個item
設定了根據索引來的位移值,此時的每一個item
的定位都必須是absolute
的,這樣位移下來才是緊挨著的,不然可能中間都會有一個itemHeight
的空格。
3D
效果中最關鍵的一點就是如何進行翻轉角度的計算。在原始碼中定義了一個常數物件:
var VISIBEL_ITEMS_ANGLE_MAP = { 3: -45, 5: -20, 7: -15};
可以看到,當只有3個可見元素的時候,高亮部分相對於X
軸平行,而上一個item
就必須繞X
軸順時針旋轉45度,反之下一個item
繞X
軸逆時針旋轉45度。另外在其中有一段程式碼特別繞,根據我的理解是這樣的:
// 当前item相对于顶部原本应该有的位移值var itemOffsetTop = index * itemHeight; // 滑动过程中,相对于最开始的位置滑动的位移值var translateOffset = dragRange[1] - currentTranslate;// 当应该有的位移值和滑动的位移值相等的时候,也就说明了当前的`item`被选中// 也就是说此时当前的角度为0var itemOffset = itemOffsetTop - translateOffset;var percentage = itemOffset / itemHeight;var angle = angleUnit * percentage;if (angle > 180) angle = 180;if (angle < -180) angle = -180;rotateElement(item, angle);
如果覺得太繞,其實也沒有必要按照他的這種做法來,我們只要想辦法確定每一個item
相對於目前選取的item
是處於上一個還是下一個,就可以根據此來計算角度。
關於餓了麼中的行動端效果中Picke的實作方法
元件就看了這麼多,整體來說跟swiper
中的滑動十分相似,其中的關鍵點在於最後的計算位移值來根據位移值滑動到正確的位置,至於怎麼計算值,其實每個人的實現方式可能都是大同小異的,也沒要必要一定要按照源碼來,可以適當地加入自己的理解,這樣或許寫起程式碼來更加得心應手。這裡只是個人的一點理解,希望能夠給自己也能提供給大家一點幫助。
以上是行動端效果中Picke的實作方法的詳細內容。更多資訊請關注PHP中文網其他相關文章!