Обработка манипуляций пальцами в WinRT

131

Главное преимущество событий Pointer - возможность отслеживания отдельных пальцев. У событий Manipulation тоже есть свое преимущество - невозможность отслеживания отдельных пальцев. События Manipulation объединяют несколько пальцев (часто под «несколькими» имеются в виду два пальца) в высокоуровневые жесты типа сведения/разведения пальцев и поворотов. Эти жесты соответствуют распространенным графическим преобразованиям: сдвигу, масштабированию (хотя и ограниченному равными размерами в горизонтальном и вертикальном направлениях) и поворотам.

Следует учитывать, что действия нескольких пальцев объединяются в одну серию событий Manipulation не для всего окна, а для каждого элемента, обрабатывающего эти события. Это означает, что вы можете использовать один или два пальца для манипуляций с одним элементом, а еще два пальца — для манипуляций с другим элементом.

Класс UIElement определяет пять событий Manipulation, которые элемент обычно получает в следующем порядке (будьте внимательны, имена первых двух событий очень похожи):

ManipulationStarting
ManipulationStarted
ManipulationDelta (несколько событий)
ManipulationInertiaStarting
ManipulationDelta (несколько событий)
ManipulationCompleted

Класс Control определяет для этих событий виртуальные методы с именами OnManipulationStarting и т.д.

Хотя мышь или перо тоже может генерировать события Manipulation, эти события возникают только при нажатии кнопки мыши или прикосновении пера к экрану. Событие ManipulationStarting происходит тогда, когда пользователь впервые прикасается к элементу пальцем, нажимает на элементе кнопку мыши или прикасается к нему пером.

Событие ManipulationStarted обычно происходит вскоре после ManipulationStarting (но как вы вскоре увидите, ключевым словом здесь является «обычно»). Далее следует серия событий ManipulationDelta, описывающих перемещение пальцев по экрану. Когда все пальцы выходят за границы элемента, инициируется событие ManipulationInertiaStarting. Элемент продолжает генерировать события ManipulationDelta, представляющие эффекты инерции, а событие ManipulationCompleted указывает на завершение серии.

Хотя событие ManipulationStarting происходит тогда, когда палец впервые прикасается к элементу (или происходит щелчок мышью или нажатие пером), за этим событием не всегда следует событие ManipulationStarted, а событие ManipulationStarted может быть немного отложено. Проблема в том, что система должна отличать касание или удержание от выполняемого жеста манипуляции. Событие ManipulationStarted инициируется при перемещении пальца (мыши, пера).

Например, если прикоснуться к элементу скользящим движением, за ManipulationStarting очень быстро последует событие ManipulationStarted и множественные события ManipulationDelta. Но если установить палец в одну точку и задержать его, событие ManipulationStarted может быть отложено на некоторое время.

Если пользователь выполняет обычное, правое или двойное касание, событие ManipulationStarted вообще не произойдет. Однако событие Holding может произойти после ManipulationStarting; в этом случае перемещение пальца приведет к генерированию ManipulationStarted и остальных событий. Далее происходит еще одно событие Holding со свойством HoldingState, равным Canceled.

Однако по умолчанию элемент вообще не генерирует никакие события Manipulation! Эти события должны быть сначала разрешены на уровне отдельных элементов. Чтобы программа могла явно указать, какие типы манипуляций ее интересуют, класс UIElement определяет свойство ManipulationMode перечислимого типа ManipulationModes (имя свойства в единственном числе, имя перечисления — во множественном). По умолчанию свойство ManipulationMode содержит значение ManipulationModes.System, которое для приложения эквивалентно ManipulationModes.None. Чтобы разрешить события Manipulation для элемента, необходимо задать свойству одно из остальных значений перечисления ManipulationModes. Элементы перечисления определяются как битовые флаги, поэтому их можно объединять поразрядным оператором ИЛИ (|).

Хотя некоторые приложения должны обрабатывать все пять событий Manipulation, можно написать код, который будет обрабатывать только событие ManipulationDelta. Именно так дело обстоит в программе ManipulationTracker. Программа отображает набор элементов управления CheckBox для членов перечисления ManipulationModes и три элемента Rectangle, с которыми можно выполнять манипуляции. Чтобы упростить код и разметку, для хранения и отображения членов ManipulationModes используется класс, производный от CheckBox:

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

namespace WinRTTestApp
{
    public class ManipulationModeCheckBox : CheckBox
    {
        public ManipulationModes ManipulationModes { set; get; }
    }
}

Десять экземпляров этой разновидности элемента CheckBox размещаются на панели StackPanel в файле MainPage.xaml; каждый элемент идентифицируется именем члена перечисления (с вставленными пробелами, чтобы имена лучше читались) и целочисленным значением:

<Page ...>

    <Page.Resources>
        <Style TargetType="local:ManipulationModeCheckBox">
            <Setter Property="Margin" Value="12 6 24 6" />
        </Style>

        <Style TargetType="Rectangle">
            <Setter Property="Width" Value="140" />
            <Setter Property="Height" Value="140" />
            <Setter Property="VerticalAlignment" Value="Top" />
            <Setter Property="HorizontalAlignment" Value="Left" />
            <Setter Property="RenderTransformOrigin" Value="0.5 0.5" />
        </Style>
    </Page.Resources>

    <Grid Background="#FF1D1D1D">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>

        <StackPanel Name="checkBoxPanel">
            <local:ManipulationModeCheckBox Checked="ManipulationModeCheckBox_Checked"
                                            Unchecked="ManipulationModeCheckBox_Checked"
                                            Content="Перемещение по X (1)" 
                                            ManipulationModes="TranslateX" />

            <local:ManipulationModeCheckBox Checked="ManipulationModeCheckBox_Checked"
                                            Unchecked="ManipulationModeCheckBox_Checked"
                                            Content="Перемещение по Y (2)" 
                                            ManipulationModes="TranslateY" />

            <local:ManipulationModeCheckBox Checked="ManipulationModeCheckBox_Checked"
                                            Unchecked="ManipulationModeCheckBox_Checked"
                                            Content="Перемещение по направляющей X (4)" 
                                            ManipulationModes="TranslateRailsX" />

            <local:ManipulationModeCheckBox Checked="ManipulationModeCheckBox_Checked"
                                            Unchecked="ManipulationModeCheckBox_Checked"
                                            Content="Перемещение по направляющей Y (8)" 
                                            ManipulationModes="TranslateRailsY" />

            <local:ManipulationModeCheckBox Checked="ManipulationModeCheckBox_Checked"
                                            Unchecked="ManipulationModeCheckBox_Checked"
                                            Content="Поворот (16)" 
                                            ManipulationModes="Rotate" />

            <local:ManipulationModeCheckBox Checked="ManipulationModeCheckBox_Checked"
                                            Unchecked="ManipulationModeCheckBox_Checked"
                                            Content="Масштабирование (32)" 
                                            ManipulationModes="Scale" />

            <local:ManipulationModeCheckBox Checked="ManipulationModeCheckBox_Checked"
                                            Unchecked="ManipulationModeCheckBox_Checked"
                                            Content="Инерция сдвига (64)" 
                                            ManipulationModes="TranslateInertia" />

            <local:ManipulationModeCheckBox Checked="ManipulationModeCheckBox_Checked"
                                            Unchecked="ManipulationModeCheckBox_Checked"
                                            Content="Инерция поворота (128)" 
                                            ManipulationModes="RotateInertia" />

            <local:ManipulationModeCheckBox Checked="ManipulationModeCheckBox_Checked"
                                            Unchecked="ManipulationModeCheckBox_Checked"
                                            Content="Инерция масштабирования (256)" 
                                            ManipulationModes="ScaleInertia" />

            <local:ManipulationModeCheckBox Checked="ManipulationModeCheckBox_Checked"
                                            Unchecked="ManipulationModeCheckBox_Checked"
                                            Content="All (0xFFFF)" 
                                            ManipulationModes="All" />
        </StackPanel>

        <Grid Name="rectanglePanel" Grid.Column="1">
            <Rectangle Fill="LimeGreen">
                <Rectangle.RenderTransform>
                    <CompositeTransform />
                </Rectangle.RenderTransform>
            </Rectangle>

            <Rectangle Fill="DarkOrange">
                <Rectangle.RenderTransform>
                    <CompositeTransform />
                </Rectangle.RenderTransform>
            </Rectangle>

            <Rectangle Fill="BlanchedAlmond">
                <Rectangle.RenderTransform>
                    <CompositeTransform />
                </Rectangle.RenderTransform>
            </Rectangle>
        </Grid>
    </Grid>
</Page>

В большей ячейке Grid размещаются три элемента Rectangle, окрашенных в цвета государственного флага Компьютерстана: красный, зеленый и синий. В файле фонового кода любая установка или сброс пользовательских разновидностей Checkbox приводит к вычислению нового значения ManipulationModes посредством объединения членов перечисления, связанных с установленными флажками, поразрядным оператором ИЛИ. Полученное составное значение ManipulationModes задается свойству ManipulationMode трех элементов Rectangle:

using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Shapes;

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();
        }

        private void ManipulationModeCheckBox_Checked(object sender, RoutedEventArgs e)
        {
            // Получение составного значения ManipulationModes
            // для установленных флажков
            ManipulationModes manipulationModes = ManipulationModes.None;

            foreach (UIElement child in checkBoxPanel.Children)
            {
                ManipulationModeCheckBox checkBox = child as ManipulationModeCheckBox;

                if ((bool)checkBox.IsChecked)
                    manipulationModes |= checkBox.ManipulationModes;
            }

            // Задать свойство ManipulationMode для каждого объекта Rectangle
            foreach (UIElement child in rectanglePanel.Children)
                child.ManipulationMode = manipulationModes;
        }

        protected override void OnManipulationDelta(ManipulationDeltaRoutedEventArgs e)
        {
            // В OriginalSource всегда хранится Rectangle, потому что
            // у всех остальных объектов свойство ManipulationMode не содержит
            // ничего другого, кроме ManipulationModes.None
            Rectangle rect = e.OriginalSource as Rectangle;
            CompositeTransform transform = (CompositeTransform)rect.RenderTransform;

            transform.TranslateX += e.Delta.Translation.X;
            transform.TranslateY += e.Delta.Translation.Y;

            transform.ScaleX *= e.Delta.Scale;
            transform.ScaleY *= e.Delta.Scale;

            transform.Rotation += e.Delta.Rotation;

            base.OnManipulationDelta(e);
        }
    }
}

Завершающая часть программы содержит переопределение OnManipulationDelta - виртуальный метод, определяемый классом Control, который упрощает доступ к событию ManipulationDelta, определяемому UIElement.ManipulationDelta — основное событие Manipulation — определяет, что сейчас делают пальцы пользователя.

Обратите внимание: переопределение OnManipulationDelta преобразует свойство OriginalSource аргументов события к типу Rectangle без предварительной проверки возможности такого преобразования. Теоретически в свойстве OriginalSource может содержаться MainPage или любой потомок MainPage. Однако в нашей программе события Manipulation разрешены только для элементов Rectangle, поэтому события ManipulationDelta могут генерироваться только элементами Rectangle.

Переопределение получает преобразование CompositeTransform, заданное свойству RenderTransform этого конкретного объекта Rectangle, и настраивает пять свойств преобразования на основании свойства Delta аргументов события. Свойство Delta относится к типу ManipulationDelta — структуры, обладающей четырьмя свойствами. (Будьте внимательны! Имя структуры совпадает с именем события, с которым она передается!) Значения обозначают изменения с момента последнего события ManipulationDelta.

Программный код обращается к трем из четырех свойств ManipulationDelta. Четвертое свойство Expansion похоже на Scale, но оно выражается в пикселах, а не в виде масштабного коэффициента. Свойство Translation структуры ManipulationDelta обозначает среднее расстояние перемещения пальцев с момента последнего события ManipulationDelta; эти значения просто прибавляются к свойствам TranslateX и TranslateY объекта CompositeTransform, а при отсутствии смещения они равны нулю.

Похожее (хотя и обрабатываемое несколько иначе) свойство Scale структуры ManipulationDelta обозначает увеличение расстояния между пальцами с момента последнего события. Свойства ScaleX и ScaleY класса CompositeTransform умножаются на этот коэффициент. (Так как события Manipulation не предоставляют отдельных масштабных коэффициентов для горизонтального и вертикального направления, все масштабирование при манипуляциях обязательно является изотропным, то есть независящим от направления.) Если масштабирование отсутствует (или не было разрешено), значение Scale равно 1. Свойство Rotate объекта ManipulationDelta определяет изменение угла поворота, обусловленного вращением пальцев относительно друг друга, и прибавляется к свойству Rotation объекта CompositeTransform.

Установите несколько флажков, и вы сможете перемещать прямоугольники мышью или пером или же использовать несколько пальцев для перемещения, масштабирования и вращения прямоугольников и даже манипулировать с двумя или тремя одновременно.

Вращение и масштабирование элементов пальцами в приложении Windows Runtime

Для программ, использующих события Manipulation, действуют очень простые правила: всегда задавайте значение, отличное от значения по умолчанию, свойству ManipulationMode элементов, для которых должны генерироваться события Manipulation. Для каждого такого элемента генерируется независимый поток событий Manipulation. Вы назначаете обработчик для события ManipulationDelta на уровне самого элемента или же обрабатываете это событие в его предке по визуальному дереву.

Обратите внимание: ни код, ни разметка XAML не содержит никаких ссылок на центры масштабирования или поворота, не считая того, что свойству RenderTransformOrigin задана относительная точка (0.5, 0.5). Соответственно все операции масштабирования и повороты выполняются относительно центра этого прямоугольника. Такое поведение неправильно. Предположим, вы поместили один палец возле угла прямоугольника и удерживаете его, а вторым пальцем захватили противоположный угол и растягиваете или вращаете его. Операции поворота и масштабирования должны выполняться относительно первого пальца. Другими словами, часть прямоугольника под первым пальцем должна оставаться на месте, тогда как прямоугольник должен вращаться или масштабироваться вокруг точки касания.

Для решения этой проблемы требуется относительно сложная логика, поэтому мы вернемся к ней позднее в одной из статей. А пока можно поэкспериментировать с другими типами манипуляций. Существуют три разных типа инерции — для сдвига, масштабирования и поворота. Способы управления степенью инерционности будут рассмотрены позднее.

В программном коде свойство ManipulationMode, эквивалентное приведенному на предыдущем экране, может быть задано следующим образом:

rect.ManipulationMode = ManipulationModes.Scale | ManipulationModes.Rotate |
                        ManipulationModes.TranslateX | 
                        ManipulationModes.TranslateY;

Но для разметки XAML такое решение не подходит. Возможности задания свойства ManipulationMode в XAML ограничиваются одним членом перечисления; скорее всего, в реальном приложении это будет значение All.

Чтобы ограничить манипуляции только горизонтальным перемещением, включите из перечисления ManipulationModes значение TranslateX, но не TranslateY:

rect.ManipulationMode = ManipulationModes.TranslateX;

И наоборот, для ограничения перемещений вертикальным направлением следует задать TranslateY, но не TranslateX.

В перечисление ManipulationModes также входят значения TranslateRailsX и TranslateRailsY. Они работают так, как положено, только при задании обоих значений TranslateX и TranslateY, например:

rect.ManipulationMode = ManipulationModes.TranslateX | 
                        ManipulationModes.TranslateY | 
                        ManipulationModes.TranslateRailsX;

Эта конфигурация позволяет свободно перемещать элемент в горизонтальном и вертикальном направлениях. Но если манипуляция начинается с перемещения по горизонтали, то элемент «входит в колею» (фигурально выражаясь), и все дальнейшие перемещения происходят только по горизонтали, пока пользователь не отнимет палец и не начнет все заново.

Следующая конфигурация ограничивает перемещение вертикальным направлением, если манипуляция началась с вертикального перемещения:

rect.ManipulationMode = ManipulationModes.TranslateX | 
                        ManipulationModes.TranslateY | 
                        ManipulationModes.TranslateRailsY;

Также можно задать оба направления:

rect.ManipulationMode = ManipulationModes.TranslateX | 
                        ManipulationModes.TranslateY | 
                        ManipulationModes.TranslateRailsX |
                        ManipulationModes.TranslateRailsY;

Начав перетаскивать элемент по диагонали, вы сможете перемещать его как угодно. Но если начать с горизонтального или вертикального перемещения, элемент будет перемещаться только в этом направлении.

Как было показано ранее в листинге, программа ManipulationTracker использует свойство Delta аргумента ManipulationDeltaRoutedEventArgs для внесения изменений в CompositeTransform:

transform.TranslateX += e.Delta.Translation.X;
transform.TranslateY += e.Delta.Translation.Y;

transform.ScaleX *= e.Delta.Scale;
transform.ScaleY *= e.Delta.Scale;

transform.Rotation += e.Delta.Rotation;

Если вы изучили свойства ManipulationDeltaRoutedEventArgs, то обнаружили, что наряду со свойством Delta имеется свойство Cumulative, также относящееся к типу ManipulationDelta. Свойство Delta обозначает изменение с момента последнего события ManipulationDelta, а свойство Cumulative обозначает изменения с момента ManipulationStarted. Казалось бы, со свойством Cumulative работать проще, чем с Delta, потому что значения можно преобразовать в соответствующие свойства CompositeTransform:

transform.TranslateX += e.Cumulative.Translation.X;
transform.TranslateY += e.Cumulative.Translation.Y;

transform.ScaleX = e.Cumulative.Scale;
transform.ScaleY = e.Cumulative.Scale;

transform.Rotation += e.Cumulative.Rotation;

В самом деле, при первой манипуляции с элементом все вроде бы работает. Но отнимите палец от экрана и попробуйте выполнить другую манипуляцию с тем же элементом. Элемент «прыгает» в свою исходную позицию в левом верхнем углу экрана!

Свойство Cumulative накапливает изменения не от начала программы, а только от конкретного события ManipulationStarted.

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