引言
在這觸控屏的時代,人性化的手勢操作已經深入了我們生活的每個部分。現代應用越來越重視與使用者的互動及體驗,手勢是最直接且最為有效的互動方式,一個好的手勢互動,能降低使用者的使用成本和流程,大大提高了使用者的體驗。
近期,公司的多個專案中都對手勢有著較高的需求,已有的手勢庫無法完全 cover
,因此便擼了一個輕量、便於使用的移動端手勢庫。這篇博文主要是解析了移動端常用手勢的原理,及從前端的角度學習過程中所使用的數學知識。希望能對大家有一點點的啟發作用,也期待大神們指出不足甚至錯誤,感恩。
主要講解專案中經常使用到的五種手勢:
- 拖動:
drag
- 雙指縮放:
pinch
- 雙指旋轉:
rotate
- 單指縮放:
singlePinch
- 單指旋轉:
singleRotate
Tips : 因為
tap
及swipe
很多基礎庫中包含,為了輕便,因此並沒有包含,但如果需要,可進行擴充套件;
實現原理
眾所周知,所有的手勢都是基於瀏覽器原生事件touchstart
, touchmove
, touchend
, touchcancel
進行的上層封裝,因此封裝的思路是通過一個個相互獨立的事件回撥倉庫handleBus
,然後在原生touch
事件中符合條件的時機觸發並傳出計算後的引數值,完成手勢的操作。實現原理較為簡單清晰,先不急,我們先來理清一些使用到的數學概念並結合程式碼,將數學運用到實際問題中,數學部分可能會比較枯燥,但希望大家堅持讀完,相信會收益良多。
基礎數學知識函式
我們常見的座標系屬於線性空間,或稱向量空間 (Vector Space)。這個空間是一個由點 (Point) 和 向量 (Vector) 所組成集合;
點(Point)
可以理解為我們的座標點,例如原點O(0,0),A(-1,2)
,通過原生事件物件的touches
可以獲取觸控點的座標,引數index
代表第幾接觸點;
向量(Vector)
是座標系中一種 既有大小也有方向的線段,例如由原點O(0,0)
指向點A(1,1)
的箭頭線段,稱為向量a
,則a=(1-0,1-0)=(1,1)
;
如下圖所示,其中i
與j
向量稱為該座標系的單位向量,也稱為基向量,我們常見的座標系單位為1
,即i=(1,0);j=(0,1)
;
獲取向量的函式:
向量模
代表 向量的長度,記為|a|
,是一個標量,只有大小,沒有方向;
幾何意義代表的是以x,y
為直角邊的直角三角形的斜邊,通過勾股定理進行計算;
getLength
函式:
向量的數量積
向量同樣也具有可以運算的屬性,它可以進行加、減、乘、數量積和向量積等運算,接下來就介紹下我們使用到的數量積這個概念,也稱為點積,被定義為公式:
當a=(x1,y1),b=(x2,y2),則a·b=|a|·|b|·cosθ=x1·x2+y1·y2;
共線定理
共線,即兩個向量處於 平行 的狀態,當a=(x1,y1),b=(x2,y2)
,則存在唯一的一個實數λ,使得a=λb
,代入座標點後,可以得到 x1·y2= y1·x2
;
因此當x1·y2-x2·y1>0
時,既斜率 ka > kb ,所以此時b
向量相對於a
向量是屬於順時針旋轉,反之,則為逆時針;
旋轉角度
通過數量積公式我們可以推到求出兩個向量的夾角:
cosθ=(x1·x2+y1·y2)/(|a|·|b|);
然後通過共線定理我們可以判斷出旋轉的方向,函式定義為:
矩陣與變換
由於空間最本質的特徵就是其可以容納運動,因此線上性空間中,
我們用向量來刻畫物件,而矩陣便是用來描述物件的運動;
而矩陣是如何描述運動的呢?
我們知道,通過一個座標系基向量便可以確定一個向量,例如 a=(-1,2)
,我們通常約定的基向量是 i = (1,0)
與 j = (0,1)
; 因此:
a = -1i + 2j = -1*(1,0) + 2*(0,1) = (-1+0,0+2) = (-1,2);
而矩陣變換的,其實便是通過矩陣轉換了基向量,從而完成了向量的變換;
例如上面的栗子,把a
向量通過矩陣(1,-2,3,0)
進行變換,此時基向量i
由 (1,0)
變換成(1,-2)
與j
由(0,1)
變換成(3,0)
,沿用上面的推導,則
a = -1i + 2j = -1(1,-2) + 2(3,0) = (5,2);
如下圖所示:
A圖表示變換之前的座標系,此時a=(-1,2)
,通過矩陣變換後,基向量i,j
的變換引起了座標系的變換,變成了下圖B,因此a
向量由(-1,2)
變換成了(5,2)
;
其實向量與座標系的關聯不變(
a = -1i+2j
),是基向量引起座標系變化,然後座標系沿用關聯導致了向量的變化;
結合程式碼
其實 CSS 的transform
等變換便是通過矩陣進行的,我們平時所寫的 translate/rotate
等語法類似於一種封裝好的語法糖,便於快捷使用,而在底層都會被轉換成矩陣的形式。例如 transform:translate(-30px,-30px)
編譯後會被轉換成 transform : matrix(1,0,0,1,30,30)
;
通常在二維座標系中,只需要 2X2 的矩陣便足以描述所有的變換了, 但由於CSS是處於3D環境中的,因此CSS中使用的是 3X3 的矩陣,表示為:
其中第三行的0,0,1
代表的就是z
軸的預設引數。這個矩陣中,(a,b)
即為座標軸的 i
基,而(c,d)
既為j
基,e
為x
軸的偏移量,f
為y
軸的偏移量;因此上慄便很好理解,translate
並沒有導致i,j
基改變,只是發生了偏移,因此translate(-30px,-30px) ==> matrix(1,0,0,1,30,30)
~
所有的transform
語句,都會發生對應的轉換,如下:
// 發生偏移,但基向量不變;
transform:translate(x,y) ==> transform:matrix(1,0,0,1,x,y)
// 基向量旋轉;
transform:rotate(θdeg)==> transform:matrix(cos(θ·π/180),sin(θ·π/180),-sin(θ·π/180),cos(θ·π/180),0,0)
// 基向量放大且方向不變;
transform:scale(s) ==> transform:matrix(s,0,0,s,0,0)
複製程式碼
translate/rotate/scale
等語法十分強大,讓我們的程式碼更為可讀且方便書寫,但是matrix
有著更強大的轉換特性,通過matrix
,可以發生任何方式的變換,例如我們常見的映象對稱,transform:matrix(-1,0,0,1,0,0)
;
MatrixTo
然而matrix
雖然強大,但可讀性卻不好,而且我們的寫入是通過translate/rotate/scale
的屬性,然而通過getComputedStyle
讀取到的 transform
卻是matrix
:
transform:matrix(1.41421, 1.41421, -1.41421, 1.41421, -50, -50);
請問這個元素髮生了怎麼樣的變化?。。這就一臉懵逼了。-_-|||
因此,我們必須要有個方法,來將matrix
翻譯成我們更為熟悉的translate/rotate/scale
方式,在理解了其原理後,我們便可以著手開始表演咯~
我們知道,前4個引數會同時受到rotate
和scale
的影響,具有兩個變數,因此需要通過前兩個引數根據上面的轉換方式列出兩個不等式:
cos(θ·π/180)*s=1.41421;
sin(θ·π/180)*s=1.41421;
將兩個不等式相除,即可以輕鬆求出θ
和s
了,perfect!!函式如下:
手勢原理
接下來我們將上面的函式用到實際環境中,通過圖示的方式來模擬手勢的操作,簡要地講解手勢計算的原理。希望各位大神理解這些基礎的原理後,能創造出更多炫酷的手勢,像我們在mac
觸控板上使用的一樣。
下面圖例:
圓點: 代表手指的觸碰點;
兩個圓點之間的虛線段: 代表雙指操作時組成的向量;
a向量/A點:代表在 touchstart 時獲取的初始向量/初始點;
b向量/B點:代表在 touchmove 時獲取的實時向量/實時點;
座標軸底部的公式代表需要計算的值;
Drag(拖動事件)
上圖是模擬了拖動手勢,由A
點移動到B
點,我們要計算的便是這個過程的偏移量;
因此我們在touchstart
中記錄初始點A的座標:
// 獲取初始點A;
let startPoint = getPoint(ev,0);
複製程式碼
然後在touchmove
事件中獲取當前點並實時的計算出△x
與△y
:
// 實時獲取初始點B;
let curPoint = getPoint(ev,0);
// 通過A、B兩點,實時的計算出位移增量,觸發 drag 事件並傳出引數;
_eventFire('drag', {
delta: {
deltaX: curPoint.x - startPoint.x,
deltaY: curPoint.y - startPoint.y,
},
origin: ev,
});
複製程式碼
Tips:
fire
函式即遍歷執行drag
事件對應的回撥倉庫即可;
Pinch(雙指縮放)
上圖是雙指縮放的模擬圖,雙指由a
向量放大到b
向量,通過初始狀態時的a
向量的模與touchmove
中獲取的b
向量的模進行計算,便可得出縮放值:
// touchstart中計算初始雙指的向量模;
let vector1 = getVector(secondPoint, startPoint);
let pinchStartLength = getLength(vector1);
// touchmove中計算實時的雙指向量模;
let vector2 = getVector(curSecPoint, curPoint);
let pinchLength = getLength(vector2);
this._eventFire('pinch', {
delta: {
scale: pinchLength / pinchStartLength,
},
origin: ev,
});
複製程式碼
Rotate(雙指旋轉)
初始時雙指向量a
,旋轉到b
向量,θ
便是我們需要的值,因此只要通過我們上面構建的getAngle
函式,便可求出旋轉的角度:
// a向量;
let vector1 = getVector(secondPoint, startPoint);
// b向量;
let vector2 = getVector(curSecPoint, curPoint);
// 觸發事件;
this._eventFire('rotate', {
delta: {
rotate: getAngle(vector1, vector2),
},
origin: ev,
});
複製程式碼
singlePinch(單指縮放)
與上面的手勢不同,單指縮放和單指旋轉都需要多個特有概念:
操作元素(
operator
):需要操作的元素。上面三個手勢其實並不關心操作元素,因為單純靠手勢自身,便能計算得出正確的引數值,而單指縮放和旋轉需要依賴於操作元素的基準點(操作元素的中心點)進行計算;按鈕:因為單指的手勢與拖動(drag)手勢是相互衝突的,需要一種特殊的互動方式來進行區分,這裡是通過特定的區域來區分,類似於一個按鈕,當在按鈕上操作時,是單指縮放或者旋轉,而在按鈕區域外,則是常規的拖動,實踐證明,這是一個使用者很容易接受且體驗較好的操作方式;
圖中由a
向量單指放大到b
向量,對操作元(正方形)素進行了中心放大,此時縮放值即為b
向量的模 / a
向量的模;
// 計算單指操作時的基準點,獲取operator的中心點;
let singleBasePoint = getBasePoint(operator);
// touchstart 中計算初始向量模;
let pinchV1 = getVector(startPoint,singleBasePoint);
singlePinchStartLength = getLength(pinchV1);
// touchmove 中計算實時向量模;
pinchV2 = getVector(curPoint, singleBasePoint);
singlePinchLength = getLength(pinchV2);
// 觸發事件;
this._eventFire('singlePinch', {
delta: {
scale: singlePinchLength / singlePinchStartLength,
},
origin: ev,
});
複製程式碼
singleRotate(單指旋轉)
結合單指縮放和雙指旋轉,可以很簡單的知道 θ
便是我們需要的旋轉角度;
// 獲取初始向量與實時向量
let rotateV1 = getVector(startPoint, singleBasePoint);
let rotateV2 = getVector(curPoint, singleBasePoint);
// 通過 getAngle 獲取旋轉角度並觸發事件;
this._eventFire('singleRotate', {
delta: {
rotate: getAngle(rotateV1, rotateV2),
},
origin: ev,
});
複製程式碼
運動增量
由於touchmove
事件是個高頻率的實時觸發事件,一個拖動操作,其實觸發了N次的touchmove
事件,因此計算出來的值只是一種增量,即代表的是一次 touchmove
事件增加的值,只代表一段很小的值,並不是最終的結果值,因此需要由mtouch.js
外部維護一個位置資料,類似於:
// 真實位置資料;
let dragTrans = {x = 0,y = 0};
// 累加上 mtouch 所傳遞出的增量 deltaX 與 deltaY;
dragTrans.x += ev.delta.deltaX;
dragTrans.y += ev.delta.deltaY;
// 通過 transform 直接操作元素;
set($drag,dragTrans);
複製程式碼
初始位置
維護外部的這個位置資料,如果初始值像上述那樣直接取0,則遇到使用css設定了transform
屬性的元素便無法正確識別了,會導致操作元素開始時瞬間跳回(0,0)
的點,因此我們需要初始去獲取一個元素真實的位置值,再進行維護與操作。此時,便需要用到上面我們提到的getComputedStyle
方法與matrixTo
函式:
// 獲取css transform屬性,此時得到的是一個矩陣資料;
// transform:matrix(1.41421,1.41421,-1.41421,1.41421,-50,-50);
let style = window.getComputedStyle(el,null);
let cssTrans = style.transform || style.webkitTransform;
// 按規則進行轉換,得到:
let initTrans = _.matrixTo(cssTrans);
// {x:-50,y:-50,scale:2,rotate:45};
// 即該元素設定了:transform:translate(-50px,-50px) scale(2) rotate(45deg);
複製程式碼
結語
至此,相信大家對手勢的原理已經有基礎的瞭解,基於這些原理,我們可以再封裝出更多的手勢,例如雙擊,長按,掃動,甚至更酷炫的三指、四指操作等,讓應用擁有更多人性化的特質。
基於以上原理,我封裝了幾個常見的工具:
Tips: 因為只針對移動端,需在移動裝置中開啟
demo
,或者pc端開啟mobile除錯模式!
-
mtouch.js : 移動端的手勢庫,封裝了上述的五種手勢,精簡的api設計,涵蓋了常見的手勢互動,基於此也可以很方便的進行擴充套件。 demo
github -
touchkit.js : 基於
mtouch
所封裝的一層更貼近業務的工具包,可用於製作多種手勢操作業務,一鍵開啟,一站式服務。 demo
github -
mcanvas.js : 基於canvas 開放極簡的api實現圖片<段落文字> <混排文字> <裁剪> <平移> <旋轉> <縮放> <水印新增> 一鍵匯出等。 demo
github
歡迎關注公眾號與我深入面基。?