Инерционное движение в WinRT

83

Событие Manipulation поддерживает эффект инерции для сдвига, масштабирования и поворота. Если инерция вам не нужна, просто не указывайте соответствующие значения ManipulationModes. Если в произвольный момент времени вам потребуется остановить манипуляцию или инерцию, в аргументах события, прилагаемых к событиям ManipulationStarted и ManipulationDelta, имеется метод Complete, при вызове которого инициируется событие ManipulationCompleted.

Если вы хотите реализовать инерцию самостоятельно, это тоже возможно. В аргументах событий ManipulationDelta и ManipulationlnertiaStarting присутствует свойство Velocities для скоростей линейного перемещения, масштабирования и поворота. Для линейного перемещения свойство Velocities задается в пикселах в миллисекунду — пожалуй, не самая очевидная единица. Экспериментируя с перемещением экранных объектов быстрым движением пальца, я подошел вплотную к 10 пикселам в миллисекунду, но превысить этот порог мне не удалось. Это составляет 10 000 пикселов в секунду, что эквивалентно примерно 100 дюймам в секунду, или немногим менее 6 миль в час.

Для инерции предусмотрено замедление по умолчанию, но если вам понадобится реализовать его по-своему, придется обрабатывать событие ManipulationlnertiaStarting. Класс ManipulationInertiaStartingRoutedEventArgs определяет следующие три свойства:

Класс InertiaTranslationBehavior (например) позволяет задать линейное замедление двумя способами: в свойстве DesiredDisplacement в пикселах (насколько далеко должен отойти объект) или в свойстве DesiredDeceleration в пикселах в миллисекунду в квадрате. Оба свойства по умолчанию равны NaN (не число).

Значения DesiredDeceleration обычно очень малы, но, пожалуй, здесь будет уместно небольшое отступление из области физики. Из школьного курса физики мы знаем, что при применении к объекту в состоянии покоя постоянного ускорения расстояние, на которое объект переместится за время t, составляет:

Например, объект в состоянии свободного падения у поверхности Земли без сопротивления воздуха испытывает постоянное ускорение 9,81 м/с2. За первую секунду объект пролетит 4,9 метра, через две секунды — 19,6 метра и т.д. Скорость v вычисляется как первая производная расстояния по времени:

И снова для объекта в свободном падении скорость составит 9,81 м/с к концу первой секунды, 19,6 м/с к концу второй секунды и т.д. Каждую секунду скорость падающего объекта возрастает на 9,81 м/с.

Замедление представляет собой тот же самый процесс, только наоборот. Из второй формулы мы знаем, что:

Если объект перемещается со скоростью и, постоянное замедление a приведет к его остановке через t секунд. Если экранный объект перемещается со скоростью 5 пикселов в миллисекунду, по этой формуле можно рассчитать замедление, необходимое для прерывания его движения за фиксированное количество секунд (например, 5 секунд или 5000 миллисекунд):

Проект FlickAndBounce выполняет похожие вычисления; время замедления задается при помощи элемента управления Slider в диапазоне от 1 до 60 секунд. Файл XAML включает элемент управления Slider и элемент Ellipse с заданным значением ManipulationMode и тремя событиями Manipulation. И хотя ManipulationMode задано значение All (в XAML выбор не так уж велик), программа использует только сдвиг, а эллипс перемещается посредством задания вложенных свойств Canvas.Left и Canvas.Top вместо преобразования:

<Page ...>

    <Grid Background="#FF1D1D1D" Name="grid">
        <Canvas>
            <Ellipse Name="ell"
                     Fill="LimeGreen"
                     Width="140"
                     Height="140"
                     ManipulationMode="All"
                     ManipulationStarted="ell_ManipulationStarted"
                     ManipulationDelta="ell_ManipulationDelta"
                     ManipulationInertiaStarting="ell_ManipulationInertiaStarting" />
        </Canvas>

        <Slider x:Name="slider" VerticalAlignment="Bottom"
                Value="5" Minimum="1" Maximum="60"
                Margin="22 0" />
    </Grid>
</Page>

Конечно, если объект просто вылетит за границу экрана, все замедление будет потеряно. По этой причине обработчик ManipulationDelta обнаруживает, когда Ellipse пытается выйти за края экрана. Элемент Ellipse продолжает двигаться в обратном направлении так, словно он отразился от края.

Обратите внимание на использование свойства IsInertial в логике отражения. Это не помешает вам перетащить Ellipse за край экрана:

using System;
using Windows.Foundation;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        int xDirection;
        int yDirection;

        public MainPage()
        {
            this.InitializeComponent();
        }

        private void ell_ManipulationStarted(object sender, ManipulationStartedRoutedEventArgs e)
        {
            // Определить направления
            xDirection = 1;
            yDirection = 1;
        }

        private void ell_ManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e)
        {
            // Определение новой позиции эллипса незавиcимо от краев
            double x = Canvas.GetLeft(ell) + xDirection * e.Delta.Translation.X;
            double y = Canvas.GetTop(ell) + yDirection * e.Delta.Translation.Y;

            if (e.IsInertial)
            {
                // Отражение от краев
                Size playground = new Size(grid.ActualWidth - ell.Width,
                                           grid.ActualHeight - ell.Height);

                while (x > playground.Width || y > playground.Height || x < 0 || y < 0)
                {
                    if (x < 0)
                    {
                        xDirection *= -1;
                        x = -x;
                    }
                    if (x > playground.Width)
                    {
                        x = 2 * playground.Width - x;
                        xDirection *= -1;
                    }
                    if (y < 0)
                    {
                        yDirection *= -1;
                        y = -y;
                    }
                    if (y > playground.Height)
                    {
                        y = 2 * playground.Height - y;
                        yDirection *= -1;
                    }
                }
            }

            Canvas.SetLeft(ell, x);
            Canvas.SetTop(ell, y);
        }

        private void ell_ManipulationInertiaStarting(object sender,
                                                  ManipulationInertiaStartingRoutedEventArgs e)
        {
            double maxVelocity = Math.Max(Math.Abs(e.Velocities.Linear.X),
                                          Math.Abs(e.Velocities.Linear.Y));

            e.TranslationBehavior.DesiredDeceleration = maxVelocity / (1000 * slider.Value);
        }
    }
}

Ближе к концу обработчика ManipulationlnertiaStarting максимум абсолютных значений горизонтальной и вертикальной скорости используется для вычисления замедления на основании текущего значения Slider в секундах.

Пройди тесты
Лучший чат для C# программистов