在一個白板類應用的交互中必定會涉及到模式之間的更換和交互衝突。白板類軟件的交互模式通常包含了筆跡書寫模式,選擇模式,擦除筆跡模式等。多個模式之間存在切換,而切換能夠發生在某個模式執行過程,如須要在白板軟件裏面支持筆跡書寫功能,在書寫的過程打斷進入筆跡的擦除模式。本文告訴你們我所在團隊的白板內核的模式交互設計方案,本文不會涉及到具體實現的邏輯代碼架構
我從 2017 開始到如今都在作白板軟件,我對整個白板體系的軟件層面都比較瞭解。整個開發過程也對整個白板軟件的模式交互方案換了有一些方案,當前使用的方案也許不是最優的,可是相對來講比較適合業務框架
整個框架(不敢說架構)裏面三個大塊,第一塊是輸入前置,第二塊是輸入切換,第三塊是業務處理函數
小夥伴都知道,在 Windows 下實現觸摸不是簡單的事情,而在 WPF 中儘管有大量的封裝,可是對於總體觸摸來講,依然存在一些業務上的坑。如按下和擡起不成對等。而我指望在上層業務裏面不該該每一個業務都處理這些交互上的問題。所以就有了對 WPF 層的交互的封裝,此封裝能夠定製交互輸入數據,同時隔離框架差別。換句話說是這套框架能夠脫離 WPF 執行設計
在觸摸屏幕上面,在 WPF 收到的觸摸能夠經過監聽三個不一樣的事件 Touch Stylus Mouse 事件,這三個事件的觸發順序以及觸摸和觸筆的差別,會讓上層業務開發者們不得不在開發的時候關注這些細節。若是業務開發者須要關注框架細節,那麼確定會帶來業務複雜度以及挖坑。畢竟相同的邏輯寫10次,基本上就有一次寫出坑繼承
在輸入前置的第一層就是 SourceInput 層,這一層將隔離框架和平臺差別的交互輸入,同時約定一些通用交互。包括定義了 PointerDown PointerMove PointerUp PointerHover 這幾個事件。從事件命名上能夠了解到,這個事件是參照 UWP 的 Pointer 的設計。不管是鼠標輸入仍是觸摸輸入仍是觸筆的輸入,所有統一化。至於鼠標和觸摸等之間的差別,會放在事件的參數裏面,提供給特殊的業務能夠判斷事件
上面有一個細節是添加了 PointerHover 事件,這個事件實際上是將本來的 Move 事件拆開爲 PointerMove 和 PointerHover 兩個事件。表達的含義是在沒有按下之間發生的都是 Hover 事件,而按下以後發生的就是 Move 事件。爲何這樣作?在閱讀大量業務的代碼發現,基本上全部用到 Move 事件的地方都須要添加一個字段用來判斷當前是不是按下,若是是按下的 Move 才作業務。爲了減小相同的業務代碼,在框架底層將 Move 分爲兩個事件,可讓業務開發者用到 Move 的時候就是按下狀態的移動開發
更進一步的封裝是將 Down Move Up 封裝一層 Drag 拖拽事件。此部分僅僅是封裝,方便開發者,不屬於框架核心it
在隔離輸入層以後,就能夠統一化輸入,在框架層不須要了解輸入細節。框架層的輸入前置還須要保證一點的是對某個模式的輸入裏面按下和擡起是成對的,保證輸入裏面必定是先按下再移動再擡起,這個順序不會亂軟件
爲何這部分保證是在 SourceInput 層以後?緣由是這個保證須要處理一些模擬輸入,也就是 SourceInput 層僅封裝 WPF 框架的輸入。不處理模式交互框架裏面的各個模式收到輸入的保證輸入成對sso
每個不一樣的交互模式都應該繼承相同的交互模式基類,交互模式指的是如筆跡書寫模式,選擇模式,擦除筆跡模式等。這些模式基本上都包含了如下定義
SwitchOn
SwitchOff
Down
Move
Up
這裏的 SwitchOn 和 SwitchOff 表示模式的開啓和關閉。在用戶進行選擇模式的以前應該開啓選擇模式,簡單的業務就是我有一個控制條,控制條上面有三個按鈕,包含了選擇、書寫、橡皮擦三個。在沒有點擊選擇按鈕的時候,此時就不該該讓選擇模式工做。那麼選擇模式如何知道本身當前沒有被選中?難道去監聽按鈕的狀態?其實經過上面的 API 設計能夠看到 SwitchOn 和 SwitchOff 就是用來解決模式的開啓和關閉,讓模式內部狀態能夠了解到當前的模式是否開啓,是否須要處理業務
更進一步的是輸入內容的轉發,假設一個模式不開啓,此時這個模式是否應該收到輸入。當前個人設計是若是這個模式沒有開啓,就不要讓這個模式收到輸入。因而這個功能又須要框架的支持啦
這個框架裏面對模式的輸入的控制能夠放在模式控制器這個類裏面,接下來講的模式切換也是這個類應該實現的功能
模式切換最簡單的切換是用戶的行爲切換,用戶點擊了選擇按鈕就告訴白板框架當前要切換爲選擇模式,用戶點擊了書寫按鈕就告訴白板框架當前要切換爲書寫模式。而各個模式的切換是須要框架層面的支持的
按照上文輸入的約定,每一個模式收到的輸入裏面按下和擡起是成對的。而交互模式自己不監聽元素的事件,須要靠框架層轉發。那麼假設在選擇移動的過程,用戶切換了模式,那麼此時當前的模式不是選擇模式,請問選擇模式何時能夠收到擡起事件。請先忽略用戶何時能夠作到在選擇移動的過程當中切換模式
最好的作法是在模式切換的時候,給舊模式補充擡起事件,而給新模式補充按下事件。補充事件的時候有一些細節。補充的事件裏面須要讓補充擡起和按下的點的座標是當前移動的座標,而一樣的在多指觸摸的時候須要補充不止一個按下和擡起才能夠
整個模式切換裏面須要處理的就是多個模式之間的切換,包括切換的舊模式的輸入補充,以及新模式如何接手舊模式的數據。這些數據主要包含了當前模式正在操做的元素,例如選擇了某些元素等。簡單的例子是在選擇模式的時候選擇了一些元素,在切換到書寫模式的時候應該清空選擇,而在切換到 xx 模式的時候就不該該去掉選擇等的這些業務。這部分的業務應該抽象出來,而不是具體的處理如是否清空選擇框等業務,支持各個模式以前的定製
上文有提到用戶在選擇的過程切換了模式,那麼用戶是如何作到切換的?其實這裏涉及了用戶行爲的判斷,一個很現實的是軟件是沒法知道用戶的將來的行爲,而有些行爲判斷須要用戶的多個交互才能肯定。最簡單的例子,可是可能行業外的小夥伴沒法理解哈,就是一個黑板擦功能,或者叫手勢擦除功能,更接地氣的手背擦除功能。這是一個什麼功能?就是當我使用手背觸摸屏幕的時候我指望如今是進行擦除筆跡,這個行爲就和在黑板上同樣,我用粉筆寫字,我用手背擦除
這個功能存在什麼問題呢?從軟件的角度上,在第一時刻,我收到了一個點。在第二時刻,我收到了這個點在移動。此時軟件的模式假設是選擇模式,那麼是否是就開始選擇模式的移動了。沒錯,從邏輯上講應該是這樣的。在第三時刻,我收到了這個點的寬度變大。而在第三時刻我收到的這個點的寬度是知足了手背擦除的觸摸面積,應該切換到手勢擦除模式裏面。當前手勢擦除和擦除模式自己是相同的模式,只是由於用戶行爲不一樣叫法不一樣而已
那麼此時問題來了,請問誰處理模式的切換,或者說如何知道模式應該切換?由於軟件是不知道用戶將來的行爲,而用戶在行爲過程可讓軟件判斷出用戶想要的模式。那麼就須要一個輸入過濾層,這個輸入過濾層能夠決定以後的模式切換到哪裏,或者說輸入傳輸到哪裏
在用到輸入過濾以前還須要先聊一下這個業務,在用戶進行手勢擦除完成以後,在擡手以後須要結束手勢擦除模式。下一次進行交互的時候應該回到上一個模式。如上一個模式是選擇模式,那麼在手勢擦除結束以後的下一次模式應該回到選擇模式。如上一個模式是筆跡書寫模式,那麼在手勢擦除結束以後的下一次模式應該回到筆跡書寫模式
上面這個業務的需求也就是框架層面須要支持一個是當前的模式,另外一個是激活模式。什麼是當前模式,當前模式就是用戶選擇的行爲,也叫主模式。就是用戶當前主要在使用的模式,如進行選擇或進行書寫等。而激活模式是用戶瞬時的一個交互行爲,通常來講這個行爲都是根據用戶的行爲做出的判斷切換到另外一個模式裏面,如手勢擦除等模式
爲何會放兩個不一樣的模式?由於激活模式能夠用來取代當前模式收到交互輸入,而當前模式保留實例等待激活模式關閉以後再次激活。默認行爲都是當前模式,而輸入過濾層,能夠在收集到必要的行爲的時候更改激活模式,開啓激活模式,將框架層的用戶交互輸入傳輸到激活模式中,關閉當前模式
輸入過濾層的做用就是決定輸入數據的流向,讓交互輸入數據走向 CurrentMode 當前模式仍是 ActiveMode 激活模式
經過上面的業務能夠了解到,激活模式 ActiveMode 與當前模式 CurrentMode 同時只會有一個生效。而激活模式 ActiveMode 的優先級高於當前模式 CurrentMode 只要 ActiveMode 存在,那麼全部交互輸入數據都應該傳入到 ActiveMode 激活模式中
而在當前模式 CurrentMode 接收用戶輸入過程當中,能夠被 ActiveMode 激活模式打斷
這一點有點難以理解,爲何須要兩個模式?緣由是兩個模式,其中一個表示激活模式表示用戶的瞬時操做,能夠用來給輸入過濾層切換。換句話說是輸入過濾層控制的是 ActiveMode 激活模式。而用戶明確行爲控制的是 CurrentMode 當前模式。使用兩個模式的另外一個緣由是框架內部能夠判斷是否存在 ActiveMode 激活模式決定交互輸入數據是否走向 CurrentMode 當前模式。同時在 ActiveMode 激活模式完成以後,能夠知道切換回的當前模式是哪一個模式
那麼輸入過濾層的定義又是什麼?和模式層相同,輸入過濾層收到的用戶信息也是框架轉發的,也就是 Filter 層和 Mode 層都繼承相同的類 InputProcessor 輸入處理者
在輸入處理者提供觸發輸入函數,也就是在輸入層通過模式控制器以後,轉發的數據到具體的各個 Filter 和 Mode 時,處理轉發數據的基類
回到問題自己,這裏的 Filter 的沒有實際的功能,僅僅是用來決定數據走向,也就是依靠切換爲具體處理業務的某個 Mode 爲 ActiveMode 完成業務。如手勢擦除就應該配套一個 EraserGestureFilter 來判斷用戶觸摸點的面積是否能夠觸發手勢擦除,如能夠觸發,那麼將 ActiveMode 設置爲橡皮擦模式
那麼能夠被做爲 ActiveMode 的模式是否須要是特殊模式?從上面的例子就能夠看出,原本能夠做爲當前模式的橡皮擦模式在手勢擦除的時候被做爲了手勢擦除模式。也就是模式自己不該該關心本身是被當前是 CurrentMode 仍是 ActiveMode 激活模式,模式只關心輸入的數據的業務處理
經過了框架的數據轉發和 Filter 決定數據走向就能完成輸入切換的功能,在沒有界面功能的時候能夠依靠用戶的行爲給軟件定義出更多模式
還有一個問題,這個方案裏面哪些是屬於不變的框架,哪些屬於業務邏輯?整個輸入層都是框架,這個輸入層解決一些 WPF 觸摸的白板業務問題。注意,這裏的白板業務問題指的是在白板這個行業裏面的業務問題不是說具體的業務哈。模式切換的框架層以及 Filter 和 Mode 的基類實現都是框架層面
而具體的 xx Filter 和 xx Mode 就都是業務了
在白板核心框架設計裏面存在的另外一個坑就是元素自己的交互和通用交互的交互衝突問題
例如我有一個元素這個元素是一個地圖,這個地圖元素支持拖動地圖內容,就和小夥伴用高德地圖同樣的交互。可是通用的交互裏面由包含了拖拽元素的行爲,也就是能夠拖動一個元素。這兩個行爲是交互衝突的,當用戶在地圖元素上面拖動的時候,請問用戶是想拖動地圖元素仍是想拖動地圖
這部分行爲就須要具體的業務定了,可是業務定下以後是否框架層能支持?其實仍是能夠的,經過設計交互優先級能夠解決此問題
假設當前的業務需求是用戶在地圖元素上面拖動的時候,應該拖動地圖而不該該拖動元素
在上面的設計在有 Filter 和 ActiveMode 就能夠解決此問題。若是某些元素的交互的優先級是大於通用交互的優先級的,那麼這些元素能夠經過設置特殊的屬性,在 Filter 層經過判斷當前命中的元素包含了這個特殊的屬性,就能夠設置 ActiveMode 爲一個什麼都不作的 NoMode 模式
按照框架的設計,當存在 ActiveMode 時,將會忽略 CurrentMode 的行爲,也就是此時是一個什麼都不作的 NoMode 模式,用戶的行爲落到了元素上,用戶能夠拖動地圖。而由於當前模式選擇模式沒有收到數據,也就不會拖動元素
因此只須要再定義一個 Filter 讓這個 Filter 處理元素交互衝突問題就能夠了
而又有另外一個問題,用戶若是是在地圖元素上進行手勢擦除呢。假設當前業務需求是手勢擦除優先,當前是手勢擦除不要拖動地圖
而手勢擦除在軟件層面其實也是移動,那麼能夠如何作,剛纔的 Filter 已經判斷了命中元素就激活了一個 NoMode 了
其實只須要引入 Filter 的優先級就能夠解決此問題,讓手勢擦除 Filter 的優先級大於元素交互衝突 ExclusiveModeFilter 的優先級。此時手勢擦除 Filter 就會設置 ActiveMode 爲橡皮擦模式,在 ExclusiveModeFilter 判斷若是存在 ActiveMode 了,也就是存在優先級更高的 Filter 知足條件,那麼 ExclusiveModeFilter 就知道當前應該禁用元素的交互,能夠經過設置元素不可命中等讓元素收不到交互
其實上面有一個細節是手勢擦除判斷通常都會比 ExclusiveModeFilter 判斷慢,緣由是第一個點按下的時候,元素交互衝突 ExclusiveModeFilter 就判斷了命中的元素了,可是手勢擦除判斷須要等待第N個點按下才知道。不過這些細節問題都很好處理,本文上面的例子僅僅只是爲了方便理解
這就是整套白板類應用的模式交互設計方案。裏面的細節特別多,每一個細節其實都須要大量的開發。如今是 2020.5 這個白板框架有 27,197 次 commit 和 300 屢次 NuGet 版本發佈。本文說到的模式交互僅僅是這個白板框架的核心一部分