第二十二章:动画(十四)-云栖社区-阿里云
你自己的等待动画
在本章的下一节中,您将看到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动画系统的其余部分如何实现这些函数
并允许您启动,控制和取消动画。