UI 最前線

マルチタッチの慣性

Charles Petzold

コード サンプルのダウンロード

コンピューターの UI は、基盤となるテクノロジのデジタルな性質を隠して、実世界のアナログな雰囲気を再現したときに、最大の魅力を発揮するようです。この傾向は、グラフィカル インターフェイスがコマンド ラインに代わって普及するようになった時期から見られるようになり、それ以降も、写真や音声などのメディアの使用増加により継続しています。

ビデオ ディスプレイにマルチタッチが組み込まれたことで、ユーザー コントロールをより実世界の操作に近くて直感的なものにする傾向に、さらに拍車がかかりました。ですが、皆さんはコンピューター画面との関係を一新するタッチ操作革命の可能性を垣間見るようになったばかりです。

おそらく、私たちはいつか、ボタン式のボリューム コントロールという、世に知られているデジタル テクノロジの最も見苦しい遺物の 1 つを排除することになるでしょう。ラジオやテレビのボリューム (および他の設定) を調整するダイヤルはとても単純でしたが、ダイヤルの代わりにリモコンに配置された使いにくいボタンやリモコン自体が使用されるようになりました。

多くの場合、コンピューターのインターフェイスには、ボリューム コントロールのスライダーが備わっています。このスライダーは、ダイヤルと同じくらいすばらしい機能です。ですが、ボタン式のボリューム コントロールも依然として存在しています。Zune HD のマルチタッチ スクリーンでも、ボリュームの調整には、プラス記号 (+) とマイナス記号 (-) を押す必要があります。ですが、このような機能には、タッチ操作に反応するダイヤルの方が適しています。

個人的には、マルチタッチ対応のダイヤルが気に入っています。私が執筆した MSDN Magazine 2010 年 3 月号の記事「Finger Style: Silverlight でのマルチタッチ サポートの詳細」(msdn.microsoft.com/ja-jp/magazine/ee336026) でダイヤルのシミュレーションに重点を置いたのも、Windows Presentation Foundation (WPF) におけるマルチタッチの慣性の扱いについて説明するために再びダイヤルを取り上げているのも、そのためです。

慣性のイベント

マルチタッチ インターフェイスでは、実世界を再現する方法の 1 つとして慣性を導入しています。慣性とは、摩擦などの他の力の影響を受けない限り、同じ速度を維持しようとする物体の性質のことです。マルチタッチ インターフェイスでは、慣性により、指を画面から離してもビジュアル オブジェクトは移動し続けます。タッチの慣性が最も一般的に利用されるのはリストの操作で、WPF の ListBox コントロールには既に慣性が組み込まれています。

今回のコラムのダウンロード可能なコードの中には、ListBoxDemo という小さなプログラムが含まれています。これは、マルチタッチ ディスプレイで WPF の ListBox コントロールをテストできるように、単に ListBox コントロールに多数の項目を追加するプログラムです。

慣性が必要な場合、独自の WPF コントロールでどの程度の慣性が必要なのかを自分で判断する必要があります。直接的な操作と同じ反応がプログラムから返されるようにする場合、慣性の使用は簡単です。難しいのは、必要な数値について十分に理解することです。

以前の記事で説明したように、WPF 要素には、ManipulationStarting (初期化の実行に適したタイミングで発生します) および ManipulationStarted という 2 つのイベントによりマルチタッチ操作の開始が通知されます。この 2 つのイベントの後には、非常に多くの ManipulationDelta イベントが発生する可能性があります。ManipulationDelta イベントでは、1 本または複数の指による操作を変換、拡大/縮小、および回転という情報にまとめます。

すべての指が操作中の要素から離れると、ManipulationInertiaStarting イベントが発生します。このタイミングで、どの程度の慣性が必要かをプログラムで指定します (その方法については、後ほど説明します)。慣性の効果としては、オブジェクトが "停止" して ManipulationCompleted イベントが終了を通知するまで、ManipulationDelta イベントが発生し続けます。ManipulationInertiaStarting イベントに対して何も操作を行わないと、このイベントの直後に ManipulationCompleted イベントが発生します。

ManipulationDelta イベントを使用して直接的な操作と慣性を示すことで、この手法が全体的に非常に使いやすくなります。基本的に、慣性は ManipulationInertiaStarting イベントで 1 つのステートメントを使用することで実装できます。必要な場合は、ManipulationDeltaEventArgs イベントの IsInertial プロパティの値から、直接的な操作と慣性の違いを見分けることができます。

これから説明するように、必要な慣性の程度は 2 つの異なる方法のいずれかで指定しますが、どちらの方法にも減速度 (速度が 0 になって物体の動きが止まるまでの一定期間における速度の減速率) が関係します。これには、ほとんどのプログラマがあまり遭遇しない、物理学の分野のいくつかの概念が関係するので、次のセクションで簡単に説明します。

加速度の復習

動いている物体には、時間あたりの距離という単位で表現できる "速度" があると言われています。時間の経過と共に速度自体が変化する場合、その物体には "加速度" があります。通常、(減速速度を示す) 負の加速度は、"減速度" と呼ばれます。

今回のコラムでは、加速度自体または減速度自体は一定であると仮定します。一定の加速度では、速度が直線的に変化する (時間単位あたりの変化量が同じ) ということになります。加速度が 0 の場合、速度は一定のまま変化しません。

たとえば、自動車メーカーは、自社の自動車が一定の秒数 (たとえば、8 秒) 内に時速 0 km から 100 km まで加速できると宣伝することがあります。その場合、自動車は最初は停止していますが、8 秒後には時速 100 km になります (図 1 参照)。

図 1 加速度

秒数 速度 (km/h)
0 0
1 12.5
2 25
3 37.5
4 50
5 62.5
6 75
7 87.5
8 100

毎秒同じ変化量で速度が増加していることに注目してください。加速度は、特定の期間における速度の変化 (この場合は時速 12.5 km) として表されます。

時間と秒という 2 つの時間の単位が関係しているため、この加速度の値は少し厄介です。時間を使用するのはやめて、秒だけを使用しましょう。100 km/h は約 28 m/s に換算されるため、自動車は 8 秒間に 0 m/s から 28 m/s まで加速すると言うことができます。この場合の加速度は 3.5 m/s/s (3.5 m 毎秒毎秒) です。

初めのように km と時間を使用することもできますが、加速度を計算すると 45,000 km/h/h というおかしな値になります。つまり、自動車は 1 時間後に 45,000 km/h で移動していることになります。もちろん、これは事実と異なります。実世界では、自動車は 100 km/h 程度に達すると速度が横ばい状態になり、加速度が 0 まで低下します。これは加速度の変化であり、工学の分野では "加加速度" と言われます。

しかし、ここでは等加速度を想定しています。速度 0 から始まり、加速度 a で時間 t の間に進む距離 x は、次の公式で求められます。

image: equation, x equals one-half a t squared

速度 v は、時間に対する距離の一次導関数として計算されるため、½ と 2 乗が必要になります。

image: equation, v equals d x over d t equals a t

a が 3.5 m/s/s で、t が 1 秒の場合、速度は 3.5 m/s になりますが、自動車は 1.75 m しか進まないことになります。t が 2 秒で、速度が 7 m/s になると、自動車は 7 m 進みます。自動車は、1 秒ごとに、その時間内の平均速度に基づいた距離だけ進みます。t が 8 秒で、速度が 28 m/s の場合、自動車は合計で 112 m 進みます。

マルチタッチの慣性は、自動車の場合と逆だと考えられます。スクリーンから指が離れた時点で、オブジェクトには、ある一定の速度がありますが、アプリケーションによって、オブジェクトの速度を直線的に 0 まで低下させる減速度が指定されます。減速度が大きいほど、オブジェクトはすばやく停止します。減速度 が 0 の場合、オブジェクトは同じ速度でずっと動き続けます。

減速させる 2 つの方法

ManipulationDelta イベントの引数には ManipulationVelocities 型の Velocities プロパティが含まれ、このプロパティ自体には、3 種類の操作に相当する次の 3 つのプロパティがあります。

  • Vector 型の LinearVelocity
  • Vector 型の ExpansionVelocity
  • double 型の AngularVelocity

最初の 2 つのプロパティの値は、デバイスに依存しないミリ秒あたりの単位で表され、3 つ目のプロパティの値は、ミリ秒あたりの角度 (deg/ms) で表されます。もちろん、ミリ秒という時間は直感的にわかりにくいので、ミリ秒の値を見てその感覚をつかむ必要がある場合は、値を 1,000 で乗算して秒単位に変換することをお勧めします。

アプリケーションで Velocity プロパティが使用されることはあまりありませんが、このプロパティでは慣性の初期速度を指定します。ユーザーがスクリーンから指を離すと、ManipulationInertiaStarting イベントが発生します。このイベント引数には、次の 3 つのプロパティが含まれます。これらのプロパティを使用して、同じ 3 種類の操作から切り離して慣性を指定できます。

  • InertiaTranslationBehavior 型の TranslationBehavior
  • InertiaExpansionBehavior 型の ExpansionBehavior
  • InertiaRotationBehavior 型の RotationBehavior

上記の各クラスには、InitialVelocity プロパティと DesiredDeceleration プロパティに加えて、もう 1 つのプロパティがあります。3 つ目のプロパティは、クラスによって名前が異なり DesiredDisplacement、DesiredExpansion、または DesiredRotation という名前になります。

変換と回転の場合、Desired プロパティに Double.NaN という既定値があります。これは、"数値以外" を示す特別なビット構成です。拡大の場合、Desired プロパティは X と Y の値が Double.NaN の Vector 型になりますが、概念は同じです。

まず、回転の慣性に注目しましょう。というのも、これは、実世界でよく見られる効果 (遊園地のメリーゴーランドなど) で、スクリーンからはみ出しまうオブジェクトについて考慮する必要がないからです。

ManipulationInertiaStarting イベントが発生するまでに InertiaRotationBehavior オブジェクトが作成され、直前の ManipulationDelta イベントによって InitialVelocity プロパティの値が設定されます。たとえば、InitialVelocity プロパティの値が 1.08 の場合、これは 1 ミリ秒あたり 1.08 度 (1 秒あたり 1,080 度)、つまり 1 秒あたり 3 回転 (180 rpm) することを意味します。

オブジェクトを回転させ続けるには、DesiredRotation プロパティと DesiredDeceleration プロパティのどちらか 1 つだけに値を設定します。両方のプロパティに値を設定すると、後から設定したプロパティが有効になり、先に設定したプロパティの値は Double.NaN になります。

1 つ目の方法として、DesiredRotation プロパティに角度の値を設定できます。オブジェクトはこの角度に達するまで回転します。たとえば、DesiredRotation プロパティに 360 という値を設定すると、回転しているオブジェクトは、あと 1 回だけ回転し、その 1 回転の間に減速して停止します。この方法の利点は、初期速度に関係なく慣性が作用するので、どのように動作するかを予測しやすいことです。ただし、若干不自然だという欠点があります。

2 つ目の方法としては、DesiredDeceleration プロパティに deg/ms/ms 単位の値を設定することが可能ですが、適切な値を推測するのが難しいので、やや信頼性に欠ける方法になります。

InitialVelocity プロパティの値が 1.08 deg/ms の場合、DesiredDeceleration プロパティの値を 0.01 deg/ms/ms に設定すると、速度は 1 ミリ秒ごとに 0.01 度ずつ減速します。つまり、1 ミリ秒後には 1.07 deg/ms、2 ミリ秒後には 1.06 deg/ms というように、速度が 0 になるまで直線的に減速します。プロセス全体が完了するまでには 108 ms (0.1 秒程度) かかります。

それよりも小さい値 (0.001 deg/ms/ms 程度) を DesiredDeceleration プロパティに設定すると、オブジェクトは 1.08 秒間回転し続けるのでわかりやすくて良いでしょう。

減速度の単位を、人間がわかりやすい単位に変換する場合は注意が必要です。1.08 deg/ms という速度は 1,080 deg/s と同じですが、0.001 deg/ms/ms という減速度は 1,000 deg/s/s になります。減速度を秒単位に変換するには、時間が 2 乗されるので、1,000 で "2 回" 乗算する必要があります。

前述の 2 つの公式を組み合わせて t を削除すると、次のような公式ができあがります。

Image: equation, a equals v squared over 2 x

この公式は、減速度を設定する前述の 2 つの方法が同等で、双方向に変換できることを示しています。InitialVelocity プロパティの値が 1.08 deg/ms の場合、DesiredDeceleration プロパティに 0.001 deg/ms/ms を設定することは、DesiredRotation プロパティに 583.2 度を設定することと同じです。どちらの場合でも、回転は 1,080 ミリ秒後に停止します。

実験

回転の慣性がどういうものか感覚をつかむために、RotationalInertiaDemo プログラムを作成しました (図 2 参照)。

image: RotationalInertiaDemo Program in Action

図 2 RotationalInertiaDemo プログラムの動作

左側のホイールは、時計回りまたは反時計回りに指で回転させます。これは非常に単純で、MainWindow に渡されるすべての Manipulation イベントを含む Grid に 2 つの Ellipse 要素が配置された UserControl の派生クラスに過ぎません。

次のように、ManipulationStarting イベントでは、操作を回転のみに制限し、1 本の指による回転を許可して、回転の中心を設定することで初期化を実行します。

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

右側の速度計は ValueMeter と呼ばれるクラスで、ホイールの現在の速度を表示します。どこかで見たことがあると感じた方がいらっしゃるかもしれませんが、それはこのホイールが、3 年以上前に私が執筆した MSDN Magazine の記事で紹介した ProgressBar テンプレートの拡張版だからです。機能強化によってラベルの柔軟性が向上したので、4 つの異なる単位で速度を表示できます。ウィンドウの中央にある GroupBox コントロールで単位を選択できます。

指でダイヤルを回しているときには、ManipulationDelta イベント引数の Velocities.AngularVelocity サブプロパティから取得される現在の角速度がメーターに表示されます。ただし、ダイヤルの速度を ValueMeter クラスに直接渡すことはできませんでした。そのため、非常に面倒なことに、0.25 秒前から現在までに取得したすべての値の加重平均を計算する小さな 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 という ID が設定された、1 つの楕円だけで構成されています。OnManipulationStarting メソッドのオーバーライド中は、変換のみが有効です。OnManipulationInertiaStarting メソッドでは、慣性減速度を次のように設定します。

args.TranslationBehavior.DesiredDeceleration = 0.0001;

これは、デバイスに依存しない毎ミリ秒毎ミリ秒あたりの単位が 0.0001 (毎秒毎秒あたりの単位が 100) つまり約 1 インチ/s/s という意味です。

OnManipulationDelta メソッドのオーバーライドを図 3 に示します。IsInertial プロパティが true の場合に、特別な処理が実行されることに注目してください。このコードの背景には、楕円の一部が画面からはみ出したときに変換率を減衰させる必要があるという概念があります。減衰率は、楕円が mainGrid 内にある場合は 0 で、楕円の一部が mainGrid の境界を越えると 1 まで上昇します。その後、減衰率は usableTranslate を計算するために、メソッドに渡される変換ベクトル (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 メソッドが呼び出され、その際、変換ベクトルの未使用領域が ReportBoundaryFeedback メソッドに渡されます (この領域は totalTranslate から usableTranslate を差し引いた領域です)。既定では、楕円の回転が減速するとウィンドウが少し跳ねるように処理されます。これは、"すべての作用に対して、大きさが等しく、方向が反対の反作用がある" という物理法則を実証しています。

この効果は、境界に達するときの速度が非常に速い場合に最もよく働きます。楕円が目に見えて減速した場合は、振動効果が働きます。これはあまり十分な効果とはいえませんが、細かく調整することが可能です。振動効果が必要ない場合 (または一部の状況でのみ必要な場合)、ReportBoundaryFeedback メソッドの呼び出しを回避できます。または、ManipulationBoundaryFeedback イベントを自分で処理することができます。既定の処理が実行されないようにするには、ManipulationBoundaryFeedback イベント引数の Handled プロパティを true に設定するか、別の方法を使用します。BoundaryDemo プログラムには、空の OnManipulationBoundaryFeedback メソッドを用意したので、ぜひご活用ください。

Charles Petzold は MSDN Magazine の記事を長期にわたって担当している寄稿編集者です。現在は、『Programming Windows Phone 7』(Microsoft Press、2010 年) を執筆中です。この書籍は、無償のダウンロード可能な電子書籍として、2010 年秋に公開されます。現時点では、彼の Web サイト (charlespetzold.com、英語) で公開中です。

この記事のレビューに協力してくれた技術スタッフの Doug KramerRobert Levy に心より感謝いたします。