Пользователь
0,0
рейтинг
11 декабря 2015 в 13:11

Разработка → Анимированное изменение ориентации экрана в приложении Windows Phone из песочницы

Легко заметить что в Магазине Windows Phone очень много приложений вообще работающих только в портретной ориентации. Отчасти это объясняется тем, что таково положение вещей по умолчанию в Windows Phone. Образцом же приложения, по максимуму использующему возможность опрокидывания экрана, можно считать стандартный Калькулятор.



В портретной ориентации мы получаем простой калькулятор. А в альбомной уже — инженерный.



Но самое прекрасное — это анимация перехода из одного режима в другой.

Разрабатывая свой небольшой проект: приложение TapHint, реализовал несколько вещей, улучшающих восприятие интерфейса программы. Почему-то мне показалась нетривиальной реализация такой полезной вещи как анимация переворота телефона. И в интернете как-то не без усилий была найдена информация по этой теме. Само приложение работает со стандартными NFC-метками NDEF формата, записывая в них информацию и, соответственно, читая её из них.

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

Портретный и альбомный вид:





Кадры из анимации промежуточных состояний:





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

XAML страницы ShowPage.xaml
<phone:PhoneApplicationPage
    x:Class="Tap_Hint.ShowPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
    xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}"
    SupportedOrientations="PortraitOrLandscape" Orientation="Portrait"
    mc:Ignorable="d"
    shell:SystemTray.IsVisible="True"
    Name="showPage" OrientationChanged="showPage_OrientationChanged">

    <phone:PhoneApplicationPage.Projection>
        <PlaneProjection x:Name="showPageRotation" CenterOfRotationX="0" RotationY="0"/>
    </phone:PhoneApplicationPage.Projection>

    <phone:PhoneApplicationPage.Resources>
        <Storyboard x:Name="showPageStoryboardTo">
            <DoubleAnimation From="-55.0" To="0.0" Duration="00:00:00.35" Storyboard.TargetName="showPageRotation" Storyboard.TargetProperty="RotationY">
                <DoubleAnimation.EasingFunction>
                    <CubicEase EasingMode="EaseIn"/>
                </DoubleAnimation.EasingFunction>
            </DoubleAnimation>
        </Storyboard>
    </phone:PhoneApplicationPage.Resources>

    <!--LayoutRoot is the root grid where all page content is placed-->
    <Grid x:Name="LayoutRoot" Background="Transparent">

        <!-- VisualStateManager.VisualStateGroups must be defined in main grid -->
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup>
                <VisualState x:Name="FromPToLR"> <!-- from portrait to landscape right and so on -->
                    <Storyboard>
                        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="showTransform" Storyboard.TargetProperty="TranslateY">
                            <DiscreteObjectKeyFrame KeyTime="0" Value="-16"/>
                        </ObjectAnimationUsingKeyFrames>
                        <DoubleAnimation From="190.0" To="0.0" Duration="00:00:00.50" Storyboard.TargetName="showTransform" Storyboard.TargetProperty="TranslateX">
                            <DoubleAnimation.EasingFunction>
                                <CubicEase EasingMode="EaseInOut"/>
                            </DoubleAnimation.EasingFunction>
                        </DoubleAnimation>
                        <DoubleAnimation From="90.0" To="0.0" Duration="00:00:00.50" Storyboard.TargetName="showTransform" Storyboard.TargetProperty="Rotation">
                            <DoubleAnimation.EasingFunction>
                                <CubicEase EasingMode="EaseInOut"/>
                            </DoubleAnimation.EasingFunction>
                        </DoubleAnimation>
                    </Storyboard>
                </VisualState>
                <VisualState x:Name="FromPToLL">
                    <Storyboard>
                        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="showTransform" Storyboard.TargetProperty="TranslateY">
                            <DiscreteObjectKeyFrame KeyTime="0" Value="-16"/>
                        </ObjectAnimationUsingKeyFrames>
                        <DoubleAnimation From="-190.0" To="0.0" Duration="00:00:00.50" Storyboard.TargetName="showTransform" Storyboard.TargetProperty="TranslateX">
                            <DoubleAnimation.EasingFunction>
                                <CubicEase EasingMode="EaseInOut"/>
                            </DoubleAnimation.EasingFunction>
                        </DoubleAnimation>
                        <DoubleAnimation From="-90.0" To="0.0" Duration="00:00:00.50" Storyboard.TargetName="showTransform" Storyboard.TargetProperty="Rotation">
                            <DoubleAnimation.EasingFunction>
                                <CubicEase EasingMode="EaseInOut"/>
                            </DoubleAnimation.EasingFunction>
                        </DoubleAnimation>
                    </Storyboard>
                </VisualState>
                <VisualState x:Name="FromLRToP">
                    <Storyboard>
                        <DoubleAnimation From="0.0" To="-190.0" Duration="00:00:00.50" Storyboard.TargetName="showTransform" Storyboard.TargetProperty="TranslateY">
                            <DoubleAnimation.EasingFunction>
                                <CubicEase EasingMode="EaseInOut"/>
                            </DoubleAnimation.EasingFunction>
                        </DoubleAnimation>
                        <DoubleAnimation From="-90.0" To="0.0" Duration="00:00:00.50" Storyboard.TargetName="showTransform" Storyboard.TargetProperty="Rotation">
                            <DoubleAnimation.EasingFunction>
                                <CubicEase EasingMode="EaseInOut"/>
                            </DoubleAnimation.EasingFunction>
                        </DoubleAnimation>
                    </Storyboard>
                </VisualState>
                <VisualState x:Name="FromLLToP">
                    <Storyboard>
                        <DoubleAnimation From="0.0" To="-190.0" Duration="00:00:00.50" Storyboard.TargetName="showTransform" Storyboard.TargetProperty="TranslateY">
                            <DoubleAnimation.EasingFunction>
                                <CubicEase EasingMode="EaseInOut"/>
                            </DoubleAnimation.EasingFunction>
                        </DoubleAnimation>
                        <DoubleAnimation From="90.0" To="0.0" Duration="00:00:00.50" Storyboard.TargetName="showTransform" Storyboard.TargetProperty="Rotation">
                            <DoubleAnimation.EasingFunction>
                                <CubicEase EasingMode="EaseInOut"/>
                            </DoubleAnimation.EasingFunction>
                        </DoubleAnimation>
                    </Storyboard>
                </VisualState>
                <VisualState x:Name="FromLRToLL">
                    <Storyboard>
                        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="showTransform" Storyboard.TargetProperty="TranslateY">
                            <DiscreteObjectKeyFrame KeyTime="0" Value="-16"/>
                        </ObjectAnimationUsingKeyFrames>
                        <DoubleAnimation From="-180.0" To="0.0" Duration="00:00:00.50" Storyboard.TargetName="showTransform" Storyboard.TargetProperty="Rotation">
                            <DoubleAnimation.EasingFunction>
                                <CubicEase EasingMode="EaseInOut"/>
                            </DoubleAnimation.EasingFunction>
                        </DoubleAnimation>
                    </Storyboard>
                </VisualState>
                <VisualState x:Name="FromLLToLR">
                    <Storyboard>
                        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="showTransform" Storyboard.TargetProperty="TranslateY">
                            <DiscreteObjectKeyFrame KeyTime="0" Value="-16"/>
                        </ObjectAnimationUsingKeyFrames>
                        <DoubleAnimation From="180.0" To="0.0" Duration="00:00:00.50" Storyboard.TargetName="showTransform" Storyboard.TargetProperty="Rotation">
                            <DoubleAnimation.EasingFunction>
                                <CubicEase EasingMode="EaseInOut"/>
                            </DoubleAnimation.EasingFunction>
                        </DoubleAnimation>
                    </Storyboard>
                </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>

        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <!--TitlePanel contains the name of the application and page title-->
        <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="0,0,0,0" Orientation="Horizontal">
            <Image Width="32" Height="32" Source="/Tap Hint - logo mini (tr, white).png" Margin="5,0,0,0"/>
            <TextBlock Canvas.ZIndex="8" Text="{Binding Path=LocalizedResources.AboutPage_ApplicationTitle, Source={StaticResource LocalizedStrings}}" FontSize="22" Margin="5,0,0,0"/>
            <!--<TextBlock Name="textBlockPlus" Canvas.ZIndex="8" Text="{Binding Path=LocalizedResources.AboutPage_ApplicationTitle_Plus, Source={StaticResource LocalizedStrings}}" FontSize="20" Margin="5,0,0,0" Foreground="#FF4959FF"/>-->
        </StackPanel>

        <!--ContentPanel - place additional content here-->
        <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
            <Border x:Name="showBorder" Grid.Row="1" Width="400" Height="300" BorderThickness="5" RenderTransformOrigin="0.5,0.5">
                <Border.RenderTransform>
                    <CompositeTransform x:Name="showTransform" Rotation="0" TranslateY="-190" TranslateX="0"/>
                </Border.RenderTransform>
                <ScrollViewer x:Name="showScroller" VerticalScrollBarVisibility="Auto">
                    <TextBlock x:Name="showTextBlock" TextAlignment="Center" TextWrapping="Wrap" ScrollViewer.VerticalScrollBarVisibility="Auto"/>
                </ScrollViewer>
            </Border>
        </Grid>
    </Grid>
</phone:PhoneApplicationPage>


Для начала нужно указать с помощью параметра SupportedOrientations=«PortraitOrLandscape», что поддерживаем обе ориентации страницы приложения. Параметр Orientation=«Portrait» задаёт ориентацию по умолчанию. Но это будет лишь опрокидывать страницу туда-сюда без всяких анимаций. Для описания набора анимаций нужно задействовать VisualStateManager. В нём можно описать все переходы-VisualState'ы, которыми хотим воспользоваться в различных ситуациях. В каждом VisualState можно описать Storyboard с анимациями (если их несколько, то они проигрываются одновременно).

Например, VisualState для перехода из портретной ориентации в альбомную правую можно назвать x:Name="FromPToLR". Можно пояснить что «альбомная правая» — это альбомная при положении правой грани телефона внизу. Таким образом получаем одну портретную ориентацию и две альбомные. Ситуация отягчается тем, что необходимо описать переходы от одной ориентации к другой в обоих направлениях. И что может быть неочевидным — переход из одного альбомного положения в другое и обратно. Итого шесть переходов.

CS страницы ShowPage.xaml.cs
using System.Linq;
using System.Windows;
using System.Windows.Navigation;
using System.Windows.Media;
using Microsoft.Phone.Controls;

namespace Tap_Hint
{
    public partial class ShowPage : PhoneApplicationPage
    {
        private PageOrientation m_ePageOrientation = PageOrientation.PortraitUp;

        public ShowPage()
        {
            InitializeComponent();

            bool isDefaultColor = SettingsStore.get().extractParamBool(SettingsStore.StoringParams.USE_DEFAULT_COLOR.ToString());
            showTextBlock.FontSize = SettingsStore.g_FontSizeStep * (SettingsStore.get().extractParamInt(SettingsStore.StoringParams.FONT_SIZE.ToString()) + 1);
            if (isDefaultColor)
            {
                showTextBlock.Foreground = (Brush)Application.Current.Resources["PhoneAccentBrush"];
            }
            else
            {
                showTextBlock.Foreground = new SolidColorBrush(Colors.White);
            }
        }

        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            if (e.NavigationMode == NavigationMode.New)
            {
                showPageStoryboardTo.Begin();
                if (NavigationContext.QueryString.Values != null && NavigationContext.QueryString.Values.ToArray() != null &&
                    NavigationContext.QueryString.Values.ToArray().Length > 0)
                {
                    if ("tagParam".Equals(NavigationContext.QueryString.Keys.ElementAt(0)))
                    {
                        showTextBlock.Text = NavigationContext.QueryString.Values.ElementAt(0);
                    }
                    if ("isEnc".Equals(NavigationContext.QueryString.Keys.ElementAt(1)) && "true".Equals(NavigationContext.QueryString.Values.ElementAt(1)))
                    {
                        showBorder.BorderBrush = new SolidColorBrush(Color.FromArgb(255, 250, 250, 0));
                    }
                    else if ("isEnc".Equals(NavigationContext.QueryString.Keys.ElementAt(1)) && "false".Equals(NavigationContext.QueryString.Values.ElementAt(1)))
                    {
                        showBorder.BorderBrush = new SolidColorBrush(Color.FromArgb(255, 20, 230, 30));
                    }
                    else if ("isEnc".Equals(NavigationContext.QueryString.Keys.ElementAt(1)) && "spec".Equals(NavigationContext.QueryString.Values.ElementAt(1)))
                    {//spec - error or encoded on enother device
                        showBorder.BorderBrush = new SolidColorBrush(Color.FromArgb(255, 240, 30, 30));
                    }
                }
                else
                {
                    showTextBlock.Text = "no param";
                }
            }
        }

        protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
        {
            if (e.NavigationMode != NavigationMode.Back)
            {//leave the page due to long back button or Windows button, stay on the page
                return;
            }

            App.Current.Terminate(); //application exiting
        }

        private void showPage_OrientationChanged(object sender, OrientationChangedEventArgs e)
        {
            //playback animations on orientation change
            if (m_ePageOrientation == PageOrientation.PortraitUp && e.Orientation == PageOrientation.LandscapeRight)
            {
                VisualStateManager.GoToState(this, "FromPToLR", true);
            }
            else if (m_ePageOrientation == PageOrientation.PortraitUp && e.Orientation == PageOrientation.LandscapeLeft)
            {
                VisualStateManager.GoToState(this, "FromPToLL", true);
            }
            else if (m_ePageOrientation == PageOrientation.LandscapeRight && e.Orientation == PageOrientation.PortraitUp)
            {
                VisualStateManager.GoToState(this, "FromLRToP", true);
            }
            else if (m_ePageOrientation == PageOrientation.LandscapeLeft && e.Orientation == PageOrientation.PortraitUp)
            {
                VisualStateManager.GoToState(this, "FromLLToP", true);
            }
            else if (m_ePageOrientation == PageOrientation.LandscapeRight && e.Orientation == PageOrientation.LandscapeLeft)
            {
                VisualStateManager.GoToState(this, "FromLRToLL", true);
            }
            else if (m_ePageOrientation == PageOrientation.LandscapeLeft && e.Orientation == PageOrientation.LandscapeRight)
            {
                VisualStateManager.GoToState(this, "FromLLToLR", true);
            }
            
            //saving current orientation mode
            if (e.Orientation == PageOrientation.PortraitUp)
            {
                m_ePageOrientation = PageOrientation.PortraitUp;
            }
            else if (e.Orientation == PageOrientation.LandscapeRight)
            {
                VisualStateManager.GoToState(this, "LandscapeState", true);
                m_ePageOrientation = PageOrientation.LandscapeRight;
            }
            else if (e.Orientation == PageOrientation.LandscapeLeft)
            {
                m_ePageOrientation = PageOrientation.LandscapeLeft;
            }
        }   
    }
}


В коде страницы остаётся воспользоваться обработчиком изменения ориентации — showPage_OrientationChanged(). Анализируя то какой была ориентация с помощью значения переменной m_ePageOrientation и то какой ориентация экрана стала в e.Orientation, можно применить нужную анимацию.

Остаётся надеяться что будет больше приложений для телефонов, в которых не будут лениться делать эти полезные украшательства.
@habracom
карма
6,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (15)

  • 0
    Остаётся надеяться что будет больше приложений для телефонов, в которых не будут лениться делать эти полезные украшательства.

    А что в них такого полезного? Ориентация экрана во время работы приложения меняется редко, обычно один раз, чтобы привести его в удобное положение. Зависимость функционала от ориентации, как в калькуляторе — редкость.
    Чтобы было больше приложений, оно должно быть если не из коробки, то хотя бы подключаться с помощью нехитрых библиотек. Руками всё это делать мало кто захочет.
    • 0
      Уже не редкость. Все стандартные приложения (почта, скайдрайв и тп) меняют функционал. Примерно как на планшетах. Появляется боковая панель, и в горизонтальном разрезе функционала становится больше, ведь и ширина по горизонтали тоже возрастает. Это некий стандарт уже у них становится.
  • 0
    Украшательства могут быть красивыми, а полезными — никогда. Полезной может быть только функциональность.
    • +1
      Часто ценой таких украшательств становится более лучший UX, посмею оспорить тезис о не полезности :)
      • +1
        Да, я эту тенденцию вижу на примере Windows: Win95 можно было освоить методом тыка, а Win7 я уже только через Хелп осваивал, вчера ткнулся в 8-ку — даже не смог найти место, откуда выводится список установленных программ! Спасибо, украсили так украсили — стало гораздо лучше!
        • +1
          Хм… на мой взгляд особо сильно со времен 95 ничего не изменилось :)
        • +1
          Возможно, дело не в Windows?
  • 0
    Лучше бы вы не показывали код…
    Переходы в в стейты можно сделать декларативно в XAML вместо code-behind. И если уж полезли в behind то вместо кучи стейтов можно было бы запилить это всё одним Storyboard'ом с изменяемым настройками в зависимости от текущего положения. Полагаю кода было бы даже меньше.
    • 0
      Спасибо за советы. Может можно это и эффективней написать. Но хотелось бы чтоб ещё и запутанность не становилась ещё более запутанной:).
  • 0
    Не знал, что в штатном калькуляторе есть нужный мне функционал, поэтому пользуюсь сторонним.
    Вообще пользуюсь только портретной ориентацией и изменение заблокировано.
    Ради калькулятора придется что-то менять теперь…
    • 0
      Более того, если повернуть в другую сторону, там будет программистский режим, с hex'ом. Итого три разных калькулятора в зависимости от ориентации экрана. Никогда бы в голову не пришло.

      Майкрософт, ну твоюжежналево! Закопали так закопали. Шикарный UX, ничего не скажешь.
      • 0
        Ага, по-хорошему, приложение должно рассказывать об этом при первом запуске.
      • 0
        Опять же в вин10 этого уже нет. Есть гамбургер и там переключение режима калькулятора. С конвертерами величин и прочими радостями жизни.

        ЗЫ: Вы может и о подсчете ипотеки в стандартном виндовом калькуляторе не знали? :)
        • 0
          Гамбургер — так себе метафора, но он уже общепринятый, и с ним всё понятно. Меня вымораживало, что в Win8 у калькулятора есть режимы, а в WP8 у того же самого приложения их нету.

          BTW, в онлайновом хелпе эта фича упоминается, но её хрен найдёшь, потому что в секции «настройки» -> «блокировка вращения экрана», последний абзац. Сейчас вот только обратил внимание.
      • +1
        Да, надо признать, тоже был не в курсе того, что в калькуляторе разные альбомные ориентации. В принципе переворачивание — это удобный и эффектный способ изменения функциональности, но конечно бывает неочевидным.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.