WPF ベータ2における境界フィードバックについて

今回は、前回のイナーシアで説明が不足していた境界フィードバックについて説明します。基本的には、前回に提示したコードを使用します。最初にManipulationDeltaイベントハンドラに記述したコードをもう一度、以下に提示します。

     // マニピュレーションコンテナの四角形を取得します
    Rect containingRect =
        new Rect(((FrameworkElement)
                   e.ManipulationContainer).RenderSize);
    // 移動後のUI要素の軸並行境界を含む四角形を取得します
    Rect shapeBounds =
        element.RenderTransform.TransformBounds(
            new Rect(element.RenderSize));

    // マニピュレーションコンテナの境界を越えたら
    // イナーシアの操作を停止します
    if (e.IsInertial && 
                     !containingRect.Contains(shapeBounds))
    {
        e.Complete();
        e.ReportBoundaryFeedback(e.DeltaManipulation);
    }

上記のコードから、e.Complete()を削除して実行してみてください。これでReportBoundaryFeedback(境界フィードバック)の動きが理解できると思います。実際に試すと、描画領域の四隅がブルブルと動くのを確認することができます。これは、WPF4のイナーシアにおいて境界フィードバックを行うことでウインドウ自体が操作した方向へ引っ張られるような効果を視覚に訴えるためだと考えられます。
これだけだと面白くないので、以下のようなメソッドを追加してみます。

 // 移動ベクターを計算します
private Vector CalculateOvershoot(UIElement element)
{
    // 要素の軸平行境界ボックスを取得します(実際の描画コレクションの軸平行境界を使用します)
    var elementBounds = element.RenderTransform.TransformBounds(
                        VisualTreeHelper.GetDrawing(element).Bounds);
    double extraX = 0.0, extraY = 0.0;
    // 座標xを計算します  上と左側が跳ね返ります
    if (elementBounds.Left < 0)
       extraX = elementBounds.Left;
    else if (elementBounds.Right > canvas.ActualWidth)
       extraX = elementBounds.Right - canvas.ActualWidth;
    // 座標yを計算します
    if (elementBounds.Top < 0)
       extraY = elementBounds.Top;
    else if (elementBounds.Bottom > canvas.ActualHeight)
       extraY = elementBounds.Bottom - canvas.ActualHeight;

    return new Vector(extraX, extraY);
}

上記はベクタを計算するメソッドで、以下のようなことを行っています。

  • X方向:
    左方向は、要素を含む軸境界ボックスの左端を設定する。
    右方向は、要素を含む軸境界ボックスの右端からキャンバスの幅を引く(単純に言えば、キャンバス内に残したいという意味ですが、厳密には違います)。
  • Y方向:
    上方向は、要素を含む軸境界ボックスの上端を設定する。
    下方向は、要素を含む軸境界ボックスの下端からキャンバスの高さを引く(単純に言えば、キャンバス内に残したいという意味ですが、厳密には違います)。

 

このコードに続いて、ManipulationInertialStartingイベントハンドラを以下のように書き換えます。

 private void canvas_ManipulationInertiaStarting(
                    object sender, ManipulationInertiaStartingEventArgs e)
{
    // イナーシアの設定を行います(慣性の移動量です)
    var element = (UIElement) e.OriginalSource;
    var shoot = CalculateOvershoot(element);
    // 要素が領域内の場合
    if (shoot.Length > 0.0)
    {
       // 移動量を計算します
       var velocity = -shoot;
       velocity.Normalize();
       e.TranslationBehavior.InitialVelocity = velocity;
       // 要求された移動量の設定
       e.TranslationBehavior.DesiredDisplacement = shoot.Length;
     }
     e.Handled = true;
}

上記のコードでは、移動量(ベクトル)が正の場合に、InitialVelocityを設定し、DesiredDisplacementを移動量に設定しています。次にManipulationDelataイベントハンドラ内のコードを以下のように記述します。

 private void canvas_ManipulationDelta(
             object sender, ManipulationDeltaEventArgs e)
{
    // 操作対象のUI要素を取得
    var element = e.OriginalSource as UIElement;
    // 移動させるために MatrixTransformを取得
    var transform = element.RenderTransform as MatrixTransform;
    // 取得したMatrixTransformよりMatrixを取得します
    // (移動させるためのMatrixTransformを作成するためです)
    var matrix = transform == null ?
                              Matrix.Identity : transform.Matrix;
    // 移動量を設定します
    // (ScaleAt:ズーム、RotateAt:回転、TranslateAt:移動)
    matrix.ScaleAt(e.DeltaManipulation.Scale.X,
                   e.DeltaManipulation.Scale.Y,
                   e.ManipulationOrigin.X,
                   e.ManipulationOrigin.Y);
    matrix.RotateAt(e.DeltaManipulation.Rotation,
                    e.ManipulationOrigin.X,
                    e.ManipulationOrigin.Y);
    matrix.Translate(e.DeltaManipulation.Translation.X,
                     e.DeltaManipulation.Translation.Y);
    // 移動します
    element.RenderTransform = new MatrixTransform(matrix);
    if (e.IsInertial)
    {
       // バウンダリーフィードバック
        var shoot = CalculateOvershoot(element);
       var delta = new ManipulationDelta(shoot,
                                      0,
                                      new Vector(0.2, 0.2),
                                      new Vector(0.2, 0.2));
       e.ReportBoundaryFeedback(delta);
    }
}

このコードで実行すると動きを理解することができますが、上端と左端に対しては跳ね返るような動作をします。右端と下端では、跳ね返らない動作をします。この理由は、CalculateOvershootメソッドにあります。CalculateOvershootメソッドで、右端と下端で計算を行っていたために、このような動作になります。これを単純に四隅の何処でも跳ね返るようにするには、CalculateOvershootメソッドを以下のように書き換えます。

 private Vector CalculateOvershoot(UIElement element)
{
    // 要素の軸平行境界ボックスを取得します(実際の描画コレクションの軸平行境界を使用します)
    var elementBounds = element.RenderTransform.TransformBounds(
                                    VisualTreeHelper.GetDrawing(element).Bounds);
    double extraX = 0.0, extraY = 0.0;
    extraX = elementBounds.Left;
    extraY = elementBounds.Top;
    return new Vector(extraX, extraY);
}

シンプルに軸並行境界の座標を持つベクター生成するようにしています。このベクターを使ってManiplulationDeltaのインスタンスを生成して、境界フィードバックを呼び出すのです。これで四隅とも跳ね返る動作を行うようになります。ManipulationDeltaのコンストラクタのパラメータは以下のように定義されています。

  • Translation:移動量をベクターで指定する。
  • Rotation:角度をDoubleで指定する。
  • Scale:スケールをベクターで指定する。
  • Expansion:ズームをベクターで指定する。

これでイナーシアの説明は終了となります。個々の設定する値は、どのように設定するかは使い方次第で色々な手法が考えられることでしょう。私自身は、ベクター計算とかは得意でないので、その道の方が良い設定方法を解説してくれることを望みます。基本的に言えるのは、WPF4ではマニピュレーションとイナーシアが統合された形でサポートされているということです。