しっぽを追いかけて

ぐるぐるしながら考えています

Unity と猫の話題が中心   掲載内容は個人の私見であり、所属組織の見解ではありません

ダブルクリック/タップで拡大する ScrollViewer をつくる

f:id:matatabi_ux:20140319200916j:plain

ScrollViewr の内部に画像を表示すれば、ピンチアウトのジェスチャによって

f:id:matatabi_ux:20140319200925j:plain

上記のような感じで拡大表示が行えます

これでも問題ないのですが、ストアアプリ以外ではダブルクリックやダブルタップで拡大を行う UI があるためにこちらの操作に慣れているユーザーも少なくないようです そこで、ScrollViewr の親要素にアタッチすることで、ダブルクリックやダブルタップで拡大表示する機能を付加するビヘイビアを作ってみました!

/// <summary>
/// ダブルタップ/クリックで表示領域を拡大できるビヘイビア
/// </summary>
public class DoubleTappedZoomableBehavior : DependencyObject, IBehavior
{
    #region IncrementalSize 依存関係プロパティ
    /// <summary>
    /// 拡大率増分 依存関係プロパティ
    /// </summary>
    private static readonly DependencyProperty IncrementalSizeProperty
        = DependencyProperty.Register(
        "IncrementalSize",
        typeof(float),
        typeof(DoubleTappedZoomableBehavior),
        new PropertyMetadata(
            1.5f,
            (s, e) =>
            {
                var control = s as DoubleTappedZoomableBehavior;
                if (control != null)
                {
                    control.OnIncrementalSizeChanged();
                }
            }));

    /// <summary>
    /// 拡大率増分 変更イベントハンドラ
    /// </summary>
    private void OnIncrementalSizeChanged()
    {
    }

    /// <summary>
    /// 拡大率増分
    /// </summary>
    public float IncrementalSize
    {
        get { return (float)this.GetValue(IncrementalSizeProperty); }
        set { this.SetValue(IncrementalSizeProperty, value); }
    }
    #endregion //IncrementalSize 依存関係プロパティ

    /// <summary>
    /// アタッチ対象のオブジェクト
    /// </summary>
    public DependencyObject AssociatedObject { get; set; }

    /// <summary>
    /// ScrollViewer
    /// </summary>
    public ScrollViewer ScrollViewer { get; set; }

    /// <summary>
    /// アタッチする
    /// </summary>
    /// <param name="associatedObject">アタッチ対象オブジェクト</param>
    public void Attach(DependencyObject associatedObject)
    {
        this.AssociatedObject = associatedObject;
        if (associatedObject is FrameworkElement)
        {
            ((FrameworkElement)associatedObject).SizeChanged += this.OnSizeChanged;
        }
    }

    /// <summary>
    /// 表示完了イベントハンドラ
    /// </summary>
    /// <param name="sender">イベント発行者</param>
    /// <param name="e">イベント引数</param>
    private void OnSizeChanged(object sender, SizeChangedEventArgs e)
    {
        this.ScrollViewer = FindChild<ScrollViewer>(this.AssociatedObject);
        if (this.ScrollViewer != null)
        {
            this.ScrollViewer.DoubleTapped += this.OnDoubleTapped;
        }
    }

    /// <summary>
    /// ダブルタップイベントハンドラ
    /// </summary>
    /// <param name="sender">イベント発行者</param>
    /// <param name="e">イベント引数</param>
    private async void OnDoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
    {
        if (this.ScrollViewer == null)
        {
            return;
        }

        // タップによるスクロール位置ずれを回避
        if (e.PointerDeviceType != PointerDeviceType.Mouse)
        {
            await Task.Delay(100);
        }

        var point = e.GetPosition(this.ScrollViewer);
        var zoomFactor = this.ScrollViewer.ZoomFactor * this.IncrementalSize;
        if (zoomFactor > 10)
        {
            zoomFactor = 1f;
        }
        var degree = zoomFactor / this.ScrollViewer.ZoomFactor;

        this.ScrollViewer.ChangeView(
            (degree * (this.ScrollViewer.HorizontalOffset + point.X)) - (this.ScrollViewer.ViewportWidth / 2),
            (degree * (this.ScrollViewer.VerticalOffset + point.Y)) - (this.ScrollViewer.ViewportHeight / 2),
            zoomFactor);

        e.Handled = true;
    }

    /// <summary>
    /// デタッチする
    /// </summary>
    public void Detach()
    {
        if (this.AssociatedObject is FrameworkElement)
        {
            ((FrameworkElement)this.AssociatedObject).SizeChanged -= this.OnSizeChanged;
        }
        if (this.ScrollViewer != null)
        {
            this.ScrollViewer.DoubleTapped += this.OnDoubleTapped;
        }
        this.AssociatedObject = null;
        this.ScrollViewer = null;
    }

    /// <summary>
    /// 指定した型の最初に見つかったビジュアル要素を返す
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="root">探索対象のビジュアル要素</param>
    /// <param name="name">対象のビジュアル要素名</param>
    /// <returns>見つかった場合はその要素</returns>
    private static T FindChild<T>(DependencyObject root, string name = null) where T : FrameworkElement
    {
        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(root); i++)
        {
            var child = VisualTreeHelper.GetChild(root, i);

            if (child != null && child is T)
            {
                if (child is FrameworkElement && (string.IsNullOrEmpty(name) || ((FrameworkElement)child).Name == name))
                {
                    return (T)child;
                }
                else
                {
                    T childOfChild = FindChild<T>(child, name);

                    if (childOfChild != null)
                    {
                        return childOfChild;
                    }
                }
            }
            else
            {
                T childOfChild = FindChild<T>(child, name);

                if (childOfChild != null)
                {
                    return childOfChild;
                }
            }
        }
        return null;
    }
}

ダブルタップの場合、タップ時にスクロール操作が走るためかすぐに拡大遷移させると抑止されるようなので、100 ms だけウェイトを入れています

また、10倍以上拡大した場合は等倍に戻すようにもしています