序言
本篇為一移動端博文,個人覺得這篇外文還可以,就翻譯了一下,最終實現的一個效果是:用手勢建立一個本地選單(點選一選單按鈕,實現設定一個觸控側滑選單,滑動滑出效果),主要涉及的知識點有移動端三大觸控事件(touchstart,touchmove,touchend),觸控屬性,以及實現側邊欄動畫,在處理移動端點選,拖動,滑動時,是不得要考慮使用者的觸控手勢,判斷手指在頁面上到底是點選還是滑動的,利用原生js的方法封裝點選,移動,抬起功能函式,儘管移動(手機)端與pc端有很多相似之處,但還是有很多要注意的地方的,如果你想獲得該Demo的原始碼,複製該標題在微信itclanCoder公眾號後臺回覆[手勢魅力-設定一個觸控選單]就可以下載了的,初次翻譯,如果有誤導的地方,歡迎路過的老師,多提意見和指正
最終程式碼實現效果圖所示:
前戲
觸控和手勢驅動裝置的興起,極大地改變了我們思考互動的方式。手勢不僅僅是娛樂性的,它們非常有用,也很熟悉
移動觸控手勢已成為每個應用程式的重要組成部分,大多數使用者甚至沒有意識到的一部分。誰不喜歡(流暢)的互動應用程式?
然而至今,憎恨可能伴隨的混亂和數學是非常容易的。我知道,令人震驚的是,尤其是當你不是第一塊碼程式碼的人,或者你只是在那裡維護它的時候
有時候,這可能是一個吃力不討好的工作。那種讓你用一隻手盯著螢幕,另一隻手放在你的額頭上,另一隻手放在滑鼠上滾動的時間
有 - 我敢說呢? - 如絲般流暢的手勢觸控手勢和動畫可能是一個挑戰,並隨著時間的推移變得越來越突出。但這是另一天的戰鬥。或另一篇文章。或兩者
今天,我們要告訴你如何用手勢建立一個本地選單
所以,在我轉向實際的程式碼之前,在那裡有一些我想要經歷的事情,所以請耐心等待HTML結構
<!-- layout開始 -->
<div class="layout">
<!-- 頭部開始 -->
<div class="header">
<div class="header-top">
<!-- 選單按鈕 -->
<div data-link="" class="app-menu-burger OSFillParent" href="#" id="b2-Menu">
<!-- 三條橫崗線,這裡其實完全用一小圖片或者icon字型圖示代替 -->
<div data-container="" class="app-menu-line"></div>
<div data-container="" class="app-menu-line OSAutoMarginTop"></div>
<div data-container="" class="app-menu-line OSAutoMarginTop"></div>
</div>
</div>
</div>
<!-- 頭部結束 -->
<!-- 中間部分開始 -->
<div data-container="" class="center-content">
<!-- 中間影象小圖片 -->
<div class="center-content-header ph" id="b3-Top"></div>
<div class="center-content-container ph" id="b3-Center">
<!--中間圖片 -->
<div data-container="">
<div data-container="" style="text-align: center;"><img data-image="" src="https://s11.postimg.org/409mhl043/icon.png">
</div>
<div data-container="" class="OSAutoMarginTop" style="text-align: center;">
<h1><!-- react-text: 75 -->Welcome to itclanCoder!<!-- /react-text --><br></h1>
<h3><!-- react-text: 78 -->手勢魅力:設定一個觸控側滑選單<!-- /react-text --><br><!-- react-text: 80 -->閱讀原文即可 <!-- /react-text --></h3>
</div>
</div>
</div>
<div class="center-content-bottom ph" id="b3-Bottom"></div>
</div>
<!-- 中間部分結束 -->
<!-- 左側帶單欄開始 -->
<div class="menu">
<!-- 黑色遮罩 -->
<div class="menu-background" style="cursor: pointer;"></div>
<div class="app-menu-container">
<div class="app-menu">
<!-- 側欄頂部開始 -->
<div class="top">
<!-- 側欄頂部圖片 -->
<div class="top-overlay">
<img src="https://s11.postimg.org/409mhl043/icon.png" width=80>
</div>
</div>
<!-- 側欄頂部結束 -->
<!-- 側欄列表內容開始 -->
<div class="bottom">
<!-- 歡迎關注微信itclanCoder公眾號,個人微訊號:suibichuanji -->
<div class="list-item">Welcome itclanCoder</div>
<div class="list-item">Fancy Time</div>
<div class="list-item">Tea Time</div>
<div class="list-item">Adventure Time</div>
<div class="list-item">Puzzle Time</div>
<div class="list-item">Sports Time</div>
<div class="list-item">Star Wars Time</div>
<div class="list-item">Internet Time</div>
<div class="list-item">Sushi Time</div>
<div class="list-item">weChatPublicId:itclanCoder</div>
<div class="list-item">personId:suibichuanji</div>
</div>
<!-- 側欄列表內容 -->
</div>
</div>
</div>
<!-- 左側選單欄結束 -->
</div>
<!-- layout 結束 -->
複製程式碼
所有你需要了解的JavaScript觸控事件
我將使用JavaScript事件來檢測我的移動觸控手勢。在這種情況下在那裡是:
touchstart
:當你觸控DOM元素時觸發。touchmove
:當你沿著DOM元素拖動手指時觸發touchend
:當你從DOM元素中移除手指時觸發
在這些事件中,我將使用觸控屬性(在那裡還有兩個屬性,但這是我現在關心的)。觸控屬性列出當前在螢幕上的所有手指:
PageX
:返回手指放置在DOM中的x座標。從左邊開始計算,如果適用,則考慮水平滾動PageY
:返回手指放置在DOM中的y座標。它是從頂部邊緣測量的,並考慮垂直滾動(如果適用)
而你也需要知道關於requestAnimationFrame
requestAnimationFrame
函式告訴瀏覽器你要執行一個動畫。它要求瀏覽器呼叫指定的函式,在下一次重繪之前更新動畫。這有什麼好處呢
- 瀏覽器將嘗試匹配顯示重新整理,以允許流暢的動畫
- 非活動選項卡中的動畫將停止(在CPU上花費的更少)
- 它不會耗盡你的電池壽命
拖動,點選和滑動:額外的東西要考慮移動觸控手勢
這些事件需要能夠檢測和區分拖拽,點選和移動,並相應地做不同的事情。所以,當你玩手機觸控手勢,想想:
- 限制:你想要什麼元素停止?您希望它在每次拖動時移動多遠?
- 這個手勢的方向:你想只能水平移動,或者還是垂直移動?也許是兩個?
- 拖動完成後你想要發生什麼?它會回到開始還是結束,取決於它在哪裡結束?它是否考慮到速度?
- 詳情:我們是否正在用這個手勢記住速度?你想在選單後面加一個遮罩,當你開啟它時會變得越來越暗嗎?
在我的情況下,我只希望手勢的方向是水平的,因為我希望滾動功能正常。我有限制,並且我希望它回到開始或結束。這取決於使用者拖動了多少以及手指在螢幕上的速度
你不知道你想知道的關於 - 是超級重要的部分
我知道你想要了解移動觸控手勢的有趣部分,但是我必須先介紹這一點,因為它會影響到你的程式碼。是的,現在是討論變數的時候了。這好訊息是,我也要解釋為什麼要設定它們的價值。這些功能將使程式碼看起來更清潔
全域性變數和設定預設值
啊,是如此的好玩!看看所需要的變數數量;正是大多數人傾向於跳過的東西。(不要,你會後悔的)
var trackableElement;
var menu = document.querySelector(".menu");
var appMenu = document.querySelector(".app-menu-container");
var overlay = document.querySelector(".menu-background");
var burger = document.querySelector(".app-menu-burger");
複製程式碼
var touchingElement = false;
var startTime;
var startX = 0,
startY = 0;
var currentX = 0,
currentY = 0;
var isOpen = false;
var isMoving = false;
var menuWidth = 0;
var lastX = 0;
var lastY = 0;
var moveX = 0; // where in the screen is the menu currently
var dragDirection = "";
var maxOpacity = 0.5;
// if you want to change this, don’t forget to change the opacity value
// of the ‘.menu--visible .menu-background’ CSS class
var init = function(element, start, move, end) {
trackableElement = element;
startTime = new Date().getTime(); // start time of the touch
addEventListeners();
}
var addEventListeners = function() {
trackableElement.addEventListener("touchstart", onTouchStart, false);
trackableElement.addEventListener("touchmove", onTouchMove, false);
trackableElement.addEventListener("touchend", onTouchEnd, false);
overlay.addEventListener("click", closeMenuOverlay, false);
// I want to be able to click the overlay and immediately close the menu
// (in the space between the actual menu and the page behind it)
}
複製程式碼
非常簡單,真的。按照這個順序,程式碼不那麼混亂,不那麼可怕,而且更容易消化
函式中的函式
這些函式被EventListener
呼叫,即使它們不是做實際的動畫或者使選單工作所必需的計算
function onTouchStart(evt) {
startTime = new Date().getTime();
startX = evt.touches[0].pageX;
startY = evt.touches[0].pageY;
touchingElement = true;
touchStart(startX, startY);
}
function onTouchMove(evt) {
if (!touchingElement)
return;
currentX = evt.touches[0].pageX;
currentY = evt.touches[0].pageY;
const translateX = currentX - startX; // distance moved in the x axis
const translateY = currentY - startY; // distance moved in the y axis
touchMove(evt, currentX, currentY, translateX, translateY);
}
function onTouchEnd(evt) {
if (!touchingElement)
return;
touchingElement = false;
const translateX = currentX - startX; // distance moved in the x axis
const translateY = currentY - startY; // distance moved in the y axis
const timeTaken = (new Date().getTime() - startTime);
}
複製程式碼
所有這些變數都用於動畫所涉及的數學運算。為了可讀性,在函式中沒有太多的程式碼行,我把它們全部分成了小的一行
這個手機觸控手勢最後有趣的一部分
現在我對觸控事件,變數和函式的解釋已經不存在了,現在是我關注如何建立動畫的時候了。這正是選單移動以及所有數學和演算法背後的原因
動畫開始
function touchStart(startX, startY) {
var menuOpen = document.querySelector(".menu.menu--visible");
if (menuOpen !== null) {
isOpen = true;
} else {
isOpen = false;
}
menu.classList.add("no-transition");
appMenu.classList.add("no-transition");
isMoving = true;
menuWidth = document.querySelector(".app-menu").offsetWidth;
lastX = startX;
lastY = startY;
if (isOpen) {
moveX = 0;
} else {
moveX = -menuWidth;
}
dragDirection = "";
menu.classList.add("menu--background-visible");
// why is this being added? ‘.menu--background-visible .menu-background’ makes the overlay
// ‘active’, displaying it on the DOM for those sweet opacity changes.
}
複製程式碼
每次觸控螢幕時,這些程式碼都會執行。此功能將用作重置為預設值,具體取決於你上次提起手指後選單發生了什麼
動畫中間
function touchMove(evt, currentX, currentY, translateX, translateY) {
if (!dragDirection) {
if (Math.abs(translateX) >= Math.abs(translateY)) {
dragDirection = "horizontal";
} else {
dragDirection = "vertical";
}
requestAnimationFrame(updateUi);
// this is what actually does the animation
}
// ...
}
複製程式碼
你想知道的第一件事是手勢的方向
在選單中,垂直滾動真的不是什麼可以關心的東西。意思是,在與手勢相關的程式碼方面,行為本身應該是預設滾動。因此,確定當什麼時候這是需要的
if (dragDirection === "vertical") {
lastX = currentX;
lastY = currentY;
} else {
evt.preventDefault();
// ...
}
複製程式碼
沒有preventDefault
,這是會發生什麼事情:
這絕對不是你想要用你的手機觸控手勢發生的事情,所以考慮一下:當你開啟/關閉選單時,你是否有興趣閱讀滾動隱藏的內容?如果你的拖拽方向是水平的,你就不能滾動
我們需要一些邊界在這裡! (設定限制)
if (moveX + (currentX - lastX) < 0 && moveX + (currentX - lastX) > -menuWidth) {
moveX = moveX + (currentX - lastX);
// ...
}
複製程式碼
所以,記得我說我有限制嗎?在這個例子中,選單隱藏在螢幕的左邊。所以,如果選單是關閉的,變數moveX
開始為-menuWidth
- 我希望它被拖動到右邊,直到完全顯示
moveX + (currentX - lastX)
複製程式碼
你可以稱之為移動間隔。這就是告訴指令碼選單在視窗中的確切位置。我使用moveX
是因為我做了實際的動畫。轉到updateUI
函式 -`` requestAnimationFrame`呼叫的函式 - 這就是你所擁有的
function updateUi() {
if (isMoving) {
var element = document.querySelector(".app-menu-container");
element.style.transform = "translateX(" + moveX + "px)";
element.style.webkitTransform = "translateX(" + moveX + "px)";
requestAnimationFrame(updateUi);
}
}
複製程式碼
我希望動畫無縫平滑。為此,指令碼可以檢測到並用於translateX
的時間間隔越小越好。目標不是看到使用translateX
引起的跳轉
現在已經完成了,下一步就是計算疊加層的淡入效果
重疊計算
目標是:
- 當moveX = -menuWidth時,不透明度= 0
- 當movX = 0,不透明度= 0.5
然而,這些計算並不那麼線性。問題始終是打破這些情況下通常使用的三路規則的零
overlay.classList.add("no-transition");
var percentageBeforeDif = (Math.abs(moveX) * 100) / menuWidth;
var percentage = 100 - percentageBeforeDif;
複製程式碼
在這裡,我確保menuWidth
對應於100%,當前位置(moveX)對應於百分比。在這個計算中我追求的百分比是
var newOpacity = (((maxOpacity) * percentage) / 100);
複製程式碼
這個計算是需要的,因為不透明度只有在0到0.5之間(如在變數中定義的)之後才有效。如果0.5不透明度與100%相關,則百分比將是期望的不透明度
動畫結束
function touchEnd(currentX, currentY, translateX, translateY, timeTaken) {
isMoving = false;
var velocity = 0.3;
//...
}
複製程式碼
首先要記住的是,有人可以簡單地點選,事件認為這是一個摸索和touchend。如果這是一個點選,選單上沒有任何事情發生
if (translateX === 0 && translateY === 0) {
if (isOpen) {
appMenu.classList.remove("no-transition");
menu.classList.remove("no-transition");
} else {
menu.classList.remove("menu--background-visible");
menu.classList.remove("no-transition");
}
}
複製程式碼
拖動結束後會發生什麼?
- 當選單開啟時,它可以關閉或保持開啟狀態 - 與動畫一起 - 返回之前的位置
- 如果它關閉了,那麼它可以開啟或者保持關閉狀態,也可以在動畫返回之前
if ((translateX < (-menuWidth) / 2) || (Math.abs(translateX) / timeTaken > velocity)) {
// if menu is open, this represents the close condition
if (translateX > menuWidth / 2 || (Math.abs(translateX) / timeTaken > velocity)) {
// if menu is closed, this represents the open condition
複製程式碼
那麼什麼被認為足以開啟選單?五個畫素移動?那麼這個選單可以根據距離開啟或關閉。也就是說,如果將其拖過寬度的中間,並且拖動的速度大於定義的速度(也就是若手指拖動側邊欄超過該選單欄的一半位置的話,或者拖動的速度大於剛開始定義的速度,則該側邊欄就關閉或者開啟的,若不是,則恢復初始前一個位置的)
就這樣,你有一個工作的觸控式選單!
總結
對本文進行總結一下,首先這個效果在我們平日的手機應用裡,非常的常見,實現這一效果,主要利用的是移動端三大事件touchstart,touchmove,touchend,以及它們的觸控屬性,也就是手指在螢幕DOM中的實際位置,這時,需要考慮手指是水平滑動還是垂直,甚至有時候還得考慮手傾斜的滑動,還要區分是一根手指滑動,還是多根手指滑動,側邊選單欄動畫的實現,以及要注意阻止預設事件,重疊計算等等一些細節
看似簡單的效果,整個過程實現起來,還是不容易的,當然很多時候,在平時中,想當然的會用一些框架,移動端庫來代替原生當中一些繁瑣的寫法的,原生js固然耐人耗腦,但也是很有味道的,其實甭管咋實現,只要能實現就好,最後在重複一遍,若想獲得本篇Demo原始碼,itclanCoder後臺複製該標題回覆[手勢魅力-設定一個觸控選單] 就可以下載了
以下是本篇提點概要
- HTML結構
- 所有你需要了解的JavaScript觸控事件(touchstart,touchmove,touchend),以及兩個觸控屬性pageX,pageY
- 需要知道關於requestAnimationFrame
- 拖動,點選和滑動:額外的東西要考慮移動觸控手勢(手勢方向,水平,垂直,還有手指根數)
- 你不知道你想知道的關於 - 是超級重要的部分
- 全域性變數和設定預設值(一些初始化值變數的設定)
- 函式中的函式(手指按下,移動,抬起功能函式的封裝呼叫)
- 這個手機觸控手勢最後有趣的一部分(建立動畫)
- 動畫中間(手指移動,拖動選單過程)
- 我們需要一些邊界在這裡!(設定限制),也就是側邊欄選單滑動的位置
- 重疊計算(透明度變化,也就是用小數來計算,百分比值)
- 動畫結束(選單欄開啟和關閉狀態,選單欄的位置)
原文地止:手勢魅力-設定一個觸控選單點選該連結可讀英文原文