第二十二章:动画(十二)-云栖社区-阿里云

永远的动画在入口动画的相反极端是永远的动画。 应用程序可以实现“永远”或至少在程序结束之前进行的动画。 这种动画的唯一目的通常是展示动画系统的功能,但最好是以令人愉快或有趣的方式。第一个示例称为FadingTextAnimation,并使用FadeTo淡入和淡出两个Label元素。

永远的动画
在入口动画的相反极端是永远的动画。 应用程序可以实现“永远”或至少在程序结束之前进行的动画。 这种动画的唯一目的通常是展示动画系统的功能,但最好是以令人愉快或有趣的方式。
第一个示例称为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,因此文本接近页面的宽度:
2019_03_11_141442
这是“更多代码”还是“更多代码”? 也许两者。
这是另一个以文本为目标的动画。 PalindromeAnimation程序将单个字符旋转180度以将其颠倒。 幸运的是,角色包含一个向前和向后读取相同的回文:
2019_03_11_141534
当所有字符颠倒翻转时,翻转整个字符集,动画再次开始。
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元素的大小和排列
看起来像翅膀:
2019_03_11_142413
该计划有两个连续轮换。 快速的人将直升机的刀片围绕其中心旋转。 较慢的旋转使机翼组件围绕页面中心旋转一圈。 两个旋转都使用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元素提供不同的背景颜色(例如蓝色和红色),则可以更容易地将其可视化:
2019_03_11_142713
现在你可以很容易地看到这两个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元素:
2019_03_11_142840
三秒钟后,辐条组合开始围绕中心旋转。 这种情况持续了一段时间,然后每个人的说话开始围绕其中心旋转,形成一个有趣的变化模式:
2019_03_11_143133
与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属性的最后一个语句,你会看到辐条定位如下:
2019_03_11_143648
所有水平线(顶部和底部除外)实际上是两个对齐的辐条。 每个辐条的中心距离页面中心的辐条长度的一半。 围绕其中心旋转每个辐条,然后创建您之前看到的初始图案。
这是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对代码隐藏文件进行动画处理,使其在空间中扭曲并几乎随机转动:
2019_03_11_144240
但是,您可能不希望以任何方式同步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次旋转。 这些都是素数。 他们没有共同的因素。 因此,在这五分钟的时间内,任何两次旋转都不会以相同的方式相互重合。
还有另一种创建同步但自动动画的方法,但它需要更深入地进入动画系统。 那将很快到来。