白板類應用的模式交互設計方案

来源:https://www.cnblogs.com/lindexi/archive/2020/05/21/12932681.html
-Advertisement-
Play Games

在一個白板類應用的交互中一定會涉及到模式之間的更換和交互衝突。白板類軟體的交互模式一般包含了筆跡書寫模式,選擇模式,擦除筆跡模式等。多個模式之間存在切換,而切換可以發生在某個模式執行過程,如需要在白板軟體裡面支持筆跡書寫功能,在書寫的過程打斷進入筆跡的擦除模式。本文告訴大家我所在團隊的白板內核的模式... ...


在一個白板類應用的交互中一定會涉及到模式之間的更換和交互衝突。白板類軟體的交互模式一般包含了筆跡書寫模式,選擇模式,擦除筆跡模式等。多個模式之間存在切換,而切換可以發生在某個模式執行過程,如需要在白板軟體裡面支持筆跡書寫功能,在書寫的過程打斷進入筆跡的擦除模式。本文告訴大家我所在團隊的白板內核的模式交互設計方案,本文不會涉及到具體實現的邏輯代碼

我從 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 拖拽事件。此部分僅僅是封裝,方便開發者,不屬於框架核心

在隔離輸入層之後,就可以統一化輸入,在框架層不需要瞭解輸入細節。框架層的輸入前置還需要保證一點的是對某個模式的輸入裡面按下和抬起是成對的,保證輸入裡面一定是先按下再移動再抬起,這個順序不會亂

為什麼這部分保證是在 SourceInput 層之後?原因是這個保證需要處理一些模擬輸入,也就是 SourceInput 層僅封裝 WPF 框架的輸入。不處理模式交互框架裡面的各個模式收到輸入的保證輸入成對

交互模式

每一個不同的交互模式都應該繼承相同的交互模式基類,交互模式指的是如筆跡書寫模式,選擇模式,擦除筆跡模式等。這些模式基本上都包含了以下定義

  • 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 版本發佈。本文說到的模式交互僅僅是這個白板框架的核心一部分


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 6.32(游戲:贏取雙骰子賭博游戲的機會)修改編程練習題6.30使該程式運行10000次,然後顯示贏得游戲的次數 6.32(Game: chance of winning at craps)Revise Exercise 6.30 to run it 15,000 times and display ...
  • 異常有可能導致某些資源沒有釋放到,這時可以使用using或者finally來釋放資源。 有些異常的可能會間歇性的發生,不影響業務運行的,可以視為正常的邏輯,那在異常可能發生前,應該使用判斷邏輯。 if (conn.State != ConnectionState.Closed) { conn.Clo ...
  • SunnyUI.Net 開發日誌:ListBox 增加跟隨滑鼠滑過高亮,這可是在別人兩年的基礎上再改進的,哈哈!~ ...
  • 什麼是Pub Sub 發佈訂閱是一種設計模式,它允許應用程式組件之間進行鬆散耦合。 其實訂閱發佈設計中主要是發佈者生成事件通道,用於在不瞭解任何訂閱者存在的情況下通知訂閱者。 當然委托EventHandlers和Event關鍵字在此事件處理機制中擔任著重要的角色。下麵我們來看看如何使用它們。 Pub ...
  • 在 Office 文檔的一些有趣的設計,顏色和畫刷是可以繼承的,這個繼承包括了屬性的繼承。在形狀填充裡面使用的漸變色是可以一部分屬性放在主題裡面,主要找到主題裡面的畫刷,替換掉形狀自己定義的內容,才是形狀的畫刷 ...
  • 之前版本是通過安裝 Blend SDK 支持 Behaviors 庫的,但是這個方法都是通過引用 dll 的方式,不夠優雅。在升級到 dotnet core 3.0 的時候就需要使用 WPF 官方團隊開源的 Microsoft.Xaml.Behaviors.Wpf 庫代替 ...
  • 本文用的是 世紀互聯 的 Azure.cn 版本,這個版本因為是在國內,所以網速會快超級超級多。使用 世紀互聯 的版本需要一塊錢哦,用一塊錢就能進入一個月的免費試用。本文主要告訴小伙伴如何使用 Azure 無伺服器 Function 函數計算服務 ...
  • 本文主要是來安利大家基於 Azure 的認知服務,主要是文本認知服務,可以做到分析輸入文本的情緒,以及判斷當前輸入文本所屬語言等功能 ...
一周排行
    -Advertisement-
    Play Games
  • 概述:在C#中,++i和i++都是自增運算符,其中++i先增加值再返回,而i++先返回值再增加。應用場景根據需求選擇,首碼適合先增後用,尾碼適合先用後增。詳細示例提供清晰的代碼演示這兩者的操作時機和實際應用。 在C#中,++i 和 i++ 都是自增運算符,但它們在操作上有細微的差異,主要體現在操作的 ...
  • 上次發佈了:Taurus.MVC 性能壓力測試(ap 壓測 和 linux 下wrk 壓測):.NET Core 版本,今天計劃準備壓測一下 .NET 版本,來測試並記錄一下 Taurus.MVC 框架在 .NET 版本的性能,以便後續持續優化改進。 為了方便對比,本文章的電腦環境和測試思路,儘量和... ...
  • .NET WebAPI作為一種構建RESTful服務的強大工具,為開發者提供了便捷的方式來定義、處理HTTP請求並返迴響應。在設計API介面時,正確地接收和解析客戶端發送的數據至關重要。.NET WebAPI提供了一系列特性,如[FromRoute]、[FromQuery]和[FromBody],用 ...
  • 原因:我之所以想做這個項目,是因為在之前查找關於C#/WPF相關資料時,我發現講解圖像濾鏡的資源非常稀缺。此外,我註意到許多現有的開源庫主要基於CPU進行圖像渲染。這種方式在處理大量圖像時,會導致CPU的渲染負擔過重。因此,我將在下文中介紹如何通過GPU渲染來有效實現圖像的各種濾鏡效果。 生成的效果 ...
  • 引言 上一章我們介紹了在xUnit單元測試中用xUnit.DependencyInject來使用依賴註入,上一章我們的Sample.Repository倉儲層有一個批量註入的介面沒有做單元測試,今天用這個示例來演示一下如何用Bogus創建模擬數據 ,和 EFCore 的種子數據生成 Bogus 的優 ...
  • 一、前言 在自己的項目中,涉及到實時心率曲線的繪製,項目上的曲線繪製,一般很難找到能直接用的第三方庫,而且有些還是定製化的功能,所以還是自己繪製比較方便。很多人一聽到自己畫就害怕,感覺很難,今天就分享一個完整的實時心率數據繪製心率曲線圖的例子;之前的博客也分享給DrawingVisual繪製曲線的方 ...
  • 如果你在自定義的 Main 方法中直接使用 App 類並啟動應用程式,但發現 App.xaml 中定義的資源沒有被正確載入,那麼問題可能在於如何正確配置 App.xaml 與你的 App 類的交互。 確保 App.xaml 文件中的 x:Class 屬性正確指向你的 App 類。這樣,當你創建 Ap ...
  • 一:背景 1. 講故事 上個月有個朋友在微信上找到我,說他們的軟體在客戶那邊隔幾天就要崩潰一次,一直都沒有找到原因,讓我幫忙看下怎麼回事,確實工控類的軟體環境複雜難搞,朋友手上有一個崩潰的dump,剛好丟給我來分析一下。 二:WinDbg分析 1. 程式為什麼會崩潰 windbg 有一個厲害之處在於 ...
  • 前言 .NET生態中有許多依賴註入容器。在大多數情況下,微軟提供的內置容器在易用性和性能方面都非常優秀。外加ASP.NET Core預設使用內置容器,使用很方便。 但是筆者在使用中一直有一個頭疼的問題:服務工廠無法提供請求的服務類型相關的信息。這在一般情況下並沒有影響,但是內置容器支持註冊開放泛型服 ...
  • 一、前言 在項目開發過程中,DataGrid是經常使用到的一個數據展示控制項,而通常表格的最後一列是作為操作列存在,比如會有編輯、刪除等功能按鈕。但WPF的原始DataGrid中,預設只支持固定左側列,這跟大家習慣性操作列放最後不符,今天就來介紹一種簡單的方式實現固定右側列。(這裡的實現方式參考的大佬 ...