Легко заметить что в Магазине Windows Phone очень много приложений вообще работающих только в портретной ориентации. Отчасти это объясняется тем, что таково положение вещей по умолчанию в Windows Phone. Образцом же приложения, по максимуму использующему возможность опрокидывания экрана, можно считать стандартный Калькулятор.
В портретной ориентации мы получаем простой калькулятор. А в альбомной уже — инженерный.
Но самое прекрасное — это анимация перехода из одного режима в другой.
Разрабатывая свой небольшой проект: приложение TapHint, реализовал несколько вещей, улучшающих восприятие интерфейса программы. Почему-то мне показалась нетривиальной реализация такой полезной вещи как анимация переворота телефона. И в интернете как-то не без усилий была найдена информация по этой теме. Само приложение работает со стандартными NFC-метками NDEF формата, записывая в них информацию и, соответственно, читая её из них.
Для страницы приложения, показывающей записанную в метку информацию, и решено было применить поддержку анимированных переворачиваний. Результат выглядит так.
Портретный и альбомный вид:
Кадры из анимации промежуточных состояний:
Как оказалось, реализация обработок всех возможных видов опрокидывания телефона получается довольно громоздкой.
<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". Можно пояснить что «альбомная правая» — это альбомная при положении правой грани телефона внизу. Таким образом получаем одну портретную ориентацию и две альбомные. Ситуация отягчается тем, что необходимо описать переходы от одной ориентации к другой в обоих направлениях. И что может быть неочевидным — переход из одного альбомного положения в другое и обратно. Итого шесть переходов.
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