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