本文章是由機器翻譯。

UI 前沿技術

多點觸控延時

Charles Petzold

下載代碼示例

電腦上的 UI 很炫,它們將底層技術的數位本質隱藏起來,取而代之類比真實生活的感覺。這種趨勢始自圖形介面開始取代命令列時,然後隨著照片、聲音和其他媒體使用量的增加而一直在延續。

將多點觸控融入視頻顯示器極大地推進了使使用者控制項更逼真且更直觀的進程。我想,您已經開始感覺到觸摸革新可能會改變您與電腦螢幕之間的關係。

或許某一天,我們甚至會淘汰眾所周知最不美觀的殘餘數位技術:按鈕式音量控制項。收音機和電視機上控制音量(和其他設置)的基本調節盤就非常簡單,但已被遙控器以及電器本身上並不美觀的按鈕所替代。

電腦介面通常使用滑塊來控制音量。這些幾乎與調節盤一樣好用。但是按鈕式音量控制項仍然存在。甚至 Zune HD 的多點觸控螢幕也希望您按加號和減號來增大和降低音量。更好的將是回應觸摸的調節盤。

我喜歡用調節盤來實現多點觸控。或許,這就是為什麼我在文章?手指之舞: 探討 Silverlight 中的多點觸控支援”(刊登在 2010 年三月出版的*《MSDN 雜誌》* 上:msdn.microsoft.com/magazine/ee336026)中著重調節盤類比,以及為什麼我要重返調節盤以探討如何在 Windows Presentation Foundation (WPF) 中處理多點觸控延時的原因。(msdn.microsoft.com/magazine/ee336026), 中著重調節盤類比,以及為什麼我要重返調節盤以探討如何在 Windows Presentation Foundation (WPF) 中處理多點觸控延時的原因。

延時事件

多點觸控介面嘗試類比現實世界的方法之一是引入延時 — 物件維持同一速度的傾向,除非受到外力作用,如摩擦。在多點觸控介面中,延時可以在手指離開螢幕後使可視物件保持移動。最常見的觸摸延時應用是導航清單,延時已內置於 WPF ListBox 中。

在本文可下載的代碼中,有一個稱為 IntertialListBoxDemo 的小程式,它只是向 WPF ListBox 中填充了一些項,您可以在自己的多點觸控顯示器上測試此程式。

在您自己的 WPF 控制項中,您需要確定所需延時的多少(如果有的話)。如果只是希望延時在直接操作時從您的程式引發同樣的回應,那麼延時的處理非常簡單。比較難的部分是深入瞭解涉及到的數值。

正如您在之前的文章中所見,WPF 元素通過兩個事件獲知多點觸控操作的開始:ManipulationStarting(這是執行一些初始化的良機)和 ManipulationStarted。這兩個事件之後,可能是非常多的 ManipulationDelta 事件,這些事件會將一根、兩根或多根手指操作整合為平移、縮放以及旋轉資訊。

當所有手指都離開操作的元素後,ManipulationInertiaStarting 事件發生。這是您的程式指定所需延時的機會。(馬上我就會對此進行介紹。)延時的效果以其他 ManipulationDelta 事件的形式表現,直至物件?停止?,ManipulationCompleted 事件發出結束信號。如果您不採用 ManipulationInertiaStarting,其後將立即出現 ManipulationCompleted。

使用 ManipulationDelta 事件指示直接操作和延時會極大地簡化這一整個架構的使用。基本上,您可以在 ManipulationInertiaStarting 事件中通過一個語句實現延時。如果有必要,您可以根據 ManipulationDeltaEventArgs 的 IsInertial 屬性辨別直接操作和延時之間的不同。

正如您將看到的,可以使用兩種不同方式之一指定所需延時,但是這兩種方法都涉及到減速度 — 一段時間內速度都會持續降低,直至速度達到零,物件停止移動。其中將涉及到一些程式師不常遇到的物理概念,因此,可能只有少許的複習課程會有所説明。

加速度回顧

如果某物體正在移動,就稱其具有 速度速率 ,可以用單位時間內移動的距離來表示。如果速率本身在一段時間內不斷變化,則該物件就具有加速度 。負加速度(指示不斷減小的速率)通常稱為 減速度

在此探討中,我們假定加速度或減速度本身是恒定不變的。恒定的加速度意味著速率的變化呈線性 — 單位時間內的變化量相等。如果加速度為 0,則速率保持恒定。

例如,汽車製造商有時候在推廣其產品時會說,其生產的汽車能夠在一定秒數(比如說 8 秒)內從 0 加速到 60 mph。汽車一開始處於停止狀態,但在 8 秒結束後,其速度將達到 60 mph,如圖 1 所示。

图 1 加速度

速度
0 0 mph
1 7.5
2 15
3 22.5
4 30
5 37.5
6 45
7 52.5
8 60

請注意,每秒速率增加的量是相同的。加速度表示速率在一特定時間段內的變化,在本例中,即為每秒 7.5 mph。

該加速度值理解起來有點困難,因為其中涉及到了兩個時間單位:小時和秒。.讓我們捨棄小時,只使用秒。因為 60 mph 是 88 英尺/秒,所以可以這麼說,汽車在 8 秒內從 0 加速到 88 英尺/秒。加速度是每秒 11 英尺/秒,或(通常是這麼說)11 英尺/ 平方 秒。

我們可以轉換為英里和小時,但是將加速度計算為 27,000 英里/平方小時有些荒謬。這意味著在一小時結束時,汽車將以 27,000 mph 速度行駛。當然,這是不可能實現的。在實際生活中,當汽車達到 60 mph 左右時,速率穩定下來並且加速度回歸至 0。這是加速度的變化,在工程界稱為 躍度

但對於本練習,我們假定加速度恒定。从速率为 0 开始,加速度为 a ,在时间 t 内行驶的距离为 x ,其结果是:

½ 和平方都是必需的,因為速率 v 是作為與時間對應的距離的第一個導數進行計算的:

如果 a 是 11 英尺/平方秒,則 t 等於 1 秒,速率為 11 英尺/秒,但汽車只行駛了 5.5 英尺。At t equal to 2 seconds, the velocity is 22 feet per second, and the car has traveled a total of 22 feet.每秒內,汽車行駛的距離根據該秒內的平均速率計算。在tequal到8秒,速度是每秒88英尺,車子已經走了總352英尺.

多點觸控延時就像反過來看汽車一樣:當手指離開螢幕時,物件具有一定的速率。應用程式指定一個可使物件速率線性減至 0 的減速度。減速度愈大,物件停止得愈快。減速度為 0 將導致物件以同樣的速率永遠移動下去。

兩種減速方式

ManipulationDelta 事件參數包括類型為 ManipulationVelocities 的 Velocities 屬性,其本身具有與三種操作類型對應的三個屬性:

  • LinearVelocity,類型為 Vector
  • ExpansionVelocity,類型為 Vector
  • AngularVelocity,類型為 double

前兩個屬性工作表示為設備無關單位/毫秒;第三個屬性以角度(度)/毫秒為單位。當然,毫秒不是那種易理解的直觀時間段,因此,如果需要查看這些值並瞭解其含義,您需要將其乘以 1,000 以換算為秒。

應用程式不常使用這些 Velocity 屬性,但它們為延時提供了初始速率。使用者手指離開螢幕後,ManipulationInertiaStarting 事件發生。該事件參數包括以下三個屬性,可用於為那些相同的三種操作類型分別指定延時:

  • TranslationBehavior,類型為 InertiaTranslationBehavior
  • ExpansionBehavior,類型為 InertiaExpansionBehavior
  • RotationBehavior,類型為 InertiaRotationBehavior

其中每個類都具有 InitialVelocity 屬性、DesiredDeceleration 屬性以及第三個名為 DesiredDisplacement、DesiredExpansion 或 DesiredRotation 的屬性(具體取決於該類)。

對於平移和旋轉,所有 Desired 屬性都具有 Double.NaN 的預設值,即指示“不是數位”的特殊位配置。對於擴展,Desired 屬性的類型均為 Vector,具有 Double.NaN 的 X 和 Y 值,但道理是一樣的。

我們首先來看旋轉延時,因為其效果類似真實世界(如操場環形跑道),您不必擔心物件飛出螢幕。

當您看到 ManipulationInertiaStarting 事件發生時,InertiaRotationBehavior 物件已創建,InitialVelocity 值已在之前的 ManipulationDelta 事件中進行了設置。例如,如果 InitialVelocity 為 1.08,即 1.08 度/毫秒或 1,080 度/秒,或三周/秒,或 180 rpm。

若要保持物件旋轉,可設置 DesiredRotation 或 DesiredDeceleration,但不能設置這兩者。如果嘗試設置這兩者,後一個設置將生效,另一個將是 Double.NaN。

第一個選項是將 DesiredRotation 設置為以度數表示的值。這是物件在停止前旋轉的度數。例如,如果將 DesiredRotation 設置為 360,旋轉物件將額外多轉一圈,並在此過程中逐漸減速直至停止。優點是您獲得了相同量的延時活動(無論初始速度如何),因此,可以輕鬆預測將要發生的情況。缺點是這不太自然。

另一種方式是將 DesiredDeceleration 設置為以度/平方毫秒單位表示的值,這有點難,因為難於猜測出一個比較適合的值。

如果 InitialVelocity 是 1.08 度/毫秒,而您將 DesiredDeceleration 設置為 0.01 度/平方毫秒,則速率將每毫秒減少 0.01 度:在第一毫秒結束時減少到 1.07 度/毫秒,在第二毫秒結束時減少到 1.06 度/毫秒,以此類推,速率線性降低直至為 0。整個過程將花費 108 毫秒,或比一秒的十分之一多點。

您可能希望將 DesiredDeceleration 設置為比此值更小的值,或許是 0.001 度/平方毫秒,這將使物件連續旋轉 1.08 秒。

如果您希望將減速度單位轉換為大家更易於思考的某種單位,請謹慎行事。速率 1.08 度/毫秒等同于 1,080 度/秒,但 0.001 度/平方毫秒的減速度則等同于 1,000 度/平方秒。若要將減速度轉換為秒,您需要乘以 1,000 兩次 ,因為時間是經過平方計算的。

如果將之前演示的兩個公式合併,並去除 t ,您將獲得:

這意味著設置減速度的兩種方法是等同的,可以相互轉換。如果 InitialVelocity 是 1.08 度/毫秒,而您將 DesiredDeceleration 設置為 0.001 度/平方毫秒,則這等同于將 DesiredRotation 設置為 583.2 度。在任一示例中,旋轉都將在 1,080 毫秒後停止。

實驗

為了瞭解旋轉延時,我構建了一個 RotationalInertiaDemo 程式,如圖 2 所示。

圖 2 運行中的 RotationalInertiaDemo 程式

左邊的輪形圖是您用手指在轉動,順時針或逆時針均可。它非常簡單:只是一個 UserControl 派生類,在 Grid 中有兩個 Ellipse 元素,所有 Manipulation 事件都在 MainWindow 中處理。

ManipulationStarting 事件通過將操作限制為僅旋轉、允許單根手指旋轉以及設置旋轉中心來執行初始化:

args.IsSingleTouchEnabled = true;
args.Mode = ManipulationModes.Rotate;
args.Pivot = new ManipulationPivot(
             new Point(ctrl.ActualWidth / 2, 
                       ctrl.ActualHeight / 2), 50);

右側的速度計是一個稱為 ValueMeter 的類,上面顯示了當前的輪速率。 如果這看起來有點眼熟,那僅僅是因為這是我三年前為該雜誌撰寫的一篇文章中用到的 ProgressBar 範本的增強版。 增強的地方包括通過標籤增加了一些靈活性,這樣我可以用它以四種不同單位顯示速率。 視窗中部的 GroupBox 可供您選擇這些單位。

當您的手指旋轉調節盤時,該儀錶盤將顯示 ManipulationDelta 事件參數的 Velocities.AngularVelocity 子屬性提供的當前角速度。 但我發現,要將調節盤的速率直接移至 ValueMeter 是不可能的。 結果太讓人緊張不安。 我必須編寫一個小 ValueSmoother 類從第四分之一秒處開始執行所有值的加權平均值。 ManipulationDelta 事件處理常式還將設置一個實際旋轉調節盤的 RotateTransform 物件:

rotate.Angle += args.DeltaManipulation.Rotation;

最後,可以使用底部的滑塊選擇減速度值。 僅在手指離開調節盤並且 ManipulationInertiaStarted 事件觸發後,才會讀取滑塊的值:

args.RotationBehavior.DesiredDeceleration = slider.Value;

這是 ManipulationInertiaStarted 事件處理常式的整體內容。 在操作的延時階段期間,速率值非常有規律,不必進行調整,因此 ManipulationDelta 處理常式使用 IsInertial 屬性確定將速率值直接傳遞至儀錶盤的時間。

邊界和回彈

多點觸控中最常用的延時是在螢幕上四處移動物件,如滾動一個長長的清單或快速將元素移至一邊。 一個重要問題是,這樣很容易導致元素飛離螢幕!

但在處理這類小問題的過程中,您會發現,WPF 具有一個內置功能,可以使操作延時更真實。 您之前可能在 ListBoxDemo 程式中通過讓延時滾動至清單的末尾或開頭時已經注意到這點,整個視窗在 ListBox 到達終點時彈回一點。 您在自己的應用程式中也可實現此效果(如果希望)。

BoundaryDemo 程式只包含一個橢圓,其中在名為 mainGrid 的 Grid 中有一個設置為其 RenderTransform 屬性設置的標識 MatrixTransform。 OnManipulationStarting 重寫期間僅支援平移。 OnManipulationInertiaStarting 方法將延時減速度設置為如下所示的值:

args.TranslationBehavior.DesiredDeceleration = 0.0001;

即 0.0001 設備無關單位/平方毫秒,或 100 設備無關單位/平方秒,或大約 1 英寸/平方秒。

OnManipulationDelta 重寫如圖 3 所示。 請注意當 IsInertial 為真時的特殊處理。 這段代碼的意思是,當橢圓部分漂離螢幕時,平移因數應當衰減。 如果橢圓位於 mainGrid 內,則衰減因數為 0,而當橢圓的小部分超過了 mainGrid 的邊界時,則此因數上升至 1。 然後,將此衰減因數應用到傳遞至該方法的平移向量(稱為 totalTranslate)以計算 usableTranslate,這將提供實際應用到轉換矩陣的值。

圖 3 BoundaryDemo 中的 OnManipulationDelta 事件

protected override void OnManipulationDelta(
  ManipulationDeltaEventArgs args) {

  FrameworkElement element = 
    args.Source as FrameworkElement;
  MatrixTransform xform = 
    element.RenderTransform as MatrixTransform;
  Matrix matx = xform.Matrix;
  Vector totalTranslate = 
    args.DeltaManipulation.Translation;
  Vector usableTranslate = totalTranslate;

  if (args.IsInertial) {
    double xAttenuation = 0, yAttenuation = 0, attenuation = 0;

    if (matx.OffsetX < 0)
      xAttenuation = -matx.OffsetX;
    else
      xAttenuation = matx.OffsetX + 
        element.ActualWidth - mainGrid.ActualWidth;

    if (matx.OffsetY < 0)
      yAttenuation = -matx.OffsetY;
    else
      yAttenuation = matx.OffsetY + 
        element.ActualHeight - mainGrid.ActualHeight;

    xAttenuation = Math.Max(0, Math.Min(
      1, xAttenuation / (element.ActualWidth / 2)));

    yAttenuation = Math.Max(0, Math.Min(
      1, yAttenuation / (element.ActualHeight / 2)));

    attenuation = Math.Max(xAttenuation, yAttenuation);

    if (attenuation > 0) {
      usableTranslate.X = 
        (1 - attenuation) * totalTranslate.X;
      usableTranslate.Y = 
        (1 - attenuation) * totalTranslate.Y;

      if (totalTranslate != usableTranslate)
        args.ReportBoundaryFeedback(
          new ManipulationDelta(totalTranslate – 
          usableTranslate, 0, new Vector(), new Vector()));

      if (attenuation > 0.99)
        args.Complete();
    }
  }
  matx.Translate(usableTranslate.X, usableTranslate.Y);
  xform.Matrix = matx;

  args.Handled = true;
  base.OnManipulationDelta(args);
}

最終的效果是,橢圓在觸碰到邊界時沒有立即停止,而是明顯地慢下來,就好像碰到了一大塊淤泥一樣。

OnManipulationDelta 重寫還將調用 ReportBoundaryFeedback 方法,以向其傳遞平移因數的未使用部分,即向量 totalTranslate 減去 usableTranslate。 預設情況下,經過處理後,視窗在橢圓慢下來時會回彈一點,以演示物理原理:每個動作都存在一個相反的對等反作用力。

該效果在碰撞速率相當大時尤為明顯。 如果橢圓明顯慢下來,則會出現不太令人滿意的震動效果,而您可能希望進行更為細緻的控制。 如果根本不想要這個效果(或只想在某些情況下使用),只需避免調用 ReportBoundaryFeedback 即可。 或者,您可以自己處理 ManipulationBoundaryFeedback 事件。 您可以通過將該事件參數的 Handled 屬性設置為真來禁用預設處理,或可以採用另一種方法。 我在程式中預留了空的 OnManipulationBoundaryFeedback 方法,您可以自行實驗。

Charles Petzold 是 《MSDN 雜誌》 的長期特約編輯。 他當前正在撰寫《Programming Windows Phone 7》(Microsoft Press),該書將在 2010 年秋季作為可免費下載的電子書發佈。 現在,已通過其網站 charlespetzold.com 提供了預覽版本。

衷心感謝以下技術專家對本文的審閱:Doug KramerRobert Levy