摘要:支持則認為是移動端,否則為端聲明事件函數端和移動端這個函數是通用的。滑塊的移動應該依據現在的位置計算。
本文轉載自blog
轉載請注明出處
目錄前言
基本原理
html結構
實踐
小結
前言移動端,滑動是很常見的需求。很多同學都用過swiper.js,本文從原理出發,實踐出一個類swiper的滑動小插件ice-skating。
小插件的例子:
移動端
pc端
在寫代碼的過程中產生的一些思考:
滑動的原理是什么
怎么判斷動畫完成
事件綁定到哪個元素,可否使用事件委托優化
pc端和移動端滑動有何不同
正在進行的動畫觸摸時怎么取得當前樣式
如何實現輪播
基本原理滑動就是用transform: translate(x,y)或者transform: translate3d(x,y,z)去控制元素的移動,在松手的時候判定元素最后的位置,元素的樣式應用transform: translate3d(endx , endy, 0)和transition-duration: time來達到一個動畫恢復的效果。標準瀏覽器提供transitionend事件監聽動畫結束,在結束時將動畫時間歸零。
Note: 這里不討論非標準瀏覽器的實現,對于不支持transform和transition的瀏覽器,可以使用position: absolute配合left和top進行移動,然后用基于時間的動畫的算法來模擬動畫效果。
html結構舉例一個基本的結構:
//example
transform: translate3d(x,y,z)就是應用在className為ice-slide的元素上。這里不展示css代碼,可以在ice-skating的example文件中里查看完整的css。css代碼并不是唯一的,簡單說只要實現下圖的結構就可以。
從圖中可以直觀的看出,移動的是綠色的元素。className為ice-slide的元素的寬乘于當前索引(offsetWidth * index),就是每次穩定時的偏移量。例如最開始transform: translate3d(offsetWidth * 0, 0, 0),切換到slide2后,transform: translate3d(offsetWidth * 1, 0, 0),大致就是這樣的過程。
實踐源碼位于ice-skating的dist/iceSkating.js。我給插件起名叫ice-skating,希望它像在冰面一樣順暢^_^
兼容各模塊標準的容器以前我們會將代碼包裹在一個簡單的匿名函數里,現在需要加一些額外的代碼來兼容各種模塊標準。
(function (global, factory) { typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define(["exports"], factory) : (factory((global))); }(this, (function (exports) { "use strict"; })));狀態容器
用兩個對象來存儲信息
一個頁面可以實例化很多滑動對象,mainStore存儲的是每個對象的信息,比如寬高,配置參數之類的。
state存儲的是觸摸之類的臨時信息,每次觸摸后都會清空。
var mainStore = Object.create(null); var state = Object.create(null);
Object.create(null)創建的對象不會帶有Object.prototype上的方法,因為我們不需要它們,例如toString、valueOf、hasOwnProperty之類的。
構造函數function iceSkating(option){ if (!(this instanceof iceSkating)) return new iceSkating(option); } iceSkating.prototype = { }
if (!(this instanceof iceSkating)) return new iceSkating(option);很多庫和框架都有這句,簡單說就是不用new生成也可以生成實例。
觸摸事件對于觸摸事件,在移動端,我們會用touchEvent,在pc端,我們則用mouseEvent。所以我們需要檢測支持什么事件。
iceSkating.prototype = { support: { touch: (function(){ return !!(("ontouchstart" in window) || window.DocumentTouch && document instanceof DocumentTouch); })() }
支持touch則認為是移動端,否則為pc端
var events = ic.support.touch ? ["touchstart", "touchmove", "touchend"]:["mousedown","mousemove","mouseup"];聲明事件函數
pc端和移動端這3個函數是通用的。
var touchStart = function(e){}; var touchMove = function(e){}; var touchEnd = function(e){};初始化事件
var ic = this; var initEvent = function(){ var events = ic.support.touch ? ["touchstart", "touchmove", "touchend"]: ["mousedown","mousemove","mouseup"]; var transitionEndEvents = ["webkitTransitionEnd", "transitionend", "oTransitionEnd", "MSTransitionEnd", "msTransitionEnd"]; for (var i = 0; i < transitionEndEvents.length; i++) { ic.addEvent(container, transitionEndEvents[i], transitionDurationEndFn, false); } ic.addEvent(container, events[0], touchStart, false); //默認阻止容器元素的click事件 if(ic.store.preventClicks) ic.addEvent(container, "click", ic.preventClicks, false); if(!isInit){ ic.addEvent(document, events[1], touchMove, false); ic.addEvent(document, events[2], touchEnd, false); isInit = true; } };
touchStart和transitionDurationEndFn函數每個實例的容器都會綁定,但是所有實例共用touchMove和touchEnd函數,它們只綁定在document,并且只會綁定一次。使用事件委托有兩個好處:
減少了元素綁定的事件數,提高了性能。
如果將touchMove和touchEnd也綁定在容器元素上,當鼠標移出容器元素時,我們會“失去控制”。在document上意味著可以“掌控全局”。
過程分析不會把封裝的函數的代碼都一一列出來,但會說明它的作用。
觸碰瞬間 touchStart函數:會在觸碰的第一時間調用,基本都在初始化state的信息
var touchStart = function(e){ //mouse事件會提供which值, e.which為3時表示按下鼠標右鍵,鼠標右鍵會觸發mouseup,但右鍵不允許移動滑塊 if (!ic.support.touch && "which" in e && e.which === 3) return; //獲取起始坐標。TouchEvent使用e.targetTouches[0].pageX,MouseEvent使用e.pageX。 state.startX = e.type === "touchstart" ? e.targetTouches[0].pageX : e.pageX; state.startY = e.type === "touchstart" ? e.targetTouches[0].pageY : e.pageY; //時間戳 state.startTime = e.timeStamp; //綁定事件的元素 state.currentTarget = e.currentTarget; state.id = e.currentTarget.id; //觸發事件的元素 state.target = e.target; //獲取當前滑塊的參數信息 state.currStore = mainStore[e.currentTarget.id]; //state的touchStart 、touchMove、touchEnd代表是否進入該函數 state.touchEnd = state.touchMove = false; state.touchStart = true; //表示滑塊移動的距離 state.diffX = state.diffY = 0; //動畫運行時的坐標與動畫運行前的坐標差值 state.animatingX = state.animatingY = 0; };移動
在移動滑塊時,可能滑塊正在動畫中,這是需要考慮一種特殊情況。滑塊的移動應該依據現在的位置計算。
如何知道動畫運行中的信息呢,可以使用window.getComputedStyle(element, [pseudoElt]),它返回的樣式是一個實時的 CSSStyleDeclaration 對象。用它取transform的值會返回一個 2D 變換矩陣,像這樣matrix(1, 0, 0, 1, -414.001, 0),最后兩位就是x,y值。
簡單封裝一下,就可以取得當前動畫translate的x,y值了。
var getTranslate = function(el){ var curStyle = window.getComputedStyle(el); var curTransform = curStyle.transform || curStyle.webkitTransform; var x,y; x = y = 0; curTransform = curTransform.split(", "); if (curTransform.length === 6) { x = parseInt(curTransform[4], 10); y = parseInt(curTransform[5], 10); } return {"x": x,"y": y}; };touchMove函數:
移動時會持續調用,如果只是點擊操作,不會觸發touchMove。
var touchMove = function(e){ // 1. 如果當前觸發touchMove的元素和觸發touchStart的元素不一致,不允許滑動。 // 2. 執行touchMove時,需保證touchStart已執行,且touchEnd未執行。 if(e.target !== state.target || state.touchEnd || !state.touchStart) return; state.touchMove = true; //取得當前坐標 var currentX = e.type === "touchmove" ? e.targetTouches[0].pageX : e.pageX; var currentY = e.type === "touchmove" ? e.targetTouches[0].pageY : e.pageY; var currStore = state.currStore; //觸摸時如果動畫正在運行 if(currStore.animating){ // 取得當前元素translate的信息 var animationTranslate = getTranslate(state.currentTarget); //計算動畫的偏移量,currStore.translateX和currStore.translateY表示的是滑塊最近一次穩定時的translate值 state.animatingX = animationTranslate.x - currStore.translateX; state.animatingY = animationTranslate.y - currStore.translateY; currStore.animating = false; //移除動畫時間 removeTransitionDuration(currStore.container); } //如果輪播進行中,將定時器清除 if(currStore.autoPlayID !== null){ clearTimeout(currStore.autoPlayID); currStore.autoPlayID = null; } //判斷移動方向是水平還是垂直 if(currStore.direction === "x"){ //currStore.touchRatio是移動系數 state.diffX = Math.round((currentX - state.startX) * currStore.touchRatio); //移動元素 translate(currStore.container, state.animatingX + state.diffX + state.currStore.translateX, 0, 0); }else{ state.diffY = Math.round((currentY - state.startY) * state.currStore.touchRatio); translate(currStore.container, 0, state.animatingY + state.diffY + state.currStore.translateY, 0); } };translate函數:
如果支持translate3d,會優先使用它,translate3d會提供硬件加速。有興趣可以看看這篇blog兩張圖解釋CSS動畫的性能
var translate = function(ele, x, y, z){ if (ic.support.transforms3d){ transform(ele, "translate3d(" + x + "px, " + y + "px, " + z + "px)"); } else { transform(ele, "translate(" + x + "px, " + y + "px)"); } };觸摸結束 touchEnd函數:
在觸摸結束時調用。
var touchEnd = function(e){ state.touchEnd = true; if(!state.touchStart) return; var fastClick ; var currStore = state.currStore; //如果整個觸摸過程時間小于fastClickTime,會認為此次操作是點擊。但默認是屏蔽了容器的click事件的,所以提供一個clickCallback參數,會在點擊操作時調用。 if(fastClick = (e.timeStamp - state.startTime) < currStore.fastClickTime && !state.touchMove && typeof currStore.clickCallback === "function"){ currStore.clickCallback(); } if(!state.touchMove) return; //如果移動距離沒達到切換頁的臨界值,則讓它恢復到最近的一次穩定狀態 if(fastClick || (Math.abs(state.diffX) < currStore.limitDisX && Math.abs(state.diffY) < currStore.limitDisY)){ //在transitionend事件綁定的函數中判定是否重啟輪播,但是如果transform前后兩次的值一樣時,不會觸發transitionend事件,所以在這里判定是否重啟輪播 if(state.diffX === 0 && state.diffY === 0 && currStore.autoPlay) autoPlay(currStore); //恢復到最近的一次穩定狀態 recover(currStore, currStore.translateX, currStore.translateY, 0); }else{ //位移滿足切換 if(state.diffX > 0 || state.diffY > 0) { //切換到上一個滑塊 moveTo(currStore, currStore.index - 1); }else{ //切換到下一個滑塊 moveTo(currStore, currStore.index + 1); } } };transitionDurationEndFn函數:
動畫執行完成后調用
var transitionDurationEndFn = function(){ //將動畫狀態設置為false ic.store.animating = false; //執行自定義的iceEndCallBack函數 if(typeof ic.store.iceEndCallBack === "function") ic.store.iceEndCallBack(); //將動畫時間歸零 transitionDuration(container, 0); //清空state if(ic.store.id === state.id) state = Object.create(null); };
至此,一個完整的滑動過程結束。
實現輪播第一時間想到的是使用setInterval或者遞歸setTimeout實現輪播,但這樣做并不優雅。
事件循環(EventLoop)中setTimeout或setInterval會放入macrotask 隊列中,里面的函數會放入microtask,當這個 macrotask 執行結束后所有可用的 microtask 將會在同一個事件循環中執行。
我們極端的假設setInterval設定為200ms,動畫時間設為1000ms。每隔200ms, macrotask 隊列中就會插入setInterval,但我們的動畫此時沒有完成,所以用setInterval或者遞歸setTimeout的輪播在這種情況下是有問題的。
最佳思路是在每次動畫結束后再將輪播開啟。
//動畫結束執行的函數: var transitionDurationEndFn = function(){ ... //檢測是否開啟輪播 if(ic.store.autoPlay) autoPlay(ic.store); };
輪播函數也相當簡單
var autoPlay = function(store){ store.autoPlayID = setTimeout(function(){ //當前滑塊的索引 var index = store.index; ++index; //到最后一個了,重置為0 if(index === store.childLength){ index = 0; } //移動 moveTo(store, index); },store.autoplayDelay); };小結
本文記錄了我思考的過程,代碼應該還有很多地方值得完善。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/82350.html
摘要:支持則認為是移動端,否則為端聲明事件函數端和移動端這個函數是通用的。滑塊的移動應該依據現在的位置計算。 本文轉載自blog 轉載請注明出處 目錄 前言 基本原理 html結構 實踐 小結 前言 移動端,滑動是很常見的需求。很多同學都用過swiper.js,本文從原理出發,實踐出一個類swiper的滑動小插件ice-skating。 小插件的例子: 移動端 pc端 在寫代碼的過程...
摘要:支持則認為是移動端,否則為端聲明事件函數端和移動端這個函數是通用的。滑塊的移動應該依據現在的位置計算。 本文轉載自blog 轉載請注明出處 目錄 前言 基本原理 html結構 實踐 小結 前言 移動端,滑動是很常見的需求。很多同學都用過swiper.js,本文從原理出發,實踐出一個類swiper的滑動小插件ice-skating。 小插件的例子: 移動端 pc端 在寫代碼的過程...
摘要:讓你收獲滿滿碼個蛋從年月日推送第篇文章一年過去了已累積推文近篇文章,本文為年度精選,共計篇,按照類別整理便于讀者主題閱讀。本篇文章是今年的最后一篇技術文章,為了讓大家在家也能好好學習,特此花了幾個小時整理了這些文章。 showImg(https://segmentfault.com/img/remote/1460000013241596); 讓你收獲滿滿! 碼個蛋從2017年02月20...
摘要:緊接著就是導航欄的特效編寫,殊不知,就是這個效果,讓原本計劃上午完成的事情,愣是被我研究了大半天才解決。剛開始我的布局是,導航欄是一個,下面有八個,分別是八個欄目。 showImg(https://segmentfault.com/img/bVYUar?w=720&h=537); 前言 今天這篇文章的標題,顯然是要搞事情。一個JS交互效果,居然花費了一天的寶貴時間才研究出來,我是不是不...
閱讀 1378·2021-11-24 09:38
閱讀 2086·2021-09-22 15:17
閱讀 2340·2021-09-04 16:41
閱讀 3452·2019-08-30 15:56
閱讀 3511·2019-08-29 17:19
閱讀 1939·2019-08-28 18:09
閱讀 1249·2019-08-26 13:35
閱讀 1711·2019-08-23 17:52