聊聊各端手勢體系以及對 Web 標準手勢的思考

「北海 Kraken」是一款基於 Flutter 的 Web 渲染引擎,通過基於 W3C 標準來開發實現前端開發者常用的能力。 Kraken 團隊也積極探索定義新的問題以及能力,期望通過參推動標準定製的方式讓 Web 技術變得更好。 歡迎大家關注 「北海 Kraken」: http://openkraken.com/

在過去,早期的 Web 更多用做內容展示的頁面,最早從後端框架中直出,再配上各種 CSS 以及 JS 的交互內容,以完成最終對頁面內容的展示,那時候的 Web 更多屬於 【內容開發】,做內容的直出與展示。而如今現代 Web 開發體系已經有了翻天覆地的變化,早已超出了【內容開發】的範疇,在各個領域都有 JavaScript 的身影。同樣,Web 也已經脫離了客戶端以及瀏覽器的限制,各式各樣基於 Web 標準或者私有標準的 Web runtime 層出不窮。【Web 應用開發】區別於傳統的【內容開發】,它對開發者提出了更高的要求,也對 Web 的能力提出了更高的要求,無論是基於標準化方面的考量,還是基於對易用性的考慮,我們都期望 Web 開發者可以獲得通過更高級的封裝的標準的高性能的能力。

而手勢能力就是其中的一塊。

目前在 Web 標準中,手勢能力是屬於缺失的一塊能力,更多的開發者通過 hammer.js 來獲得一個通過 JavaScript 模擬出來的手勢事件來開發一個手勢強交互的應用,或者是直接基於更底層的 Touch event來做進一步的封裝。

但是無論是類 hammer.js 的前端手勢方案,還是 Touch event的封裝,都會導致一些問題,我將從易用性、性能、標準化的角度來做進一步的分析:

  • 易用性: 開發者必須手動去實現或者封裝更高一級的手勢能力,無法直接從 element 上獲得某個高級手勢的 event 事件。無論是開發成本,都需要額外加載或者執行額外的 CDN,都是對前端資源的一種損耗。
  • 性能: 通過 JavaScript 實現的方案需要頻繁地通過 Bridge 將手勢的能力傳遞到前端,然後再去計算模擬相關的手勢事件。頻繁的傳遞數據增加了 Bridge 的消耗,不斷執行的 JavaScript 會阻塞 UI 線程,如果需要更強大的手勢能力支撐,我們必須進一步封裝【競爭場】等能力的實現來達到手勢競爭的目的,而這部分能力本應下沉到渲染引擎本身,而不是在 JavaScript 中處理。
  • 標準化: 各個開發者實現的標準不統一,判斷的基準不一致,透出的 event 能力不對齊會導致各個平臺甚至到各個頁面的標準不統一。譬如說在同一個 iOS 設備上訪問兩個不同開發者開發的頁面,不統一的手勢能力可能會給用戶帶來及其糟糕的體驗。非標準化的手勢能力在各個端上也顯得格外突出,下面我介紹各端的手勢能力時會介紹這些差異點。

連續手勢與離散手勢

首先我需要介紹一下連續手勢與離散手勢的概念,以便讀者可以更好地區分這兩種手勢的不同,以及瞭解實現不同的手勢能力對開發、性能、易用性等緯度的影響。

首先,我們需要知道,由於在端側有各種各樣的屏幕操作的設備,常見的比如說類 apple pencil 的電子筆設備(pen),手指直接觸摸操作(touch),還有鼠標(mouse)等。所以在 W3C 標準中, 將所有的接觸屏幕的物理設備抽象成了一個 pointer,無論上層是那種物理設備,對於屏幕只感知與抽象這一個觸摸到的點,基於 type 區分具體的上層的物理設備。

pointer

一個完整的手勢包含了手指開始接觸屏幕(pointer down),然後手指在屏幕上進行偏移(pointer move),以及手指擡起離開屏幕(pointer up),暫時不討論 cancel、out 等情況。當然,其中中間 pointer move 的過程是可以省略的,最常見的省略 pointer move 的手勢譬如說 click 或者 long press 等(當然,如果點擊設備不是一個鼠標而是一根手指,其實手指實際接觸是肯定會產生輕微移動情況的,譬如在 FLutter 中,允許這個細微的移動距離在 18 個像素點內,即視爲不移動)。可以預見的是未來會有更多的物理設備操作屏(甚至不是屏),基於底層觸摸點的 pointer 抽象有利於上層做更多的擴展。

瞭解了這些,接下來我們來了解一下連續手勢與離散手勢的差異。

  • 連續手勢:從 pointer down 到 pointer moves 到 pointer up,中間過程可以通過 state 狀態來描述的手勢,可以清楚地通過不同的回調或者不同的狀態讓開發者感知目前手勢所處的狀態的手勢。常見連續手勢:pan。
  • 離散手勢:完整手勢觸發完畢後纔會通過回調來通知開發者,無中間狀態的轉換。常見離散手勢:click。

連續手勢會頻繁通過回調或者狀態來通知開發者目前手勢所處的狀態,我們來看一種情況:

element.addEventLisenter('pan', (gestureEvent) => {
	if (gestureEvent.state === 'up') {
		// do something...
	}
})

假設我們需要在 Web 標準中實現 pan 這個手勢,如果它是一個連續手勢,而我們的場景只需要用 up 這種狀態,就需要不斷地將當前的狀態通過 Bridge 以及 JS engine 傳遞到 JavaScript 中,這頻繁的傳遞開銷是對設備性能的一種浪費。當然,也有框架方案通過更加細分的粒度去解決這個事情,譬如說拆分成 panstartpanupdatepanend等,當開發者不給這些方法註冊回調時,可以在框架內部判斷並做相應優化。然而細分的 API 抽象不夠底層,對於開發者來說也並不那麼友好。

而對於離散手勢,我們則不需要考慮手勢過程中的狀態傳遞,只需要把最終的結果返回給開發者即可,離散手勢屏蔽了許多內部處理的細節,保證了開發者註冊的回調只能完整的手勢操作完以後才能被命中。有效地降低了連續手勢數據的傳遞量。但是相較於連續手勢,離散手勢的缺點是開發者無法很好地感知中間狀態。

接下來我們來看一下各個端上實現的手勢體系、優缺點以及差異性。

各端手勢體系

hammer.js

hammer

hammer.js 作爲一個前端實現的 gesture lib,通過註冊 Touch 事件做封裝來完成具體的操作的判斷,在前端做手勢的方案在前面已經提過,需要不斷地通過 Bridge 以及 JS engine 傳遞到 JavaScript 中,然後才能最終在 JavaScript 中處理手勢操作,只要有操作就會被拋到 JavaScript 中進行處理,頻繁的傳遞耗費了許多不必要的性能。我們更希望這部分能力可以下沉到渲染引擎本身,這樣可以節省非常多不必要的數據傳遞開銷。

如果在基礎手勢判斷之上想進一步引入更加複雜的【競技場】等能力,這部分會使得 JavaScript 中的邏輯更加複雜,即便拋開“能不能”在前端做相關實現來說,過多的 JavaScript 運行佔用計算資源也是我們並不想要的。

同時,需要單獨引入一個 CDN 腳本來支持相關的功能,對於包體積以及首屏也增加了額外的成本。但是又考慮到本身瀏覽器並不自帶這些功能,一般開發者也無法很好地將這套方案優化並下沉到瀏覽器中,所以在反而在大部分前端業務場景成爲來較優的技術選型。

Flutter

Flutter

  • Tap
  • Double tap
  • Long press (500 ms 以上的長按)
  • Vertical(Horizontal) drag 橫(縱)滑 在 drag 上做了進一步封裝,在 x 軸或者 y 軸偏移超過最小距離並達到閾值速度可觸發。
  • scale scale 會包含放大縮小以及旋轉的手勢,相當於其他端中的 Pinch + Rotation
  • Pan Pan 內部實現,需要達到一個最小速度以及最小移動距離的 drag 才能觸發 Pan,Pan 是基於 drag 之上的封裝,增加了判斷。Flutter 內置一個 pointer down + pointer moves + pointer up 只能觸發一次手勢,所以 Pan 只能觸發一次(與 hammer 不同,爲有狀態手勢) 詳見: https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/gestures/monodrag.dart#L579

Flutter 的手勢體系除了 Long press 均爲連續手勢,無離散手勢,Tap 也會通過 TapDown、TapUp 等狀態來完成。每個中間狀態會通過不同的回調函數來支持開發者處理邏輯,返回參數也根據中間狀態的不同而不同。排除 Widget 的統一封裝來看,跟安卓類似,回調過多,且返回參數不統一,不利於標準化。但是對於內部的手勢回調來說,細分的接口各司其職,傳遞所需的參數都是必須的,開發者可以直接獲取具體的返回信息。

iOS

  • Tap(離散手勢,100 ms 左右的點擊行爲)
  • Long Press (連續手勢,500 ms 以上的點擊行爲)
  • Pan (連續手勢,平移,類似 drag,但是可以在移動過程中不斷變化方向)
  • Swipe (離散手勢)
  • Pinch(連續手勢,向外捏時放大,向內捏時縮小)
  • Rotation(連續手勢,旋轉)

iOS手勢

爲了方便大家瞭解各個手勢的區別,尤其是 Pan 跟 Swipe 的區別,特地放上了iOS 開發者文檔的一些圖片

iOS 的手勢可以帶上多個 touch pointer,同時滿足了幾個手指操作的能力。比如三指滑動(三根手指的swipe)、雙指點擊(兩根手指的 Tap)等。它提供了開發者對某一個手勢處理成一個註冊回調函數,通過 state 判斷目前手勢的狀態。離散手勢與連續手勢共存。

Android

  • View 上直接提供 click 以及 touch 的一些方法
    • OnDragListener:拖動事件。
    • OnLongClickListener:長按擡起時的事件。
  • GestureDetector.OnGestureListener
    • onDown:手勢識別器的 down 事件。
    • onFling: 類似 swipe。
    • onLongPress: 長按。
    • onScroll:scroll view 滾動時的事件。
    • onShowPress:按下後沒擡起,相當於(up、move、down 的中間 move 狀態,只是沒move)。
    • onSingleTapUp: 點擊擡起,對應 onDown。
  • GestureDetector.OnDoubleTapListener: 雙擊。
  • ScaleGestureDetector:旋轉,捏,分 begin、onScale、end。

相對來說,Android 的手勢體系比較細分,大致上跟 FLutter 比較像,但是 Flutter 是不同手勢在不同類中的,Flutter 基本上都是離散手勢,安卓很多連續手勢,但是更加細分。

標準

綜上分析了 Flutter、iOS、Android 以及一個前端實現的 gesture lib(hammer.js),不難發現,每個端實現的手勢方案都大同小異。無非都實現了這幾種方法: click(Tap)、swipe、Pan、Long Press (Press)、Pinch 與 Rotation(或者 Scale)。但是各個平臺對每個手勢的實現還是有些許的差異,無論是具體手勢的代碼邏輯判斷還是具體手勢的拆分或者命名,均有不同。

那在 Web 技術上,我們應該使用怎麼樣一套手勢規範,來兼顧易用性、性能以及標準化呢?就目前來看,基於 Web 技術體系發展來的 Web runtime 的已經非常多了,諸如 Web、React Native、小程序等體系已經在端側帶來了巨大的運行時碎片化。未來不止於移動端上,還有各種 IOT 設備出現,可能會有越來越多的 Web runtime 會出現。未來可能會有更多領域會有不一樣的終端設備,而摺疊屏、柔性屏的到來也可能會讓端側的設備(手機、IOT、車載等)形成更多更復雜的的跨端場景,隨之而來的也是更多的交互手勢來與這些設備進行“溝通交流”。

很遺憾的是目前 W3C 上沒有相應的手勢規範,我們更期望有一個統一的既定標準來規範,我們也在 W3C 中文興趣小組上發起了一個討論,目前此討論已經提到了 UIEvent。我們期望通過易用性、性能以及標準化這幾個緯度去討論手勢規範以及對應的手勢標準化能力的必要性,以及最終推動規範建立的可行性,也歡迎更多的小夥伴加入該討論。

此外,該標準提案目前已經在 北海 Kraken 上實現,開發者可以直接使用 增強的手勢能力 來開發複雜的交互應用。後續 北海 Kraken 團隊將會在複雜的業務場景上定義出更多問題以及通用能力,期望可以通過參與推動標準定製的方式讓 Web 技術變得更好。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章