要做一款移動設備上的軟鍵盤,那麼怎麼處理用戶的點擊位置,就是你遇到的第一個難題,在這個問題上,我也走了很長的路。
我把落格輸入法開發以來的觸控邏輯大致分類為三個階段,現在分別來講講設計思路,希望能夠對你有所幫助。
第一代觸控引擎
顯然,對於一個初學者來說,沒什麼比系統控件更好用的了,功能全,速度也不慢,業務邏輯完善,所以,落格輸入法的第一代消息處理就是用的 的UIButton 的 TouchUpInside 消息。因为一开始我甚至用的是 xib 构建键盘布局,所以直接使用了 @IBAction為 FUNC buttonTouchUpInside(_ 寄件人: 的UIButton) 這樣的聲明,你一看就懂了,對吧?處理很方便,用戶點擊了哪個 Button,那麼傳進來的就是哪個,不需要做額外的判斷,系統都幫你搞定了一切。但很快就遇到了第一個問題——按下按鈕後程序總要執行一會,於是 UI 就會卡頓,很明顯從按下按鍵到候選出現,會有延遲。
為了避免輸入法業務邏輯干擾(實際上是阻擋)UI 更新,我改為在後台處理進一步的消息,實際上你也應該總是這麼做——永遠不要在主線程處理業務邏輯。
1 2 3 |
DispatchQueue.global(qos: .userInitiated).async { self.input(button:button) } |
但這樣又引入了另一個問題,當用戶點擊按鍵間隔太短速度太快時,按鍵處理的順序會錯亂,於是,我把異步改為同步,這樣業務邏輯還是在後台線程處理,但會嚴格按照調用的順序依次執行(這確保了用戶按鍵以實際順序進行處理)
1 2 3 |
DispatchQueue.global(qos: .userInitiated).sync { self.input(button:button) } |
注意第一行,異步→同步。
第二代觸控引擎
第一代引擎正常工作了很久,但最終還是遇到了另一個問題:當用戶點擊更加快的時候,某些後台邏輯處理不正常。
這實際上是由於 的UIButton 自身觸控處理機制衝突造成的,當用戶點擊屏幕鍵盤速度太快,實際上短時間內同時按下了兩個按鈕,此時主線程自身是互相阻止的,只有當用戶兩個手指都離開屏幕,消息才會發送,即 @IBAction為 FUNC buttonTouchUpInside(_ 寄件人: 的UIButton) 被立即調用兩次。在極短時間內,兩次連續調用,雖然是有嚴格順序的,但每個按鍵消息都會處理候選欄的刷新,這就需要異步更改 UI,這就導致了一些處理邏輯異常——在 UI 刷新完成之前,下一個消息已經開始執行。
解決的思路有兩個,要么把業務邏輯的一些判斷改為和 UI 不關聯,要么想辦法讓系統能夠處理用戶同時按下多個 的UIButton 的情況。 ——顯然,應該從後者入手,於是,我在開發 落格輸入法 X 時做了一個按鍵緩存機制,它不再使用 TouchUpInside ,而是 接地 和 潤色 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
var pendingKey:UIButton? @IBAction func keyboardKeyDown(_ sender:UIButton) { if sender == pendingKey {return} if let pending = pendingKey { keyboardKeyConfirm(pending) } pendingKey = sender } @IBAction func keyboardKeyUp(_ sender: UIButton) { guard let pending = pendingKey, sender.tag == pending.tag else {return} keyboardKeyConfirm(pending) pendingKey = nil } func keyboardKeyConfirm(_ sender:UIButton) { buttonTouchUpInside(sender) } |
注意 buttonTouchUpInside 就是上一代的處理邏輯,並沒有什麼變化,這麼寫是為了讓你明白我的變更思路,這樣當用戶按下一個鍵,那麼就立即緩存它,當用戶再按下一個鍵,如果已經有緩存了,那麼就處理它,並將新的加入緩存;當用戶抬起手指,那麼就處理緩存的那個按鍵。如此一來,所有的按鍵都會得到處理,並且不會被 TouchUpInside 這個信號阻攔——因為我已經不再使用它了。
由於更改了信號獲取源頭(從 TouchUpInside 改為 接地 + 潤色 ),對用戶來說“手感”變化很大。
第三代觸控引擎
第二代實際上已經工作的很好,但在 落格輸入法 X 上架之前,我們內部測試就發現了另一個問題——實際上並不能說是新發現的,因為它一直都存在,那就是“q”和“p”的問題。在新的 iOS 系統當中,似乎是為了避免和系統手勢衝突,iOS 為屏幕邊緣的 5 個像素做了保留處理,當你點擊到屏幕邊緣的時候(即按按鍵“q”或者“p”時稍微靠邊了點), 接地 這個消息是不會立即被觸發的。
它會被延遲到你抬起手指的那一刻,然後和 潤色 一起發送給鍵盤。這就導致了用戶正常打字的時候,遇到這兩個位置,總是會感覺“卡頓”了一下,因為視覺和聲音反饋上,確實是延遲到你抬起手指的那一刻而不是按下就立即觸發。
如果是在 app 當中,你可以這樣做:
1 2 3 |
override var preferredScreenEdgesDeferringSystemGestures: UIRectEdge { return [.left,.right] } |
但顯然,在鍵盤中這個代碼並不生效。總之,為了讓即將上架的 落格輸入法 X 更加具有競爭力,我只好硬著頭皮繼續想辦法。
我在嘗試了很多方案之後,我終於找到了一個能獲取信號的控件—— UITapGestureRecognizer 。
不是用它來進行標準識別點擊——這同樣是沒用的,必須使用它的 FUNC gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> 布爾 這個代理方法,只有它能夠越過系統屏蔽,正確獲得 接地 調用,而不是被延遲到用戶抬起手指的那一刻。
對應地,我又使用 的UIView 本身的 FUNC touchesEnded(_ 觸摸: 組<UITouch>, 同 事件: 的UIEvent?) 來獲取 潤色 行為,如此一來,就可以參考上一代引擎的邏輯實現了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class TouchLayer:UIView,UIGestureRecognizerDelegate { var tapGR = UITapGestureRecognizer() init(keyboard:KeyboardViewController) { super.init(frame: CGRect.zero) tapGR.delegate = self self.addGestureRecognizer(tapGR) } var currentTouch:UITouch? override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { if let t = currentTouch { touchUpInside(t) } } func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { if let t = currentTouch { touchUpInside(t) } touchDown(touch) return true } } |
其他代碼略過不表,這樣操作之後,把這個 TouchLayer 覆蓋在鍵盤上方即可——當然,你也可以在你的 按鍵 子類裡進行這樣的操作,然後單獨處理。這裡我則蓋在鍵盤上方,進行統一處理了。
為此,我不得不對鍵盤的業務邏輯進行了一番調整和重構……直到去年年底,我寫了一篇文章 落格輸入法 X 是如何處理屏幕邊緣延遲問題的。
總之,這就是現在線上版本 落格輸入法 X 在使用的觸控邏輯,目前來看,一切良好。如果說缺點,大概就是從二代升級三代代價實在是太大了,這幾乎是 落格輸入法 X 與 經典版 的重點區別之一了。
本文由 落格博客 原創撰寫:落格博客 » 落格輸入法是如何處理按鍵消息的
轉載請保留出處和原文鏈接:https://www.logcg.com/archives/3205.html
原來以前經常切窗口的時候會在輸入框留下一個q字母是這個原因…
不過這樣看來,iOS原生輸入法沒有UX方面的問題,用的API和留給第三方的輸入API很可能不是同一套?略坑
第二代那套邏輯有點像做ACT遊戲處理輸入緩衝的思路