2014年9月11日木曜日

[WPF][Silverlight][C#] ScrollViewer でマウスでの横スクロールと慣性スクロール

将来的に横スクロールにしたいという話があって、事前調査してみました。
やりたいことは下記3つ。
  1. マウスホイールで横スクロール
  2. ドラッグで横スクロール
  3. 慣性スクロール
あんまり情報ないですねー。とりあえずざくっとと作ってみたので、コードだけ紹介します。WPF版では ScrollViewer のカスタムコントロールで、Silverlight では ScrollViewer  を継承できないので Behavior で作りました。ほぼ同じようなコードになってます。加速度までは考慮していないのであしからず。

WPF版(Custom Control)
    /// <summary>
    /// 慣性スクロールビューア
    /// </summary>
    public class MomentumScrollViewer : ScrollViewer
    {
        #region インスタンスフィールド
        /// <summary>
        /// マウスオフセット位置
        /// </summary>
        private double mouseOffset;
        /// <summary>
        /// 開始位置
        /// </summary>
        private double startOffset;
        /// <summary>
        /// プレス有無
        /// </summary>
        private bool isPressed = false;
        /// <summary>
        /// 最終位置
        /// </summary>
        private double lastPosition;
        /// <summary>
        /// 移動量
        /// </summary>
        private double momentumValue;
        #endregion
        #region 依存プロパティ
        /// <summary>
        /// 運動量を取得または設定します。
        /// </summary>
        public double MomentumValue
        {
            get { return (double)GetValue(MomentumValueProperty); }
            set { SetValue(MomentumValueProperty, value); }
        }
        /// <summary>
        /// 運動量依存関係プロパティを識別します。
        /// </summary>
        public static readonly DependencyProperty MomentumValueProperty =
            DependencyProperty.Register("MomentumValue", typeof(double), typeof(MomentumScrollViewer), new PropertyMetadata(0.5));
        /// <summary>
        /// 水平位置を取得または設定します。
        /// </summary>
        public double HorizontalPosition
        {
            get { return (double)GetValue(HorizontalPositionProperty); }
            set { SetValue(HorizontalPositionProperty, value); }
        }
        /// <summary>
        /// 水平位置依存関係プロパティを識別します。
        /// </summary>
        public static readonly DependencyProperty HorizontalPositionProperty =
            DependencyProperty.Register("HorizontalPosition", typeof(double), typeof(MomentumScrollViewer), new FrameworkPropertyMetadata(0.0, new PropertyChangedCallback(OnHorizontalPositionChanged)));
        #endregion
        #region コンストラクタ
        /// <summary>
        /// コンストラクタ
        /// </summary>
        public MomentumScrollViewer()
        {
            // 各イベント登録
            this.PreviewMouseWheel += MomentumScrollViewer_PreviewMouseWheel;
            this.Loaded += (sender, e) =>
                {
                    var content = this.GetValue(ScrollViewer.ContentProperty) as FrameworkElement;
                    if (content != null)
                    {
                        content.MouseLeftButtonDown += content_MouseLeftButtonDown;
                        content.MouseLeftButtonUp += content_MouseLeftButtonUp;
                        content.MouseMove += content_MouseMove;
                        content.MouseLeave += content_MouseLeave;
                    }
                };
        }
        #endregion
        #region イベント
        /// <summary>
        /// 水平スクロール位置変更イベント
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="e"></param>
        private static void OnHorizontalPositionChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            var scrollViewer = obj as MomentumScrollViewer;
            if (scrollViewer != null)
            {
                scrollViewer.ScrollToHorizontalOffset((double)e.NewValue);
            }
        }
        #region ホイールでのスクロール
        /// <summary>
        /// マウスホイールイベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void MomentumScrollViewer_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
        {
            beginScroll(e.Delta);
            e.Handled = true;
        }
        #endregion
        #region ドラッグによるスクロール
        /// <summary>
        /// 左ボタンダウンイベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void content_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            var position = e.GetPosition(this);
            this.mouseOffset = position.X;
            this.startOffset = this.HorizontalOffset;
            this.isPressed = true;
        }
        /// <summary>
        /// マウス移動イベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void content_MouseMove(object sender, MouseEventArgs e)
        {
            if (this.isPressed)
            {
                var position = e.GetPosition(this);
                // 前回値からの運動量を保持し、前回値を更新
                momentumValue = lastPosition - position.X;
                lastPosition = position.X;
                // 変化量を算出し、スクロール
                var delta = (position.X > mouseOffset) ? -(position.X - mouseOffset) : mouseOffset - position.X;
                this.ScrollToHorizontalOffset(startOffset + delta);
            }
        }
        /// <summary>
        /// マウスリーブイベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void content_MouseLeave(object sender, MouseEventArgs e)
        {
            dragEnd();
        }
        /// <summary>
        /// 左ボタンアップイベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void content_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            dragEnd();
        }
        #endregion
        #endregion
        #region プライベートメソッド
        /// <summary>
        /// ドラッグ終了処理
        /// </summary>
        private void dragEnd()
        {
            if (this.isPressed)
            {
                this.isPressed = false;
                // 運動量がある場合は慣性スクロール
                if (momentumValue != 0)
                {
                    beginScroll(momentumValue * (-1));
                }
            }
        }

        /// <summary>
        /// 慣性スクロール開始
        /// </summary>
        /// <param name="delta">変化量</param>
        private void beginScroll(double delta)
        {
            // 目的地設定
            var to = HorizontalOffset - delta * MomentumValue;
            if (to < 0)
                to = 0;
            if (to > ExtentWidth)
                to = ExtentWidth;
            // 慣性スクロールアニメーション開始
            var animation = new DoubleAnimation(to, new Duration(TimeSpan.FromMilliseconds(1000)));
            animation.EasingFunction = new CircleEase() { EasingMode = EasingMode.EaseOut };
            this.BeginAnimation(MomentumScrollViewer.HorizontalPositionProperty, animation);
        }
        #endregion
    }


Sivlerlight版(Behavior)
    /// <summary>
    /// 慣性スクロールビヘイビア
    /// </summary>
    public class MomentumScrollingBehavior : Behavior<ScrollViewer>
    {
        #region インスタンスフィールド
        /// <summary>
        /// マウスオフセット位置
        /// </summary>
        private double mouseOffset;
        /// <summary>
        /// 開始位置
        /// </summary>
        private double startOffset;
        /// <summary>
        /// プレス有無
        /// </summary>
        private bool isPressed = false;
        /// <summary>
        /// 最終位置
        /// </summary>
        private double lastPosition;
        /// <summary>
        /// 移動量
        /// </summary>
        private double momentumValue;
        #endregion
        #region 依存プロパティ
        /// <summary>
        /// 運動量を取得または設定します。
        /// </summary>
        public double MomentumValue
        {
            get { return (double)GetValue(MomentumValueProperty); }
            set { SetValue(MomentumValueProperty, value); }
        }
        /// <summary>
        /// 運動量依存関係プロパティを識別します。
        /// </summary>
        public static readonly DependencyProperty MomentumValueProperty =
            DependencyProperty.Register("MomentumValue", typeof(double), typeof(MomentumScrollingBehavior), new PropertyMetadata(0.5));
        /// <summary>
        /// 水平位置を取得または設定します。
        /// </summary>
        public double HorizontalPosition
        {
            get { return (double)GetValue(HorizontalPositionProperty); }
            set { SetValue(HorizontalPositionProperty, value); }
        }
        /// <summary>
        /// 水平位置依存関係プロパティを識別します。
        /// </summary>
        public static readonly DependencyProperty HorizontalPositionProperty =
            DependencyProperty.Register("HorizontalPosition", typeof(double), typeof(MomentumScrollingBehavior), new PropertyMetadata(0.0, new PropertyChangedCallback(OnHorizontalPositionChanged)));
        #endregion

        #region イベント
        /// <summary>
        /// アタッチ完了
        /// </summary>
        protected override void OnAttached()
        {
            base.OnAttached();
            this.AssociatedObject.MouseWheel += scrollViewer_MouseWheel;
            this.AssociatedObject.Loaded += (sender, e) =>
            {
                var content = this.AssociatedObject.GetValue(ScrollViewer.ContentProperty) as FrameworkElement;
                if (content != null)
                {
                    content.MouseLeftButtonDown += content_MouseLeftButtonDown;
                    content.MouseLeftButtonUp += content_MouseLeftButtonUp;
                    content.MouseMove += content_MouseMove;
                    content.MouseLeave += content_MouseLeave;
                }
            };
        }
        /// <summary>
        /// デタッチ
        /// </summary>
        protected override void OnDetaching()
        {
            this.AssociatedObject.MouseWheel -= scrollViewer_MouseWheel;
            var content = this.AssociatedObject.GetValue(ScrollViewer.ContentProperty) as FrameworkElement;
            if (content != null)
            {
                content.MouseLeftButtonDown -= content_MouseLeftButtonDown;
                content.MouseLeftButtonUp -= content_MouseLeftButtonUp;
                content.MouseMove -= content_MouseMove;
                content.MouseLeave -= content_MouseLeave;
            }
            base.OnDetaching();
        }
        /// <summary>
        /// 水平スクロール位置変更イベント
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="e"></param>
        private static void OnHorizontalPositionChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            var behavior = obj as MomentumScrollingBehavior;
            if (behavior != null)
            {
                behavior.AssociatedObject.ScrollToHorizontalOffset((double)e.NewValue);
            }
        }
        /// <summary>
        /// マウスホイールイベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void scrollViewer_MouseWheel(object sender, MouseWheelEventArgs e)
        {
            beginScroll(e.Delta);
            e.Handled = true;
        }
        #region ドラッグによるスクロール
        /// <summary>
        /// 左ボタンダウンイベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void content_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            var position = e.GetPosition(this.AssociatedObject);
            this.mouseOffset = position.X;
            this.startOffset = this.AssociatedObject.HorizontalOffset;
            this.isPressed = true;
        }
        /// <summary>
        /// マウス移動イベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void content_MouseMove(object sender, MouseEventArgs e)
        {
            if (this.isPressed)
            {
                var position = e.GetPosition(this.AssociatedObject);
                // 前回値からの運動量を保持し、前回値を更新
                momentumValue = lastPosition - position.X;
                lastPosition = position.X;
                // 変化量を算出し、スクロール
                var delta = (position.X > mouseOffset) ? -(position.X - mouseOffset) : mouseOffset - position.X;
                this.AssociatedObject.ScrollToHorizontalOffset(startOffset + delta);
            }
        }
        /// <summary>
        /// マウスリーブイベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void content_MouseLeave(object sender, MouseEventArgs e)
        {
            dragEnd();
        }
        /// <summary>
        /// 左ボタンアップイベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void content_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            dragEnd();
        }
        #endregion
        #endregion
        #region プライベートメソッド
        /// <summary>
        /// ドラッグ終了処理
        /// </summary>
        private void dragEnd()
        {
            if (this.isPressed)
            {
                this.isPressed = false;
                // 運動量がある場合は慣性スクロール
                if (momentumValue != 0)
                {
                    beginScroll(momentumValue * (-1));
                }
            }
        }

        /// <summary>
        /// 慣性スクロール開始
        /// </summary>
        /// <param name="delta">変化量</param>
        private void beginScroll(double delta)
        {
            // 目的地設定
            var to = this.AssociatedObject.HorizontalOffset - delta * MomentumValue;
            if (to < 0)
                to = 0;
            if (to > this.AssociatedObject.ExtentWidth)
                to = this.AssociatedObject.ExtentWidth;
            // 慣性スクロールアニメーション開始
            var storyboard = new Storyboard();
            storyboard.Children.Add(new DoubleAnimation()
            {
                From = this.AssociatedObject.HorizontalOffset,
                To = to,
                Duration = new Duration(TimeSpan.FromMilliseconds(1000)),
                EasingFunction = new CircleEase() { EasingMode = EasingMode.EaseOut }
            });
            Storyboard.SetTarget(storyboard, this);
            Storyboard.SetTargetProperty(storyboard, new PropertyPath("HorizontalPosition"));
            storyboard.Begin();
        }
        #endregion
    }










0 件のコメント:

コメントを投稿