Однопальцевое вращение элементов в WinRT

115

Хотя событие ManipulationStarting не обязательно сообщает о фактическом начале манипуляции, оно предоставляет и другие возможности инициализации; все они основаны на использовании свойств ManipulationStartingRoutedEventArgs:

Свойство Mode

Относится к уже упоминавшемуся перечислимому типу ManipulationModes. В данном случае оно задает типы манипуляций, которые должны обрабатываться приложением. Но помните, что событие ManipulationStarting будет получено только в том случае, если свойству ManipulationMode элемента задано значение, отличное от ManipulationModes.None или ManipulationModes.System.

Свойство Container

Доступно только для чтения во всех остальных событиях Manipulation, но доступно для записи в событии ManipulationStarting. По умолчанию свойство Container содержит то же значение, что и свойство OriginalSource, но в последующих событиях в нем содержится элемент, относительно которого задается свойство Position. Если вы хотите, чтобы свойство Position задавалось относительно элемента, отличного от OriginalSource, задайте этот элемент в свойстве Container.

Свойство Pivot

Дает возможность выполнять вращение одним пальцем. Сейчас я покажу, как это делается.

Представьте фотографию, лежащую на столе (речь идет о реальной фотографии на реальном столе). Вы прикасаетесь пальцем к уголку и тянете его на себя. Сохранит ли фотография прежнюю ориентацию? Не обязательно. Если прикосновение будет легким, сила трения между поверхностью стола и фотографией заставит фотографию слегка повернуться.

Аналогичный эффект достигается однопальцевым поворотом, но вы должны использовать только что представленный метод поворота объектов относительно центра. Собственно, файл XAML этого проекта выглядит почти так же, как файл проекта CenteredTransforms:

<Page ...>

    <Grid Background="#FF1D1D1D">
        <Image Name="image"
               RenderTransformOrigin="0 0"
               Source="http://professorweb.ru/my/windows8/rt/level1/files/win8logo.png"
               Stretch="None"
               HorizontalAlignment="Left"
               VerticalAlignment="Top">
            <Image.RenderTransform>
                <TransformGroup x:Name="xTransformGroup">
                    <MatrixTransform x:Name="matrixXform" />
                    <CompositeTransform x:Name="compositeXform" />
                </TransformGroup>
            </Image.RenderTransform>
        </Image>
    </Grid>
</Page>

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

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

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

            image.ManipulationMode = ManipulationModes.All &
                                     ~ManipulationModes.TranslateRailsX &
                                     ~ManipulationModes.TranslateRailsY;
        }

        protected override void OnManipulationStarting(ManipulationStartingRoutedEventArgs e)
        {
            e.Pivot = new ManipulationPivot(
                new Point(image.ActualWidth / 2, image.ActualHeight / 2), 50);
            base.OnManipulationStarting(e);
        }

        protected override void OnManipulationDelta(ManipulationDeltaRoutedEventArgs e)
        {
            // Преобразование назначается текущим полным преобразованием
            matrixXform.Matrix = xTransformGroup.Value;

            // Использование для преобразования свойства Position
            Point center = matrixXform.TransformPoint(e.Position);

            // Цент нового инкрементального преобразования
            compositeXform.CenterX = center.X;
            compositeXform.CenterY = center.Y;

            // Задание других свойств
            compositeXform.TranslateX = e.Delta.Translation.X;
            compositeXform.TranslateY = e.Delta.Translation.Y;
            compositeXform.ScaleX = e.Delta.Scale;
            compositeXform.ScaleY = e.Delta.Scale;
            compositeXform.Rotation = e.Delta.Rotation;

            base.OnManipulationDelta(e);
        }
    }
}

Ключевым моментом здесь является задание свойству Pivot объекта ManipulationStartingRoutedEventArgs значения типа ManipulationPivot, оно определяет два параметра:

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

Помните программу SliderSketch из статьи «Настройки элемента Slider в WinRT» Конечно, вместо элементов управления Slider в ней было бы удобнее использовать более привычные круглые рукоятки. Программа DialSketch, завершающая эту статью, использует элемент управления Dial, реализующий однопальцевые повороты.

Чтобы немного упростить определение класса Dial, я решил объявить его производным от RangeBase вместо Slider. При этом элемент управления получает готовые свойства Minimum, Maximum и Value типа double, а также событие ValueChanged. В нашем элементе управления все свойства double определяют углы поворота, а из всех режимов манипуляции разрешен только поворот:

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

namespace WinRTTestApp
{
    public class Dial : RangeBase
    {
        public Dial()
        {
            ManipulationMode = ManipulationModes.Rotate;
        }

        protected override void OnManipulationDelta(ManipulationDeltaRoutedEventArgs e)
        {
            Value = Math.Max(Minimum,
                    Math.Min(Maximum, Value + e.Delta.Rotation));

            base.OnManipulationDelta(e);
        }

        protected override void OnManipulationStarting(ManipulationStartingRoutedEventArgs e)
        {
            e.Pivot = new ManipulationPivot(
                new Point(ActualWidth / 2, ActualHeight / 2), 48);
            base.OnManipulationStarting(e);
        }
    }
}

Вот и все! Конечно, у элемента управления еще нет шаблона, и он не обращается ни к каким преобразованиям. Он только задает новое значение свойства Value (в результате чего RangeBase выдает событие ValueChanged), а все остальное должно быть реализовано где-то в другом месте.

Два экземпляра элемента управления Dial создаются в файле XAML проекта DialSketch. Секция Resources посвящена определению Style для этих двух элементов, включая ControlTemplate. Элементу управления Dial необходимы визуальные эффекты, которые сообщат пользователю о выполнении поворота; в шаблоне используется пунктирная линия с очень короткими штрихами для имитации делений.

Обратите внимание на значения Minimum и Maximum, задаваемые для Dial. Они подразумевают, что элемент Dial может сделать 10 полных поворотов между минимальной и максимальной позицией. Чтобы провести линию от одной стороны панели DialSketch до противоположной стороны, необходимо повернуть рукоятку 10 раз:

<Page ...>

    <Page.Resources>
        <Style TargetType="local:Dial">
            <Setter Property="Maximum" Value="1800" />
            <Setter Property="Minimum" Value="-1800" />
            <Setter Property="RenderTransformOrigin" Value="0.5 0.5" />
            <Setter Property="Width" Value="140" />
            <Setter Property="Height" Value="140" />
            <Setter Property="Margin" Value="22" />
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate>
                        <Grid>
                            <Ellipse Fill="DarkOrange" />
                            <Ellipse Fill="Black" Width="6" Height="6" />
                            <Ellipse Stroke="Black" StrokeThickness="12"
                                     StrokeDashArray="0.1 1" Margin="3" />
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Page.Resources>

    <Grid Background="#FF1D1D1D">
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition />
            <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>

        <Border Grid.ColumnSpan='3'
                BorderBrush="#DEFFFFFF"
                BorderThickness="3 0 0 3"
                Background="#C0C0C0"
                Padding="22">

            <Grid Name="drawingGrid">
                <Polyline Name="polyline"
                          Stroke="#414141"
                          StrokeThickness="3" />
            </Grid>
        </Border>

        <local:Dial x:Name="horzDial"
                    Grid.Row="1"
                    Maximum="1800"
                    ValueChanged="dial_ValueChanged">
            <local:Dial.RenderTransform>
                <RotateTransform />
            </local:Dial.RenderTransform>
        </local:Dial>

        <Button Content="Очистить"
                HorizontalAlignment="Center"
                VerticalAlignment="Center"
                Grid.Row="1"
                Grid.Column="1"
                Click="ClearButton_Click" />

        <local:Dial x:Name="vertDial"
                    Grid.Row="1"
                    Grid.Column="2"
                    Maximum="1800"
                    ValueChanged="dial_ValueChanged">
            <local:Dial.RenderTransform>
                <RotateTransform />
            </local:Dial.RenderTransform>
        </local:Dial>
    </Grid>
</Page>

Стоит заметить, что настройки Maximum повторяются для конкретных элементов управления Dial. В версии Windows 8, которую я использую, значение из стиля почему-то игнорируется. Также обратите внимание на то, что с каждым элементом управления Dial связывается преобразование RotateTransform.

Файл фонового кода инициализирует объект Polyline точкой в центре. Для каждого события ValueChanged от Dial элементу управления назначается преобразование RotateTranform, а в Polyline добавляется новый объект Point:

using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Media;

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

            Loaded += (sender, args) =>
            {
                polyline.Points.Add(new Point(drawingGrid.ActualWidth / 2,
                                              drawingGrid.ActualHeight / 2));
            };
        }

        private void dial_ValueChanged(object sender, RangeBaseValueChangedEventArgs e)
        {
            Dial dial = sender as Dial;
            RotateTransform rotateTransform = dial.RenderTransform as RotateTransform;

            rotateTransform.Angle = e.NewValue;

            double xFraction = (horzDial.Value - horzDial.Minimum) /
                                    (horzDial.Maximum - horzDial.Minimum);

            double yFraction = (vertDial.Value - vertDial.Minimum) /
                                    (vertDial.Maximum - vertDial.Minimum);

            double y = yFraction * drawingGrid.ActualHeight;
            double x = xFraction * drawingGrid.ActualWidth;
            polyline.Points.Add(new Point(x, y));
        }

        private void ClearButton_Click(object sender, RoutedEventArgs e)
        {
            polyline.Points.Clear();
        }
    }
}

Конечно, пользоваться программой все равно невозможно, но на следующем рисунке она пытается поприветствовать вас — так, как умеет:

Использование сенсорных элементов управления Slider
Пройди тесты
Лучший чат для C# программистов