移動端 Touch 滑動反彈
什麼是
Touch
滑動?就是類似於PC
端的滾動事件,但是在移動端是沒有滾動事件的,所以就要用到Touch
事件結合js
去實現,效果如下:
1. 準備工作
什麼是移動端的
Touch
事件?在移動端Touch
事件可以細分成三種,分別是:touchstart
、touchmove
和touchend
,並且touch
事件必須要用addEventListener
去監聽。
touchStart
當手指觸碰到螢幕的時候觸發touchmove
當手指在螢幕上不斷移動的時候觸發touchend
當手指離開螢幕的時候觸發
Touch 事件觸發的 Event 物件:
1 2 3 4 5 |
// 手指觸碰到螢幕時觸發 element.addEventListener('touchstart', function (e) { // 列印的事件物件 console.log(e); }) |
changedTouches
、targetTouches
、touches
都是偽陣列,裡面裝的是手指列表
三種返回物件的區別:
其實這三種返回的物件,都是表示使用者觸控事件時的手指資訊,之所以是一個偽陣列,是因為有可能出現多指同時觸控,但是在實際工作中一般不去考慮多指的情況。而它們唯一的區別就是在
touchstart
和touchmove
事件的時候,changedTouches
、targetTouches
、touches
都能獲取到手指的資訊,但是在touchend
事件的時候,targetTouches
、touches
物件是不能返回離開螢幕時的手指資訊,只有changedTouches
物件能返回。
有哪些手指資訊?
我們可以看下上面的圖片,在
changedTouche[0]
中,有些值:
1 2 3 4 5 6 |
clientX:74 // 觸控點相對於瀏覽器的 viewport 左邊緣的 x 座標,不會包括左邊的滾動距離。 clientY:73 // 觸控點相對於瀏覽器的 viewport 上邊緣的 Y 座標,不會包括上邊的滾動距離。 screenX:2202 // 觸控點相對於螢幕左邊緣的 x 座標。 screenY:327 // 觸控點相對於螢幕上邊緣的 Y 座標。 pageX:65 // 觸控點相對於 document 的左邊緣的 x 座標,包括左邊的滾動距離 pageY:18 // 觸控點相對於 document 的上邊緣的 Y 座標,包括左邊的滾動距離 |
2. 基本結構
此案例模擬的是移動端的一種滑動選單效果。
HTML部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<aside class="main"> <div class="draw" id="draw"> <ul> <li style="background:orange">列表一</li> <li style="background:yellowgreen">列表二</li> <li style="background:yellow">列表三</li> <li style="background:cyan">列表四</li> <li style="background:orangered">列表五</li> <li style="background:pink">列表六</li> <li style="background:red">列表七</li> <li style="background:purple">列表八</li> <li style="background:violet">列表九</li> <li style="background:brown">列表十</li> </ul> </div> </aside> |
css部分:
在列表的父盒子上設定一個
overflow: hidden
屬性,使超出盒子部分的列表暫時隱藏掉,後面會通過js
去實現滑動。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
/* 樣式初始化 */ * { margin: 0; padding: 0; } html, body { width: 100%; } aside { height: 100%; width: 100%; } /* 列表的父盒子,限制寬高 */ /* 注意設定overflow: hidden;樣式後,超出這個盒子的ul將不會顯示 */ .draw { width: 60px; height: 500px; border: 2px solid #ccc; overflow: hidden; position: fixed; left: 10px; top: 50%; transform: translateY(-50%); } /* li 設定了浮動, 所以 ul 要清除浮動 */ ul:after { content: ""; display: block; visibility: hidden; height: 0; clear: both; } ul { zoom: 1; } li { list-style: none; float: left; width: 60px; height: 60px; line-height: 60px; text-align: center; } |
效果圖:
3. 首次滑動
手指觸控到列表向下滑動的時候,列表應該跟著向下滑動,當手指離開螢幕的時候,列表應該停在滑動的位置。這裡就會用到上面準備階段的知識點了,不明白的可以參考上面的概念。
實現原理:
- 1、
touchstart
的時候,獲取手指觸控的落點A
,通過這個點物件裡面的clientY
屬性,獲取距離頂部可視區的距離; - 2、
touchmove
的時候,獲取手指的點B
,同樣的獲取移動時距離頂部可視區的距離; - 3、
touchmove
的時候,還要做另一件事情,就是獲取兩點的差值(B.clientY-A.clientY
),將這個差值動態賦值給ul
,ul
只需要設定向Y
軸方向偏移這個距離,就能實現列表隨手指滑動
先來張示意圖,怎麼通過 js 讓列表滑動起來:
示例程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
var draw = document.querySelector('#draw'); var ul = draw.children[0]; // touchstart 時,記錄手指在 Y 軸上的落點距離可視頂部距離 var startY = 0 ul.addEventListener('touchstart', function (e) { startY = e.changedTouches[0].clientY; }) // touchmove 時,記錄此時手指在 Y 軸上的落點距離可視頂部距離 ul.addEventListener('touchmove', function (e) { // 獲取差值 var dy = e.changedTouches[0].clientY - startY; // 設定 ul 在 Y 軸上的偏移 ul.style.transform = 'translateY(' + dy + 'px)'; }) |
效果圖:
4. 再次滑動
上面的效果圖,細心的朋友可能已經發現了問題,在第一次的時候,得到了我們想要的效果,但是在第二次的時候,我們繼續向下移動了一段距離,但是
ul
並沒有接著第一次的位置繼續向下,而是瞬間跳了上去。
問題分析:
雖然第二次是繼續向下移動了一段距離,但是觸控結束後,最終是將此時的差值,重新賦值給了
ul
的Y
軸偏移,所以視覺上“跳了上去”。
解決方法:
每一次滑動結束之後,都應該記錄下此次滑動的距離,與之前的進行累加,待下一次滑動的時候,
ul
在Y
軸的偏移值應該是之前的距離加上本次滑動的距離。
- 新增
touchend
事件,在該事件裡同樣的可以獲取到本次滑動的距離,將它與上一次的距離相加,賦值給一個全域性變數; - 在
touchmove
事件裡有點小改動,就是在給ul
設定偏移值的時候,除了本次滑動的差值還要加上這個上一次的值;
示意圖:
示例程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
var draw = document.querySelector('#draw'); var ul = draw.children[0]; var startY = 0 // 剛觸碰到螢幕的時的手指資訊 var centerY = 0 // 用來記錄每次觸控時上一次的偏移距離 // touchstart 時,記錄手指在 Y 軸上的落點距離可視頂部距離 ul.addEventListener('touchstart', function (e) { startY = e.changedTouches[0].clientY; }) // touchmove 時,記錄此時手指在 Y 軸上的落點距離可視頂部距離 ul.addEventListener('touchmove', function (e) { // 獲取差值 var dy = e.changedTouches[0].clientY - startY; // 上次的滑動距離加上本次的滑動距離 var tempY = centerY + dy; // 設定 ul 在 Y 軸上的偏移 ul.style.transform = 'translateY(' + tempY + 'px)'; }) // touchend 時,記錄此時手指在 Y 軸上的落點距離可視頂部距離 ul.addEventListener('touchend', function (e) { // 獲取差值 var dy = e.changedTouches[0].clientY - startY; // 記錄移動的距離 centerY = centerY + dy; }) |
效果圖:
5. 限制滑動區間
到上面一步,我們已經可以實現列表的滑動了,但是也存在一個問題,就是向上或者向下的時候沒有限制,上下可以無限的滑動,甚至再用點力,就看不到列表了。為了美觀和實用,這樣肯定不行的,需要給它設定一個區間,設定向上或者向下最多隻能留白多少。
限制向下滑動最大區間:
設定向下最大區間的值比較簡單,直接設定一個值,當上一次滑動的距離加上本次滑動的距離大於這個值的時候,就不讓它再繼續往下滑了,讓他直接等於這個設定的值。
示例程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
var maxDown = 50; // 設定一個最大向下滑動的距離 // touchmove 時,記錄此時手指在 Y 軸上的落點距離可視頂部距離 ul.addEventListener('touchmove', function (e) { // 獲取差值 var dy = e.changedTouches[0].clientY - startY; // 上次的滑動距離加上本次的滑動距離 var tempY = centerY + dy; // 當上次滑動的距離加上本次滑動的距離 大於 設定的最大向下距離的時候 if (tempY > maxDown) { // 直接讓偏移的值 等於這個設定值 tempY = maxDown; } // 設定 ul 在 Y 軸上的偏移 ul.style.transform = 'translateY(' + tempY + 'px)'; }) |
限制向上滑動最大區間:
向上滑動時,當
ul
的底部距盒子底部的距離大於設定值的時候,不讓其繼續向上滑動,關鍵是這個值怎麼去判斷?
求出向上滑動最大值:
注意:因為ul
是向上滑動的,所以求得的距離前面要加上一個負號(-
)
示例程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// 設定一個最大向下滑動的距離 var maxDown = 50; // 求得一個最大向上滑動的距離 var maxUp = -(ul.offsetHeight - draw.offsetHeight + maxDown); // touchmove 時,記錄此時手指在 Y 軸上的落點距離可視頂部距離 ul.addEventListener('touchmove', function (e) { // 獲取差值 var dy = e.changedTouches[0].clientY - startY; // 上次的滑動距離加上本次的滑動距離 var tempY = centerY + dy; // 當上次滑動的距離加上本次滑動的距離 大於 設定的最大向下距離的時候 if (tempY > maxDown) { tempY = maxDown; } // 當上次滑動的距離加上本次滑動的距離 小於 設定的最大向上距離的時候 else if (tempY < maxUp) { // 直接讓偏移的值 等於這個設定值 tempY = maxUp; } // 設定 ul 在 Y 軸上的偏移 ul.style.transform = 'translateY(' + tempY + 'px)'; }) |
效果圖:
認真觀察上圖,雖然成功的設定了最大滑動區間,但是你有沒有發現,一直往一個方向滑動的時候,雖然列表不會繼續往下滑動,但是接著往相反方向滑動的時候,感覺列表滑不動,需要滑一段距離後,列表才會跟著走,這是為什麼呢?因為滑動的過程centerY
是一直變的,列表雖然視覺上不動了,但是在touchend
事件的時候,它的centerY
值一直在累加。解決方法請往下看:
6. 設定反彈區間
“滑動反彈”,這裡的反彈是本篇文章的最後一步,上面說到的問題,就在這裡解決。因為每一次觸發
touchend
事件的時候,centerY
值就累加一次,所以需要在touchend
事件裡做判斷。我們設定一個反彈區間,就是當centerY
的值大於或者小於某個值的時候,讓它觸發反彈。
設定向上反彈值:
向上的值比較簡單,設定成“
0
”。為什麼是“0
”呢?我們限定只要手指離開時,上一次的滑動距離加上本次的距離> 0
的時候,就讓它觸發反彈,並且反彈回0
點的位置,也就是兩次滑動的距離和= 0
。
示例程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// 向上反彈 var maxUpBounce = 0; // touchend 時,記錄此時手指在 Y 軸上的落點距離可視頂部距離 ul.addEventListener('touchend', function (e) { // 獲取差值 var dy = e.changedTouches[0].clientY - startY; // 記錄移動的距離 centerY = centerY + dy; // 兩次滑動的距離 大於 設定的 向上 反彈值時 if (centerY > maxUpBounce) { // 讓兩次滑動的距離 等於 設定的值 centerY = maxUpBounce; // 新增過渡 ul.style.transition = 'transform .5s'; ul.style.transform = 'translateY(' + centerY + 'px)'; } }) |
設定向下反彈值:
向下的值其實跟之前求滑動區間差不多,我們參考下圖,當列表向上滑動,滑動到列表底部的時候,只要此時再向上滑動,就讓它向下反彈。向下反彈值就是
-(ul.offsetHeight - draw.offsetHeight)
,只要滑動的差值小於這個設定值,就讓它向下反彈,並且反彈回設定值的位置。
示例程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
// 向上反彈值 var maxUpBounce = 0; // 向下反彈值 var maxDownBounce = -(ul.offsetHeight - draw.offsetHeight); // touchend 時,記錄此時手指在 Y 軸上的落點距離可視頂部距離 ul.addEventListener('touchend', function (e) { // 獲取差值 var dy = e.changedTouches[0].clientY - startY; // 記錄移動的距離 centerY = centerY + dy; // 兩次滑動的距離 大於 設定的 向上 反彈值時 if (centerY > maxUpBounce) { // 讓兩次滑動的距離 等於 設定的值 centerY = maxUpBounce; // 新增過渡 ul.style.transition = 'transform .5s'; ul.style.transform = 'translateY(' + centerY + 'px)'; } // 兩次滑動的距離 小於 設定的 向下 反彈值時 else if (centerY < maxDownBounce) { // 讓兩次滑動的距離 等於 設定的值 centerY = maxDownBounce; // 新增過渡 ul.style.transition = 'transform .5s'; ul.style.transform = 'translateY(' + centerY + 'px)'; } }) |
注意: 在touchend
事件的時候,給列表新增了transition
屬性才會有反彈的效果,但是,下一次滑動的時候,touchmove
事件的時候,這個屬性還存在,所以就會出現滑動的時候有頓挫感,所以在touchmove
事件的時候,一進來就清一下過渡ul.style.transition = 'none';
。
完成後效果圖:
7. 完整程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>移動端 Touch 滑動反彈</title> <style> /* 樣式初始化 */ * { margin: 0; padding: 0; } html, body { width: 100%; } aside { height: 100%; width: 100%; } /* 列表的父盒子,限制寬高 */ /* 注意設定overflow: hidden;樣式後,超出這個盒子的ul將不會顯示 */ .draw { width: 60px; height: 500px; border: 2px solid #ccc; overflow: hidden; position: fixed; left: 10px; top: 50%; transform: translateY(-50%); } /* li 設定了浮動, 所以 ul 要清除浮動 */ ul:after { content: ""; display: block; visibility: hidden; height: 0; clear: both; } ul { zoom: 1; } li { list-style: none; float: left; width: 60px; height: 60px; line-height: 60px; text-align: center; } </style> </head> <aside class="main"> <div class="draw" id="draw"> <ul> <li style="background:orange">列表一</li> <li style="background:yellowgreen">列表二</li> <li style="background:yellow">列表三</li> <li style="background:cyan">列表四</li> <li style="background:orangered">列表五</li> <li style="background:pink">列表六</li> <li style="background:red">列表七</li> <li style="background:purple">列表八</li> <li style="background:violet">列表九</li> <li style="background:brown">列表十</li> </ul> </div> </aside> <body> <script> var draw = document.querySelector('#draw'); var ul = draw.children[0]; var startY = 0; // 剛觸碰到螢幕的時的手指資訊 var centerY = 0; // 用來記錄每次觸控時上一次的偏移距離 var maxDown = 50; // 設定一個最大向下滑動的距離 var maxUp = -(ul.offsetHeight - draw.offsetHeight + maxDown); // 求得一個最大向上滑動的距離 var maxUpBounce = 0; // 向上反彈值 var maxDownBounce = -(ul.offsetHeight - draw.offsetHeight); // 向下反彈值 // touchstart 時,記錄手指在 Y 軸上的落點距離可視頂部距離 ul.addEventListener('touchstart', function (e) { startY = e.changedTouches[0].clientY; }) // touchmove 時,記錄此時手指在 Y 軸上的落點距離可視頂部距離 ul.addEventListener('touchmove', function (e) { // 清除過渡 ul.style.transition = 'none'; // 獲取差值 var dy = e.changedTouches[0].clientY - startY; // 上次的滑動距離加上本次的滑動距離 var tempY = centerY + dy; // 當上次滑動的距離加上本次滑動的距離 大於 設定的最大向下距離的時候 if (tempY > maxDown) { tempY = maxDown; } // 當上次滑動的距離加上本次滑動的距離 小於 設定的最大向上距離的時候 else if (tempY < maxUp) { tempY = maxUp; } // 設定 ul 在 Y 軸上的偏移 ul.style.transform = 'translateY(' + tempY + 'px)'; }) // touchend 時,記錄此時手指在 Y 軸上的落點距離可視頂部距離 ul.addEventListener('touchend', function (e) { // 獲取差值 var dy = e.changedTouches[0].clientY - startY; // 記錄移動的距離 centerY = centerY + dy; // 兩次滑動的距離 大於 設定的 向上 反彈值時 if (centerY > maxUpBounce) { // 讓兩次滑動的距離 等於 設定的值 centerY = maxUpBounce; // 新增過渡 ul.style.transition = 'transform .5s'; ul.style.transform = 'translateY(' + centerY + 'px)'; } // 兩次滑動的距離 小於 設定的 向下 反彈值時 else if (centerY < maxDownBounce) { // 讓兩次滑動的距離 等於 設定的值 centerY = maxDownBounce; // 新增過渡 ul.style.transition = 'transform .5s'; ul.style.transform = 'translateY(' + centerY + 'px)'; } }) </script> </body> </html> |