天天看點

聊聊各端手勢體系以及對 Web 标準手勢的思考連續手勢與離散手勢各端手勢體系FlutterAndroid标準

作者:染陌

聊聊各端手勢體系以及對 Web 标準手勢的思考連續手勢與離散手勢各端手勢體系FlutterAndroid标準

「北海 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的封裝,都會導緻一些問題,我将從易用性、性能、标準化的角度來做進一步的分析:

聊聊各端手勢體系以及對 Web 标準手勢的思考連續手勢與離散手勢各端手勢體系FlutterAndroid标準
  • 易用性:

    開發者必須手動去實作或者封裝更高一級的手勢能力,無法直接從 element 上獲得某個進階手勢的 event 事件。無論是開發成本,都需要額外加載或者執行額外的 CDN,都是對前端資源的一種損耗。

  • 性能:

    通過 JavaScript 實作的方案需要頻繁地通過 Bridge 将手勢的能力傳遞到前端,然後再去計算模拟相關的手勢事件。頻繁的傳遞資料增加了 Bridge 的消耗,不斷執行的 JavaScript 會阻塞 UI 線程,如果需要更強大的手勢能力支撐,我們必須進一步封裝【競争場】等能力的實作來達到手勢競争的目的,而這部分能力本應下沉到渲染引擎本身,而不是在 JavaScript 中處理。

  • 标準化:

    各個開發者實作的标準不統一,判斷的基準不一緻,透出的 event 能力不對齊會導緻各個平台甚至到各個頁面的标準不統一。譬如說在同一個 iOS 裝置上通路兩個不同開發者開發的頁面,不統一的手勢能力可能會給使用者帶來及其糟糕的體驗。非标準化的手勢能力在各個端上也顯得格外突出,下面我介紹各端的手勢能力時會介紹這些差異點。

連續手勢與離散手勢

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

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

聊聊各端手勢體系以及對 Web 标準手勢的思考連續手勢與離散手勢各端手勢體系FlutterAndroid标準

一個完整的手勢包含了手指開始接觸螢幕(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 中,這頻繁的傳遞開銷是對裝置性能的一種浪費。當然,也有架構方案通過更加細分的粒度去解決這個事情,譬如說拆分成 

panstart

panupdate

panend

等,當開發者不給這些方法注冊回調時,可以在架構内部判斷并做相應優化。然而細分的 API 抽象不夠底層,對于開發者來說也并不那麼友好。

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

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

各端手勢體系

hammer.js

聊聊各端手勢體系以及對 Web 标準手勢的思考連續手勢與離散手勢各端手勢體系FlutterAndroid标準

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

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

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

Flutter

聊聊各端手勢體系以及對 Web 标準手勢的思考連續手勢與離散手勢各端手勢體系FlutterAndroid标準
  • Tap
  • Double tap
  • Long press (500 ms 以上的長按)
  • Vertical(Horizontal) drag  橫(縱)滑

    在 drag 上做了進一步封裝,在 x 軸或者 y 軸偏移超過最小距離并達到門檻值速度可觸發。

  • scale

    scale 會包含放大縮小以及旋轉的手勢,相當于其他端中的 Pinch + Rotation

  • 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(連續手勢,旋轉)
聊聊各端手勢體系以及對 Web 标準手勢的思考連續手勢與離散手勢各端手勢體系FlutterAndroid标準

為了友善大家了解各個手勢的差別,尤其是 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 技術變得更好。

聊聊各端手勢體系以及對 Web 标準手勢的思考連續手勢與離散手勢各端手勢體系FlutterAndroid标準

繼續閱讀