第二十二章:动画(十二)-云栖社区-阿里云
永远的动画
在入口动画的相反极端是永远的动画。 应用程序可以实现“永远”或至少在程序结束之前进行的动画。 这种动画的唯一目的通常是展示动画系统的功能,但最好是以令人愉快或有趣的方式。
第一个示例称为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次旋转。 这些都是素数。 他们没有共同的因素。 因此,在这五分钟的时间内,任何两次旋转都不会以相同的方式相互重合。
还有另一种创建同步但自动动画的方法,但它需要更深入地进入动画系统。 那将很快到来。