用 canvas 實現 Web 手勢解鎖

發表於2017-04-04

最近參加 360 暑假的前端星計劃,有一個線上作業,截止日期是 3 月 30 號,讓手動實現一個 H5 手勢解鎖,具體的效果就像原生手機的九宮格解鎖那樣。

用 canvas 實現 Web 手勢解鎖

實現的最終效果就像下面這張圖這樣:

用 canvas 實現 Web 手勢解鎖

基本要求是這樣的:將密碼儲存到 localStorage 裡,開始的時候會從本地讀取密碼,如果沒有就讓使用者設定密碼,密碼最少為五位數,少於五位要提示錯誤。需要對第一次輸入的密碼進行驗證,兩次一樣才能保持,然後是驗證密碼,能夠對使用者輸入的密碼進行驗證。

H5 手勢解鎖

掃碼線上檢視:

用 canvas 實現 Web 手勢解鎖

或者點選檢視手機版

專案 GitHub 地址,H5HandLock

首先,我要說明一下,對於這個專案,我是參考別人的,H5lock

我覺得一個比較合理的解法應該是利用 canvas 來實現,不知道有沒有大神用 css 來實現。如果純用 css 的話,可以將連線先設定 display: none,當手指劃過的時候,顯示出來。光設定這些應該就非常麻煩吧。

之前瞭解過 canvas,但沒有真正的寫過,下面就來介紹我這幾天學習 canvas 並實現 H5 手勢解鎖的過程。

準備及佈局設定

我這裡用了一個比較常規的做法:

常規方法,比較易懂和操作,弊端就是,可以被隨意的修改。

傳入的引數中要包含一個 dom 物件,會在這個 dom 物件內建立一個 canvas。當然還有一些其他的 dom 引數,比如 message,info 等。

關於 css 的話,懶得去新建檔案了,就直接內聯了。

canvas

1. 學習 canvas 並搞定畫圓

MDN 上面有個簡易的教程,大致瀏覽了一下,感覺還行。Canvas教程

先建立一個 canvas,然後設定其大小,並通過 getContext 方法獲得繪畫的上下文:

然後呢,先畫 n*n 個圓出來:

畫圓函式,需要注意:如何確定圓的半徑和每個圓的圓心座標(這個我是參考的),如果以圓心為中點,每個圓上下左右各擴充套件一個半徑的距離,同時為了防止四邊太擠,四周在填充一個半徑的距離。那麼得到的半徑就是 width / ( 4 * n + 2),對應也可以算出每個圓所在的圓心座標,也有一套公式,GET

2. 畫線

畫線需要藉助 touch event 來完成,也就是,當我們 touchstart 的時候,傳入開始時的相對座標,作為線的一端,當我們 touchmove 的時候,獲得座標,作為線的另一端,當我們 touchend 的時候,開始畫線。

這只是一個測試畫線功能,具體的後面再進行修改。

有兩個函式,獲得當前 touch 的相對座標:

畫線:

然後就是監聽 canvas 的 touchstarttouchmove、和 touchend 事件了。

3. 畫折線

所謂的畫折線,就是,將已經觸控到的點連起來,可以把它看作是畫折線。

首先,要用兩個陣列,一個陣列用於已經 touch 過的點,另一個陣列用於儲存未 touch 的點,然後在 move 監聽時候,對 touch 的相對位置進行判斷,如果觸到點,就把該點從未 touch 移到 touch 中,然後,畫折線,思路也很簡單。

4. 標記已畫

前面已經說了,我們把已經 touch 的點(圓)放到陣列中,這個時候需要將這些已經 touch 的點給標記一下,在圓心處畫一個小實心圓:

同時新增一個 reset 函式,當 touchend 的時候呼叫,400ms 呼叫 reset 重置 canvas。

到現在為止,一個 H5 手勢解鎖的簡易版已經基本完成。

password

為了要實現記住和重置密碼的功能,把 password 儲存在 localStorage 中,但首先要新增必要的 html 和樣式。

1. 新增 message 和 單選框

為了儘可能的使介面簡潔(越醜越好),直接在 body 後面新增了:

將新增到 dom 已 option 的形式傳給 handLock:

2. info 資訊顯示

關於 info 資訊顯示,自己寫了一個懸浮窗,然後預設為 display: none,然後寫了一個 showInfo 函式用來顯示提示資訊,直接呼叫:

關於 info 的樣式,在 html 中呢。

3. 關於密碼

先不考慮從 localStorage 讀取到情況,新加一個 lsPass 物件,專門用於儲存密碼,由於密碼情況比較多,比如設定密碼,二次確認密碼,驗證密碼,為了方便管理,暫時設定了密碼的三種模式,分別是:

model:1 驗證密碼模式

model:2 設定密碼模式

model:3 設定密碼二次驗證

具體看下面這個圖:

用 canvas 實現 Web 手勢解鎖

這三種 model ,只要處理好它們之間如何跳轉就 ok 了,即狀態的改變。

所以就有了 initPass:

有必要再來介紹一下 lsPass 的格式:

因為之前已經有了一個基本的實現框架,現在只需要在 touchend 之後,寫一個函式,功能就是先對當前的 model 進行判斷,實現對應的功能,這裡要用到 touchCircles 陣列,表示密碼的順序:

密碼的設定要參考前面那張圖,要時刻警惕狀態的改變。

4. 手動重置密碼

思路也很簡單,就是新增點選事件,點選之後,改變 model 即可,點選事件如下:

ps:這裡面還有幾個小的 bug,因為 model 只有 3 個,所以設定的時候,當點選重置密碼的時候,沒有設定密碼成功,又切成驗證密碼狀態,此時無法提升沿用舊密碼,原因是 model 只有三個

5. 新增 touchend 顏色變化

實現這個基本上就大功告成了,這個功能最主要的是給使用者一個提醒,若使用者劃出的密碼符合規範,顯示綠色,若不符合規範或錯誤,顯示紅色警告。

因為之前已經設定了一個 succ 變數,專門用於重繪。

那麼,一個可以演示的版本就生成了,儘管還存在一些 bug,隨後會來解決。(詳情分支 password)

一些 bugs

有些 bugs 在做的時候就發現了,一些 bug 後來用手機測試的時候才發現,比如,我用 chrome 的時候,沒有察覺這個 bug,當我用 android 手機 chrome 瀏覽器測試的時候,發現當我 touchmove 向下的時候,會觸發瀏覽器的下拉重新整理,解決辦法:加了一個 preventDefault,沒想到居然成功了。

關於 showInfo

由於showInfo 中有 setTimeout 函式,可以看到函式裡的演出為 1s,導致如果我們操作的速度比較快,在 1s 內連續 show 了很多個 info,後面的 info 會被第一個 info 的 setTimeout 弄亂,顯示的時間小於 1s,或更短。比如,當重複點選設定手勢密碼和驗證手勢密碼,會產生這個 bug。

解決辦法有兩個,一個是增加一個專門用於顯示的陣列,每次從陣列中取值然後顯示。另一種解題思路和防抖動的思路很像,就是當有一個新的 show 到來時,把之前的那個 setTimeout 清除掉。

這裡採用第二種思路:

解決小尾巴

所謂的小尾巴,如下:

用 canvas 實現 Web 手勢解鎖

解決辦法也很簡單,在 touchend 的時候,先進行 clearRect 就 ok 了。

關於優化

效能優化一直都是一個大問題,不要以為前端不需要考慮記憶體,就可以隨便寫程式碼。

之前在設計自己網頁的時候,用到了滾動,滑鼠滑輪輕輕一碰,滾動函式就執行了幾十多則幾百次,之前也考慮過解決辦法。

優化 canvas 部分

對於 touchmove 函式,原理都是一樣的,手指一劃,就執行了 n 多次,這個問題後面在解決,先來看另一個問題。

touchmove 是一個高頻函式,看到這裡,如果你並沒有仔細看我的程式碼,那你對我採用的 canvas 畫圖方式可能不太瞭解,下面這個是 touchmove 函式幹了哪些事:

  1. 先判斷,如果當前處於未選中一個密碼狀態,則繼續監視當前的位置,直到選中第一個密碼,進入第二步;
  2. 進入 update 函式,update 函式主要幹四件事,重繪圓(密碼)、判斷當前位置、重繪點、重繪線;

第二步是一個很揪心的動作,為什麼每次都要重繪圓,點和線呢?

用 canvas 實現 Web 手勢解鎖

上面這個圖可以很好的說明問題,因為在設定或驗證密碼的過程中,我們需要用一條線來連線觸點到當前的最後一個密碼,並且當 touchmove 的時候,能看到它們在變化。這個功能很棒,可以勾勒出 touchmove 的軌跡。

但是,這就必須要時刻重新整理 canvas,效能大大地降低,重新整理的那可是整個 canvas。

因為 canvas 只有一個,既要畫背景圓(密碼),又要畫已選密碼的點,和折線。這其中好多步驟,自始至終只需要一次就好了,比如背景圓,只需在啟動的時候畫一次,已選密碼,只要當 touchCircles 新加元素的時候才會用一次,還不用重繪,只要畫就可以了。折線分成兩部分,一部分是已選密碼之間的連線,還有就是最後一個密碼點到當前觸點之間的連線。

如果有兩個 canvas 就好了,一個儲存靜態的,一個專門用於重繪

為什麼不可以有呢!

我的解決思路是,現在有兩個 canvas,一個在底層,作為描繪靜態的圓、點和折線,另一個在上層,一方面監聽 touchmove 事件,另一方面不停地重繪最後一個密碼點的圓心到當前觸點之間的線。如果這樣可以的話,touchmove 函式執行一次的效率大大提高。

插入第二個 canvas:

要改換對第二個 ctx2 進行 touch 監聽,並設定一個 this.reDraw 引數,表示有新的密碼新增進來,需要對點和折線新增新內容, update 函式要改成這樣:

相應的 drawPoints 和 drawLine 函式也要對應修改,由原理畫所有的,到現在只需要畫新加的。

效果怎麼樣:

用 canvas 實現 Web 手勢解鎖

move 函式執行多次,而其他函式只有當新密碼加進來的時候才執行一次。

加入節流函式

之前也已經說過了,這個 touchmove 函式執行的次數比較多,儘管我們已經用兩個 canvas 對重繪做了很大的優化,但 touchmove 還是有點大開銷。

這個時候我想到了防抖動和節流,首先防抖動肯定是不行的,萬一我一直處於 touch 狀態,重繪會延遲死的,這個時候節流會好一些。防抖和節流

先寫一個節流函式:

節流函式的意思:在延遲為 delay 的時間內,如果函式再次觸發,則重新計時,這個功能和防抖動是一樣的,第三個引數 mustRun 是一個時間間隔,表示在時間間隔大於 mustRun 後的一個函式可以立即直接執行。

然後對 touchmove 的回撥函式進行改造:

關於 delay 和 mustRun 的時間間隔問題,web 效能裡有一個 16ms 的概念,就是說如果要達到每秒 60 幀,間隔為 1000/60 大約為 16 ms。如果間隔大於 16ms 則 fps 會比 60 低。

鑑於此,我們這裡將 delay 和 mustRun 都設為 16,在極端的情況下,也就是最壞的情況下,或許需要 15 + 15 = 30ms 才會執行一次,這個時候要設定兩個 8 才合理,不過考慮到手指活動是一個連續的過程,怎麼可能會每 15 秒執行一次,經過線上測試,發現設定成 16 效果還不錯。

效能真的能優化嗎,我們來看兩個圖片,do 和 wantdo 表示真實執行和放到節流函式中排隊準備執行。

當 touchmove 速度一般或很快的時候:

用 canvas 實現 Web 手勢解鎖

當 touchmove 速度很慢的時候:

用 canvas 實現 Web 手勢解鎖

可以看出來,滑動過程中,速度一般和快速,平均優化了一半,慢速效果也優化了 20 到 30% 之間,平時手勢鎖解鎖時候,肯定速度很快。可見,節流的優化還是很明顯的。

關鍵是,優化之後的流程性,沒有受到任何影響。

這個節流函式最終還是出現了一個 bug:由於是延遲執行的,導致 e.preventDefault 失效,在手機瀏覽器向下滑會出現重新整理的情況,這也算事件延遲的一個危害吧。

解決辦法:在節流函式提前取消預設事件:

總結

大概花了三天左右的時間,將這個 H5 的手勢解鎖給完成,自己還是比較滿意的,雖然可能達不到評委老師的認可,不過自己在做的過程中,學習到了很多新知識。

參考

H5lock
Canvas教程
js獲取單選框裡面的值
前端高效能滾動 scroll 及頁面渲染優化

相關文章