永远的动画
在入口动画的相反极端是永远的动画。 应用程序可以实现“永远”或至少在程序结束之前进行的动画。 这种动画的唯一目的通常是展示动画系统的功能,但最好是以令人愉快或有趣的方式。
第一个示例称为FadingTextAnimation,并使用FadeTo淡入和淡出两个Label元素。 XAML文件将两个Label元素放在单个单元格中,以便它们重叠。 第二个将其Opacity属性设置为0:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="FadingTextAnimation.FadingTextAnimationPage"
BackgroundColor="White"
SizeChanged="OnPageSizeChanged">
<ContentPage.Resources>
<ResourceDictionary>
<Style TargetType="Label">
<Setter Property="HorizontalTextAlignment" Value="Center" />
<Setter Property="VerticalTextAlignment" Value="Center" />
</Style>
</ResourceDictionary>
</ContentPage.Resources>
<Grid>
<Label x:Name="label1"
Text="MORE"
TextColor="Blue" />
<Label x:Name="label2"
Text="CODE"
TextColor="Red"
Opacity="0" />
</Grid>
</ContentPage>
创建一个“永远”运行的动画的一种简单方法是将所有动画代码放在一个while循环中,条件为true。 然后从构造函数中调用该方法:
public partial class FadingTextAnimationPage : ContentPage
{
public FadingTextAnimationPage()
{
InitializeComponent();
// Start the animation going.
AnimationLoop();
}
void OnPageSizeChanged(object sender, EventArgs args)
{
if (Width > 0)
{
double fontSize = 0.3 * Width;
label1.FontSize = fontSize;
label2.FontSize = fontSize;
}
}
async void AnimationLoop()
{
while (true)
{
await Task.WhenAll(label1.FadeTo(0, 1000),
label2.FadeTo(1, 1000));
await Task.WhenAll(label1.FadeTo(1, 1000),
label2.FadeTo(0, 1000));
}
}
}
无限循环通常是危险的,但是当Task.WhenAll方法表示完成两个动画时,这个循环非常短暂地执行一次 - 第一个淡出一个Label,第二个淡入另一个Label。 页面的SizeChanged处理程序设置文本的FontSize,因此文本接近页面的宽度:
这是“更多代码”还是“更多代码”? 也许两者。
这是另一个以文本为目标的动画。 PalindromeAnimation程序将单个字符旋转180度以将其颠倒。 幸运的是,角色包含一个向前和向后读取相同的回文:
当所有字符颠倒翻转时,翻转整个字符集,动画再次开始。
XAML文件只包含一个水平StackLayout,还没有任何子代:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="PalindromeAnimation.PalindromeAnimationPage"
SizeChanged="OnPageSizeChanged">
<StackLayout x:Name="stackLayout"
Orientation="Horizontal"
HorizontalOptions="Center"
VerticalOptions="Center"
Spacing="0" />
</ContentPage>
代码隐藏文件的构造函数使用17个Label元素填充此StackLayout,以拼出回文短语“NEVER ODD OR EVEN”。与前一个程序一样,页面的SizeChanged处理程序会调整这些标签的大小。 每个Label都有一个统一的WidthRequest和一个基于该宽度的FontSize。 文本字符串中的每个字符必须占据相同的宽度,以便它们在翻转时仍然间隔相同:
public partial class PalindromeAnimationPage : ContentPage
{
string text = "NEVER ODD OR EVEN";
double[] anchorX = { 0.5, 0.5, 0.5, 0.5, 1, 0,
0.5, 1, 1, -1,
0.5, 1, 0,
0.5, 0.5, 0.5, 0.5 };
public PalindromeAnimationPage()
{
InitializeComponent();
// Add a Label to the StackLayout for each character.
for (int i = 0; i < text.Length; i++)
{
Label label = new Label
{
Text = text[i].ToString(),
HorizontalTextAlignment = TextAlignment.Center
};
stackLayout.Children.Add(label);
}
// Start the animation.
AnimationLoop();
}
void OnPageSizeChanged(object sender, EventArgs args)
{
// Adjust the size and font based on the display width.
double width = 0.8 * this.Width / stackLayout.Children.Count;
foreach (Label label in stackLayout.Children.OfType<Label>())
{
label.FontSize = 1.4 * width;
label.WidthRequest = width;
}
}
async void AnimationLoop()
{
bool backwards = false;
while (true)
{
// Let's just sit here a second.
await Task.Delay(1000);
// Prepare for overlapping rotations.
Label previousLabel = null;
// Loop through all the labels.
IEnumerable<Label> labels = stackLayout.Children.OfType<Label>();
foreach (Label label in backwards ? labels.Reverse() : labels)
{
uint flipTime = 250;
// Set the AnchorX and AnchorY properties.
int index = stackLayout.Children.IndexOf(label);
label.AnchorX = anchorX[index];
label.AnchorY = 1;
if (previousLabel == null)
{
// For the first Label in the sequence, rotate it 90 degrees.
await label.RelRotateTo(90, flipTime / 2);
}
else
{
// For the second and subsequent, also finish the previous flip.
await Task.WhenAll(label.RelRotateTo(90, flipTime / 2),
previousLabel.RelRotateTo(90, flipTime / 2));
}
// If it's the last one, finish the flip.
if (label == (backwards ? labels.First() : labels.Last()))
{
await label.RelRotateTo(90, flipTime / 2);
}
previousLabel = label;
}
// Rotate the entire stack.
stackLayout.AnchorY = 1;
await stackLayout.RelRotateTo(180, 1000);
// Flip the backwards flag.
backwards ^= true;
}
}
}
AnimationLoop方法的大部分复杂性来自重叠动画。每个字母需要旋转180度。但是,每个字母旋转的最后90度与下一个字母的前90度重叠。这要求以不同方式处理第一个字母和最后一个字母。
AnchorX和AnchorY属性的设置使字母旋转更加复杂。对于每次旋转,AnchorY设置为1,旋转发生在Label的底部。但是AnchorX属性的设置取决于短语中字母出现的位置。 “NEVER”的前四个字母可以围绕字母的底部中心旋转,因为它们在倒置时形成“偶数”字样。但是“R”需要在其右下角旋转,以便它成为“OR”一词的结尾。 “NEVER”之后的空间需要围绕其左下角旋转,以便它成为“OR”和“EVEN”之间的空间。基本上,“永远”的“R”和空间交换的地方。这句话的其余部分继续类似。每个字母的各种AnchorX值存储在类顶部的anchorX数组中。
当所有字母都单独旋转时,整个StackLayout旋转180度。虽然在程序开始运行时,旋转的StackLayout看起来与StackLayout相同,但它不一样。该短语的最后一个字母现在是StackLayout中的第一个子节点,第一个字母现在是StackLayout中的最后一个子节点。这就是向后变量的原因。 foreach语句使用它来向前或向后枚举StackLayout子项。
您会注意到,动画启动之前,所有AnchorX和AnchorY属性都在AnimationLoop中设置,即使它们从未在程序过程中发生变化。这是为了适应iOS的问题。必须在元素大小后设置属性,并且在此循环中设置这些属性非常方便。
如果iOS的问题不存在,可以在程序的构造函数中甚至在XAML文件中设置所有AnchorX和AnchorY属性。在XAML文件中定义所有17个Label元素并使用每个Label上的唯一AnchorX设置和Style中的常见AnchorY设置并不是不合理的。
实际上,在iOS设备上,PalindromeAnimation程序无法承受从纵向到横向和背面的方向变化。调整Label元素的大小后,应用程序无法修复AnchorX和AnchorY属性的内部使用。
CopterAnimation程序模拟一个围绕页面围成一圈的小型直升机。然而,模拟非常简单:直升机只是两个BoxView元素的大小和排列
看起来像翅膀:
该计划有两个连续轮换。 快速的人将直升机的刀片围绕其中心旋转。 较慢的旋转使机翼组件围绕页面中心旋转一圈。 两个旋转都使用0.5的默认AnchorX和AnchorY设置,因此iOS上没有问题。
然而,程序隐含地使用手机的宽度作为直升机机翼飞行的圆周。 如果您将手机侧向转为横向模式,直升机实际上会飞出手机范围。
CopterAnimation简洁的秘诀是XAML文件:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="CopterAnimation.CopterAnimationPage">
<ContentView x:Name="revolveTarget"
HorizontalOptions="Fill"
VerticalOptions="Center">
<ContentView x:Name="copterView"
HorizontalOptions="End">
<AbsoluteLayout>
<BoxView AbsoluteLayout.LayoutBounds="20, 0, 20, 60"
Color="Accent" />
<BoxView AbsoluteLayout.LayoutBounds="0, 20, 60, 20"
Color="Accent" />
</AbsoluteLayout>
</ContentView>
</ContentView>
</ContentPage>
整个布局由两个嵌套的ContentView元素组成,内部ContentView中的AbsoluteLayout用于两个BoxView翼。 外部ContentView(名为revolveTarget)扩展到手机的宽度,并在页面上垂直居中,但它只与内部ContentView一样高。 内部ContentView(名为copterView)位于外部ContentView的最右侧。
如果关闭动画并为两个ContentView元素提供不同的背景颜色(例如蓝色和红色),则可以更容易地将其可视化:
现在你可以很容易地看到这两个ContentView元素可以围绕它们的中心旋转,以实现旋转翅膀飞行的效果:
public partial class CopterAnimationPage : ContentPage
{
public CopterAnimationPage()
{
InitializeComponent();
AnimationLoop();
}
async void AnimationLoop()
{
while (true)
{
revolveTarget.Rotation = 0;
copterView.Rotation = 0;
await Task.WhenAll(revolveTarget.RotateTo(360, 5000),
copterView.RotateTo(360 * 5, 5000));
}
}
}
这两个动画都有5秒的持续时间,但在此期间,外部ContentView仅围绕其中心旋转一次,而直升机机翼组件围绕其中心旋转五次。
RotatingSpokes程序从页面中心抽取24个辐条,其长度基于页面高度和宽度的较小值。 当然,每个辐条都是一个薄的BoxView元素:
三秒钟后,辐条组合开始围绕中心旋转。 这种情况持续了一段时间,然后每个人的说话开始围绕其中心旋转,形成一个有趣的变化模式:
与CopterAnimation一样,RotatingSpokes程序使用AnchorX和AnchorY的默认值进行所有旋转,因此在iOS设备上更改手机方向没有问题。
但是RotatingSpokes中的XAML文件只包含一个AbsoluteLayout,并且没有提示该程序的工作原理:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="RotatingSpokes.RotatingSpokesPage"
BackgroundColor="White"
SizeChanged="OnPageSizeChanged">
<AbsoluteLayout x:Name="absoluteLayout"
HorizontalOptions="Center"
VerticalOptions="Center" />
</ContentPage>
其他一切都是在代码中完成的。 构造函数向AbsoluteLayout添加了24个黑色BoxView元素,页面的SizeChanged处理程序将它们置于辐条模式中:
public partial class RotatingSpokesPage : ContentPage
{
const int numSpokes = 24;
BoxView[] boxViews = new BoxView[numSpokes];
public RotatingSpokesPage()
{
InitializeComponent();
// Create all the BoxView elements.
for (int i = 0; i < numSpokes; i++)
{
BoxView boxView = new BoxView
{
Color = Color.Black
};
boxViews[i] = boxView;
absoluteLayout.Children.Add(boxView);
}
AnimationLoop();
}
void OnPageSizeChanged(object sender, EventArgs args)
{
// Set AbsoluteLayout to a square dimension.
double dimension = Math.Min(this.Width, this.Height);
absoluteLayout.WidthRequest = dimension;
absoluteLayout.HeightRequest = dimension;
// Find the center and a size for the BoxView.
Point center = new Point(dimension / 2, dimension / 2);
Size boxViewSize = new Size(dimension / 2, 3);
for (int i = 0; i < numSpokes; i++)
{
// Find an angle for each spoke.
double degrees = i * 360 / numSpokes;
double radians = Math.PI * degrees / 180;
// Find the point of the center of each BoxView spoke.
Point boxViewCenter =
new Point(center.X + boxViewSize.Width / 2 * Math.Cos(radians),
center.Y + boxViewSize.Width / 2 * Math.Sin(radians));
// Find the upper-left corner of the BoxView and position it.
Point boxViewOrigin = boxViewCenter - boxViewSize * 0.5;
AbsoluteLayout.SetLayoutBounds(boxViews[i],
new Rectangle(boxViewOrigin, boxViewSize));
// Rotate the BoxView around its center.
boxViews[i].Rotation = degrees;
}
}
__
}
当然,渲染这些辐条的最简单方法是将所有24个薄的BoxView元素从AbsoluteLayout的中心直接向上延伸 - 很像前一章中BoxViewClock指针的初始12:00位置 - 然后旋转它们每个都围绕它的底部边缘增加15度。但是,这需要将这些BoxView元素的AnchorY属性设置为1以进行底边旋转。这对于这个程序是行不通的,因为每个BoxView元素必须稍后动画以围绕其中心旋转。
解决方案是首先计算AbsoluteLayout中每个BoxView中心的位置。这是名为boxViewCenter的SizeChanged处理程序中的Point值。如果BoxView的中心位于boxViewCenter,则boxViewOrigin就是BoxView的左上角。如果你注释掉for循环中设置每个BoxView的Rotation属性的最后一个语句,你会看到辐条定位如下:
所有水平线(顶部和底部除外)实际上是两个对齐的辐条。 每个辐条的中心距离页面中心的辐条长度的一半。 围绕其中心旋转每个辐条,然后创建您之前看到的初始图案。
这是AnimationLoop方法:
public partial class RotatingSpokesPage : ContentPage
{
__
async void AnimationLoop()
{
// Keep still for 3 seconds.
await Task.Delay(3000);
// Rotate the configuration of spokes 3 times.
uint count = 3;
await absoluteLayout.RotateTo(360 * count, 3000 * count);
// Prepare for creating Task objects.
List<Task<bool>> taskList = new List<Task<bool>>(numSpokes + 1);
while (true)
{
foreach (BoxView boxView in boxViews)
{
// Task to rotate each spoke.
taskList.Add(boxView.RelRotateTo(360, 3000));
}
// Task to rotate the whole configuration.
taskList.Add(absoluteLayout.RelRotateTo(360, 3000));
// Run all the animations; continue in 3 seconds.
await Task.WhenAll(taskList);
// Clear the List.
taskList.Clear();
}
}
}
在只有AbsoluteLayout本身的初步旋转之后,while块在旋转辐条和AbsoluteLayout时永远执行。请注意,创建了List >以存储25个并发任务。 foreach循环向此List添加一个Task,它为每个BoxView调用RelRotateTo,使轮辐在三秒内旋转360度。最后一个任务是AbsoluteLayout本身的另一个RelRotateTo。
在永久运行的动画中使用RelRotateTo时,目标Rotation属性会越来越大。实际旋转角度是Rotation属性模360的值。
Rotation属性不断增加的价值是一个潜在的问题吗?
从理论上讲,没有。即使底层平台使用单精度浮点数来表示旋转值,在值超过3.4×1038之前也不会出现问题。即使你每秒将Rotation属性增加360度,你也是在大爆炸(138亿年前)时开始动画,旋转值仅为4.4×1000000000000000000。
然而,实际上,问题可能会蔓延,并且比您想象的要快得多。旋转角度为36,000,000度,360度旋转100,000次,使得物体呈现一点点
不同于旋转角度0,并且对于更高的旋转角度,偏差变得更大。
如果你想探索这个,你会在本章的源代码中找到一个名为RotationBreakdown的程序。 该程序以相同的速度旋转两个BoxView元素,一个使用RotateTo从0到360度,另一个使用RelRotateTo,参数为36000.使用RotateTo旋转的BoxView通常模糊使用RelRotateTo旋转的BoxView,但是BoxView的底层是 红色,在一分钟之内,你开始看到红色的BoxView偷看。 程序运行的时间越长,偏差越大。
通常,当您组合动画时,您希望它们全部同时开始和结束。 但是其他时候,特别是对于永远运行的动画 - 你需要几个动画彼此独立运行,或者至少看起来像是独立运行。
SpinningImage程序就是这种情况。 该程序使用Image元素显示位图:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="SpinningImage.SpinningImagePage">
<Image x:Name="image"
Source="https://developer.xamarin.com/demo/IMG_0563.JPG"
Scale="0.5" />
</ContentPage>
通常,Image会在保持位图宽高比的同时渲染位图以适应屏幕。 在纵向模式下,渲染位图的宽度将与手机的宽度相同。 但是,如果“缩放”设置为0.5,则图像大小为该大小的一半。
然后使用RotateTo,RotateXTo和RotateYTo对代码隐藏文件进行动画处理,使其在空间中扭曲并几乎随机转动:
但是,您可能不希望以任何方式同步RotateTo,RotateXTo和RotateYTo,因为这会导致重复的模式。
这里的解决方案实际上确实创建了一个重复模式,但是长度为五分钟。 这是Task.WhenAll方法中三个动画的持续时间:
public partial class SpinningImagePage : ContentPage
{
public SpinningImagePage()
{
InitializeComponent();
AnimationLoop();
}
async void AnimationLoop()
{
uint duration = 5 * 60 * 1000; // 5 minutes
while (true)
{
await Task.WhenAll(
image.RotateTo(307 * 360, duration),
image.RotateXTo(251 * 360, duration),
image.RotateYTo(199 * 360, duration));
image.Rotation = 0;
image.RotationX = 0;
image.RotationY = 0;
}
}
}
在这五分钟的时间内,三个独立的动画各自进行不同数量的360度旋转:RotateTo为307次旋转,RotateXTo为251次旋转,RotateYTo为199次旋转。 这些都是素数。 他们没有共同的因素。 因此,在这五分钟的时间内,任何两次旋转都不会以相同的方式相互重合。
还有另一种创建同步但自动动画的方法,但它需要更深入地进入动画系统。 那将很快到来。