移動端 Touch 滑動反彈
什麼是Touch
滑動?就是類似於PC
端的滾動事件,但是在移動端是沒有滾動事件的,所以就要用到Touch
事件結合js
去實現,效果如下:
1. 準備工作
什麼是移動端的Touch
事件?在移動端Touch
事件可以細分成三種,分別是:touchstart
、touchmove
和touchend
,並且touch
事件必須要用addEventListener
去監聽。
-
touchStart
當手指觸碰到屏幕的時候觸發 -
touchmove
當手指在屏幕上不斷移動的時候觸發 -
touchend
當手指離開屏幕的時候觸發
Touch 事件觸發的 Event 對象:
// 手指觸碰到屏幕時觸發
element.addEventListener('touchstart', function (e) {
// 打印的事件對象
console.log(e);
})
changedTouches
、targetTouches
、touches
都是僞數組,裏面裝的是手指列表
三種返回對象的區別:
其實這三種返回的對象,都是表示用戶觸摸事件時的手指信息,之所以是一個僞數組,是因爲有可能出現多指同時觸摸,但是在實際工作中一般不去考慮多指的情況。而它們唯一的區別就是在touchstart
和touchmove
事件的時候,changedTouches
、targetTouches
、touches
都能獲取到手指的信息,但是在touchend
事件的時候,targetTouches
、touches
對象是不能返回離開屏幕時的手指信息,只有changedTouches
對象能返回。
有哪些手指信息?
我們可以看下上面的圖片,在changedTouche[0]
中,有些值:
clientX:74 // 觸摸點相對於瀏覽器的 viewport 左邊緣的 x 座標,不會包括左邊的滾動距離。
clientY:73 // 觸摸點相對於瀏覽器的 viewport 上邊緣的 Y 座標,不會包括上邊的滾動距離。
screenX:2202 // 觸摸點相對於屏幕左邊緣的 x 座標。
screenY:327 // 觸摸點相對於屏幕上邊緣的 Y 座標。
pageX:65 // 觸摸點相對於 document 的左邊緣的 x 座標,包括左邊的滾動距離
pageY:18 // 觸摸點相對於 document 的上邊緣的 Y 座標,包括左邊的滾動距離
2. 基本結構
此案例模擬的是移動端的一種滑動菜單效果。
HTML部分:
<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
去實現滑動。
/* 樣式初始化 */
* {
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 讓列表滑動起來:
示例代碼:
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
設置偏移值的時候,除了本次滑動的差值還要加上這個上一次的值;
示意圖:
示例代碼:
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. 限制滑動區間
到上面一步,我們已經可以實現列表的滑動了,但是也存在一個問題,就是向上或者向下的時候沒有限制,上下可以無限的滑動,甚至再用點力,就看不到列表了。爲了美觀和實用,這樣肯定不行的,需要給它設定一個區間,設定向上或者向下最多隻能留白多少。
限制向下滑動最大區間:
設定向下最大區間的值比較簡單,直接設定一個值,當上一次滑動的距離加上本次滑動的距離大於這個值的時候,就不讓它再繼續往下滑了,讓他直接等於這個設定的值。
示例代碼:
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
是向上滑動的,所以求得的距離前面要加上一個負號(-
)
示例代碼:
// 設定一個最大向下滑動的距離
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
。
示例代碼:
// 向上反彈
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)
,只要滑動的差值小於這個設定值,就讓它向下反彈,並且反彈回設定值的位置。
示例代碼:
// 向上反彈值
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. 完整代碼
<!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>