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

你自己的等待动画在本章的下一节中,您将看到Xamarin.Forms实现的基础动画基础结构。这些底层方法允许您定义自己的动画函数,这些函数返回Task对象,并且可以与await一起使用。在第20章“异步和文件I / O”中,您了解了如何使用静态Task.Run方法创建执行的辅助线程,以执行像Mandelbrot计算这样的密集后台作业。

你自己的等待动画
在本章的下一节中,您将看到Xamarin.Forms实现的基础动画基础结构。这些底层方法允许您定义自己的动画函数,这些函数返回Task对象,并且可以与await一起使用。
在第20章“异步和文件I / O”中,您了解了如何使用静态Task.Run方法创建执行的辅助线程,以执行像Mandelbrot计算这样的密集后台作业。 Task.Run方法返回一个Task对象,该对象可以在后台作业完成时发出信号。
但动画并不是那样的。动画不需要花费大量时间来处理数字。它只需要做一些非常简单和简单的事情 - 比如设置一个Rotation属性 - 每16毫秒一次。该作业可以在用户界面线程中运行 - 事实上,实际的属性访问必须在用户界面线程中运行 - 并且可以使用Device.StartTimer或Task.Delay来处理时间。
您不应该使用Task.Run来实现动画,因为执行的辅助线程是不必要的并且是浪费的。但是,当您实际坐下来编写类似于Xamarin.Forms动画方法(如RotateTo)的动画方法时,您可能会遇到障碍。该方法必须返回一个Task对象,并且可能使用Device.StartTimer作为计时,但这似乎不可能。
这是第一次尝试编写这样的方法。 参数包括目标VisualElement,from和to值以及持续时间。 它使用Device.StartTimer和Stopwatch来计算Rotation属性的当前设置,并在动画完成时退出Device.StartTimer回调:

Task MyRotate(VisualElement visual, double fromValue, double toValue, uint duration)
{
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();
    Device.StartTimer(TimeSpan.FromMilliseconds(16), () =>
    {
        double t = Math.Min(1, stopwatch.ElapsedMilliseconds / (double)duration);
        double value = fromValue + t * (toValue - fromValue);
        visual.Rotation = value;
        bool completed = t == 1;
 
        if (completed)
        {
            // Need to signal that the Task has completed. But how?
        }
        return !completed; 
    });
    // Need to return a Task object here but where does it come from?
}

在两个关键点上,该方法不知道该怎么做。在方法调用Device.StartTimer之后,它需要退出并将Task对象返回给调用者。但是这个Task对象来自哪里? Task类有一个构造函数,但是像Task.Run一样,该构造函数创建了第二个执行线程,并且没有理由创建该线程。此外,当动画结束时,该方法需要以某种方式表示任务已完成。
幸运的是,存在一个完全符合您要求的类。 它叫做TaskCreationSource。 它是一个泛型类,其中type参数与要创建的Task对象的type参数相同。 askCreationSource对象的Task属性提供了您需要的Task对象。 这是您的异步方法返回的内容。 当您的方法完成处理后台作业时,它可以在TaskCreationSource对象上调用SetResult,表示作业已完成。
以下TryAwaitableAnimation程序显示如何在从Button的Clicked处理程序调用的MyRotateTo方法中使用TaskCreationSource:

public partial class TryAwaitableAnimationPage : ContentPage
{
    public TryAwaitableAnimationPage()
    {
        InitializeComponent();
    }
    async void OnButtonClicked(object sender, EventArgs args)
    {
        Button button = (Button)sender;
        uint milliseconds = UInt32.Parse((string)button.StyleId);
        await MyRotate(button, 0, 360, milliseconds);
    }
    Task MyRotate(VisualElement visual, double fromValue, double toValue, uint duration)
    {
        TaskCompletionSource<object> taskCompletionSource = new TaskCompletionSource<object>();
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        Device.StartTimer(TimeSpan.FromMilliseconds(16), () =>
            {
                double t = Math.Min(1, stopwatch.ElapsedMilliseconds / (double)duration);
                double value = fromValue + t * (toValue - fromValue);
                visual.Rotation = value;
                bool completed = t == 1;
 
                if (completed)
                {
                    taskCompletionSource.SetResult(null);
                }
                return !completed; 
        });
        return taskCompletionSource.Task;
    }
}

注意TaskCreationSource的实例化,该对象的Task属性的返回值,以及动画完成后对Device.StartTimer回调内的SetResult的调用。
TaskCreationSource没有非通用形式,因此如果您的方法只返回Task对象而不是Task 对象,则在定义TaskCreationSource实例时需要指定类型。 按照惯例,您可以使用object来实现此目的,在这种情况下,您的方法使用null参数调用SetResult。
TryAwaitableAnimation XAML文件实例化共享此Clicked处理程序的三个Button元素。 它们中的每一个都将自己的动画持续时间定义为StyleId属性。 (正如您所记得的,StyleId不在Xamarin.Forms中使用,仅供应用程序员使用,作为将任意数据附加到元素的便捷方式。)

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="TryAwaitableAnimation.TryAwaitableAnimationPage">
    <StackLayout>
        <StackLayout.Resources>
            <ResourceDictionary>
                <Style TargetType="Button">
                    <Setter Property="Text" Value="Tap Me!" />
                    <Setter Property="FontSize" Value="Large" />
                    <Setter Property="HorizontalOptions" Value="Center" />
                    <Setter Property="VerticalOptions" Value="CenterAndExpand" />
                </Style>
            </ResourceDictionary>
        </StackLayout.Resources>
        <Button Clicked="OnButtonClicked" StyleId="5000" />
        <Button Clicked="OnButtonClicked" StyleId="2500" />
        <Button Clicked="OnButtonClicked" StyleId="1000" />
    </StackLayout>
</ContentPage>

即使这些Button元素中的每一个都通过调用MyRotate来设置动画,您也可以让所有按钮同时旋转。每次调用MyRotate都会获得自己的局部变量集,并在每个Device.StartTimer回调中使用这些局部变量。
但是,如果在按钮仍处于旋转状态时点击按钮,则会向该按钮应用第二个动画,并且两个动画相互争斗。代码需要的是在应用新动画时取消上一个动画的方法。
一种方法是MyRotate方法维护Dictionary 定义为字段。每当它开始动画时,MyRotate都会将目标VisualElement作为该字典的键添加,其值为false。动画结束时,它会从字典中删除此条目。一个单独的方法(可能名为CancelMyRotate)可以将字典中的值设置为true,这意味着取消动画。 Device.StartTimer回调可以通过检查特定VisualElement的字典值开始,如果动画已被取消,则从回调返回false。但是你会在讨论中发现如何用更少的代码来完成它。
现在您已经看到了ViewExtensions类中实现的高级动画函数,让我们来探讨Xamarin.Forms动画系统的其余部分如何实现这些函数
并允许您启动,控制和取消动画。