Поворот элементов в WinRT

95

Обычно в учебниках изложение темы преобразований начинается с математически простых случаев: TranslateTransform для перемещения объектов и ScaleTransform для их увеличения или уменьшения. Но вряд ли эти примеры произведут на вас особое впечатление - ведь вы уже видели анимации, которые перемещают объекты по экрану или изменяют их размеры. Именно поэтому я начну с того, что нельзя сделать другим способом.

Ранее я показал, как задать свойство Angle объекта RotateTransform прямо в XAML, но гораздо интереснее динамически изменять свойство Angle в привязке данных или анимации, а результат более наглядно продемонстрирует, что же при этом происходит. В следующем файле XAML свойство Angle объекта RotateTransform привязано к свойству Value элемента управления Slider с диапазоном от 0 до 360:

<Grid Background="#FF1D1D1D">
        <Border HorizontalAlignment="Center"
                VerticalAlignment="Center"
                BorderBrush="#DEFFFFFF"
                BorderThickness="1">
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition />
                    <RowDefinition />
                </Grid.RowDefinitions>

                <Slider Name="slider"
                        Minimum="0"
                        Maximum="360" />

                <TextBlock Name="txb"
                           Text="Повернуть текст с использованием ползунка"
                           Grid.Row="1"
                           FontSize="40">
                    <TextBlock.RenderTransform>
                        <RotateTransform Angle="{Binding ElementName=slider, Path=Value}" />
                    </TextBlock.RenderTransform>
                </TextBlock>
            </Grid>
        </Border>
</Grid>

Элементы управления Slider и TextBlock занимают две строки панели Grid, расположенной внутри Border. Вот как это выглядит при первом появлении текста:

Стартовое состояние приложения с поворотом надписи

Ширина TextBlock определяет ширину Grid, которая, в свою очередь, определяет ширину Slider и ширину Border. При изменении текущего значения Slider пальцами или мышью объект TextBlock поворачивается против часовой стрелки. Вот как выглядит поворот:

Поворот текстовой надписи

Из иллюстрации видно, что размеры Grid и Border продолжают базироваться на размерах TextBlock до поворота, а повернутый объект TextBlock вышел за пределы границ своих предков в визуальном дереве.

Свойство UIElement, которому задается преобразование RotateTransform, называется RenderTransform и на его имя стоит обратить особое внимание. Слово «render» означает, что преобразование влияет только на способ визуализации элемента, а не на его местонахождение в макете. У этого обстоятельства есть как свои положительные, так и отрицательные стороны.

С одной стороны, преобразование выполняется на достаточно глубоком уровне системы формирования изображения. Поворот TextBlock не требует обновления макета с участием всего визуального дерева. Система формирования макета ничего не знает о том, что объект TextBlock был повернут, анимации преобразований могут выполняться во вторичном потоке с очень хорошей производительностью.

Действительно плохая новость - система формирования макета ничего не знает о том, что объект TextBlock был повернут. Предположим, вы хотите вывести в поле TextBlock вертикальный текст, развернув его на 90° (скажем, как подпись к оси графика). Было бы очень удобно, если бы система формирования макета могла вычислить размеры повернутого объекта TextBlock, чтобы вы могли поместить его в ячейку Grid и расположить точно в нужном месте. Однако в Windows Runtime не существует простых, универсальных средств для решения этой задачи.

Напротив, версия UIElement из WPF (Windows Presentation Foundation) определяет как свойство RenderTransform (которое работает по аналогии с Windows Runtime), так и свойство LayoutTransform, которое задает преобразование, распознаваемое системой формирования макета. Свойство LayoutTransform было утрачено при переходе от WPF к Silverlight и Windows Runtime, а для его имитации придется потрудиться.

Вернемся к работающей программе с поворотом текста. Измените состояние Slider так, чтобы объект TextBlock оказался поверх Slider:

Поворот надписи до наложения на элемент Slider

Теперь отведите пальцы от экрана (или отпустите кнопку мыши) и попробуйте прикоснуться или щелкнуть на элементе управления Slider в том месте, где он перекрывается с TextBlock. Slider не реагирует, потому что TextBlock блокирует ввод с мыши или сенсорного экрана. Итак, хотя система формирования макета не знает о перемещении TextBlock, логика проверки нажатия прекрасно знает, где находится повернутый объект. (С другой стороны, непосредственно в процессе манипуляций с Slider поле TextBlock не мешает работе, потому что Slider захватывает ввод.)

Также следует заметить, что поворот TextBlock выполняется относительно левого верхнего угла, который на концептуальном уровне является началом координат TextBlock: точкой (0, 0). Во многих графических системах преобразования определяются относительно местонахождения контейнера, на котором позиционируется графический объект. В Windows Runtime все преобразования задаются относительно элемента, к которому они применяются.

Очень часто бывает удобнее использовать при преобразовании другую точку вместо левого верхнего угла. Иногда эта точка называется «центром вращения». Ее можно задать тремя разными способами.

Первый способ наиболее четко проявляет математическую сторону преобразования, но я отложу его на потом.

Второй способ основан на использовании класса RotateTransform. Класс определяет свойства CenterX и CenterY, по умолчанию равные 0. Если вы хотите, чтобы этот конкретный объект TextBlock поворачивался относительно центра, задайте CenterX половину ширины, a CenterY - половину высоты объекта TextBlock. Эта информация может быть получена в обработчике Loaded; ниже приведен фрагмент, который может быть включен в конструктор из файла фонового кода. При этом нам пригодится имя, присвоенное полю TextBlock (и не используемое в файле XAML):

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

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

        private void Page_Loaded(object sender, RoutedEventArgs e)
        {
            RotateTransform tr = (RotateTransform)txb.RenderTransform;
            tr.CenterX = 0.5 * txb.ActualWidth;
            tr.CenterY = 0.5 * txb.ActualHeight;
        }
    }
}

Честно говоря, это решение выглядит немного громоздко, поэтому я рад сообщить, что третий вариант намного проще. В нем используется свойство RenderTransformOrigin, определяемое классом UIElement. Оно относится к типу Point, но его значения представляют собой относительные координаты, у которых значения X и Y лежат в диапазоне от 0 до 1. По умолчанию используется точка (0, 0), то есть левый верхний угол. Точка (1,0) соответствует правому верхнему углу, точка (0,1) - левому нижнему углу, а точка (1,1) - правому нижнему углу. Чтобы задать в качестве исходной точки центр элемента, используйте точку (0.5,0.5):

<TextBlock Name="txb" ... RenderTransformOrigin="0.5 0.5">
    ...
</TextBlock>

Обратите внимание: CenterX и CenterY являются свойствами RotateTransform, а свойство RenderTransformOrigin определяется UIElement и является общим для всех элементов. Если задать RenderTransformOrigin помимо CenterX и CenterY, эффекты объединяются. В нашем примере объединенным эффектом станет поворот вокруг правого нижнего угла TextBlock.

Центр вращения может находиться за пределами элемента. Следующий файл XAML располагает поле TextBlock в середине верхней части страницы, после чего начинает вращать его в бесконечной анимации:

<Page ... SizeChanged="Page_SizeChanged">

    <Grid Background="#FF1D1D1D">
        <TextBlock Name="txb"
                           Text="Поворот текста"
                           FontSize="40"
                           VerticalAlignment="Top"
                           HorizontalAlignment="Center">
            <TextBlock.RenderTransform>
                <RotateTransform x:Name="rtTransform" />
            </TextBlock.RenderTransform>
        </TextBlock>
    </Grid>

    <Page.Triggers>
        <EventTrigger>
            <BeginStoryboard>
                <Storyboard RepeatBehavior="Forever">
                    <DoubleAnimation Storyboard.TargetName="rtTransform"
                                     Storyboard.TargetProperty="Angle"
                                     From="0" To="360" Duration="0:0:1.8" />
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
    </Page.Triggers>
</Page>

Без какого-либо дополнительного кода эта программа поворачивает объект TextBlock вокруг его левого верхнего угла, и в отдельные моменты анимации объект выходит за пределы экрана. Конструктор в файле фонового кода определяет два обработчика событий для задания свойств CenterX и CenterY объекта RotateTransform:

public sealed partial class MainPage : Page
{
        public MainPage()
        {
            this.InitializeComponent();
            this.Loaded += Page_Loaded;
        }

        private void Page_Loaded(object sender, RoutedEventArgs e)
        {
            rtTransform.CenterX = txb.ActualWidth / 2;
        }

        private void Page_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            rtTransform.CenterY = e.NewSize.Height / 2;
        }
}

В качестве центра вращения задается точка, выровненная по горизонтальному центру TextBlock, но находящаяся ниже TextBlock на расстоянии, равном половине высоты страницы. В результате объект TextBlock описывает окружность относительно центра страницы.

Визуальная обратная связь

Анимационные преобразования хорошо привлекают внимание пользователя к тому, что происходит на экране и требует его внимания или разрешения на выполнения операции. В следующей программе я добавил новый элемент UserControl с именем JiggleButton, но затем сменил базовый класс в файлах XAML и C# с UserControl на Button. Полное содержимое файла JiggleButton.xaml выглядит так:

<Button x:Class="WinRTTestApp.JiggleButton"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    RenderTransformOrigin="0.5 0.5"
    Click="JiggleButton_Click">

    <Button.Resources>
        <Storyboard x:Key="animation">
            <DoubleAnimation Storyboard.TargetName="rtTransform"
                             Storyboard.TargetProperty="Angle"
                             AutoReverse="True"
                             From="0" To="10" Duration="0:0:0.33">
                <DoubleAnimation.EasingFunction>
                    <ElasticEase EasingMode="EaseIn" />
                </DoubleAnimation.EasingFunction>
            </DoubleAnimation>
        </Storyboard>
    </Button.Resources>

    <Button.RenderTransform>
        <RotateTransform x:Name="rtTransform" />
    </Button.RenderTransform>
</Button>

В этом файле XAML не определяется содержимое Button, но задаются три свойства Button: RenderTransformOrigin (в корневом теге), Resources и RenderTransform. Обычно для «качания» элемента посредством поворота необходимо использовать анимацию по ключевым кадрам, потому что сначала следует выполнить поворот от 0 до 10 (например) градусов, затем несколько раз от 10° до -10°, а потом снова вернуться к 0. Но функция ElasticEase со свойством EasingMode, равным EaseIn, является замечательной альтернативой. Объект DoubleAnimation определяется для поворота кнопки на 10° с последующим возвратом к 0, но функция ElasticEase задает широкий отрицательный ход, поэтому анимация фактически проходит в диапазоне от -10° до 10°.

Файл фонового кода кнопки JiggleButton просто инициирует анимацию в обработчике события Click:

using Windows.UI.Xaml.Media.Animation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace WinRTTestApp
{
    public sealed partial class JiggleButton : Button
    {
        public JiggleButton()
        {
            this.InitializeComponent();
        }

        private void JiggleButton_Click(object sender, RoutedEventArgs e)
        {
            ((Storyboard)this.Resources["animation"]).Begin();
        }
    }
}

Файл MainPage.xaml создает экземпляр JiggleButton, с которым можно поэкспериментировать:

<Grid Background="#FF1D1D1D">
     <local:JiggleButton FontSize="22"
                         HorizontalAlignment="Center"
                         VerticalAlignment="Center">
          Кнопка
     </local:JiggleButton>
</Grid>

He забудьте, что класс JiggleButton является производным от Button, поэтому он может использоваться как любой другой объект Button - впрочем, его не стоит задавать свойствам RenderTransform или RenderTransformOrigin, потому что это помешает анимации.

Для размещения современных нагруженных веб-приложений требуются отказоустойчивые серверы, имеющие облачную модель хранения данных, такой как, например, http://cloud.mts.by/. Данные в таких дата-центрах хранятся на распределённых в сети серверах, обеспечивая тем самым высокую производительность приложений. Использование облачных систем является лучшим выбором, если вам требуется безотказная работа вашего приложения.

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