SkiaSharp 中的基本动画
了解如何对 SkiaSharp 图形进行动画处理
通过定期调用 PaintSurface
方法,每次绘制略有不同的图形,可以在 Xamarin.Forms 中对 SkiaSharp 图形进行动画处理。 下面是本文后面显示的一个动画,其中包含似乎从中心展开的同心圆:
示例程序中的“脉动椭圆”页面对椭圆的两个轴进行动画处理,使其看起来正在脉动,你甚至可以控制此脉动的速率。 PulsatingEllipsePage.xaml 文件实例化一个 Xamarin.FormsSlider
和一个 Label
以显示滑块的当前值。 这是与其他 Xamarin.Forms 视图集成 SKCanvasView
的常见方法:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
x:Class="SkiaSharpFormsDemos.PulsatingEllipsePage"
Title="Pulsating Ellipse">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Slider x:Name="slider"
Grid.Row="0"
Maximum="10"
Minimum="0.1"
Value="5"
Margin="20, 0" />
<Label Grid.Row="1"
Text="{Binding Source={x:Reference slider},
Path=Value,
StringFormat='Cycle time = {0:F1} seconds'}"
HorizontalTextAlignment="Center" />
<skia:SKCanvasView x:Name="canvasView"
Grid.Row="2"
PaintSurface="OnCanvasViewPaintSurface" />
</Grid>
</ContentPage>
代码隐藏文件实例化一个 Stopwatch
对象以充当高精度时钟。 OnAppearing
重写将 pageIsActive
字段设置为 true
并调用名为 AnimationLoop
的方法。 OnDisappearing
重写将 pageIsActive
字段设置为 false
:
Stopwatch stopwatch = new Stopwatch();
bool pageIsActive;
float scale; // ranges from 0 to 1 to 0
public PulsatingEllipsePage()
{
InitializeComponent();
}
protected override void OnAppearing()
{
base.OnAppearing();
pageIsActive = true;
AnimationLoop();
}
protected override void OnDisappearing()
{
base.OnDisappearing();
pageIsActive = false;
}
AnimationLoop
方法在 pageIsActive
为 true
时启动 Stopwatch
,然后循环。 这本质上是一个页面处于活动状态时会进行的“无限循环”,但它不会导致程序挂起,因为循环最终使用 await
运算符调用 Task.Delay
,使得程序函数的其他部分可以正常工作。 Task.Delay
的参数导致它在 1/30 秒后完成。 这定义了动画的帧速率。
async Task AnimationLoop()
{
stopwatch.Start();
while (pageIsActive)
{
double cycleTime = slider.Value;
double t = stopwatch.Elapsed.TotalSeconds % cycleTime / cycleTime;
scale = (1 + (float)Math.Sin(2 * Math.PI * t)) / 2;
canvasView.InvalidateSurface();
await Task.Delay(TimeSpan.FromSeconds(1.0 / 30));
}
stopwatch.Stop();
}
while
循环首先从 Slider
中获取周期时间。 这是一个以秒为单位的时间,例如 5 秒。 第二个语句计算时间 t
的值。 如果 cycleTime
为 5,t
每 5 秒从 0 增加到 1。 第二个语句中 Math.Sin
函数的参数每 5 秒在 0 到 2π 之间变动。 Math.Sin
函数返回一个值,该值每 5 秒的变动规律为 0 -> 1 -> 0 -> -1 -> 0,但当数值接近 1 或 -1 时,值变化会较慢。 由于加上了值 1,因此值始终为正值,然后将其除以 2,因此值的变动规律为 1/2 -> 1 -> 1/2 -> 0 -> 1/2,但在值 1 和 0 附近变化会较慢。 这存储在 scale
字段中,并且 SKCanvasView
无效。
PaintSurface
方法使用此 scale
值计算椭圆的两个轴:
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
float maxRadius = 0.75f * Math.Min(info.Width, info.Height) / 2;
float minRadius = 0.25f * maxRadius;
float xRadius = minRadius * scale + maxRadius * (1 - scale);
float yRadius = maxRadius * scale + minRadius * (1 - scale);
using (SKPaint paint = new SKPaint())
{
paint.Style = SKPaintStyle.Stroke;
paint.Color = SKColors.Blue;
paint.StrokeWidth = 50;
canvas.DrawOval(info.Width / 2, info.Height / 2, xRadius, yRadius, paint);
paint.Style = SKPaintStyle.Fill;
paint.Color = SKColors.SkyBlue;
canvas.DrawOval(info.Width / 2, info.Height / 2, xRadius, yRadius, paint);
}
}
该方法根据显示区域的大小计算最大半径,以及基于最大半径的最小半径。 由于 scale
值在 0 -> 1 -> 0 之间的变动进行动画处理,因此该方法使用它来计算在 minRadius
和 maxRadius
之间变动的 xRadius
和 yRadius
。 这些值用于绘制和填充椭圆:
请注意,SKPaint
对象是在 using
块中创建的。 与许多 SkiaSharp 类一样,SKPaint
派生自 SKObject
,后者又派生自实现 IDisposable
接口的 SKNativeObject
。 SKPaint
替代 Dispose
方法以释放非托管资源。
将 SKPaint
放入 using
块可确保在该块末尾调用该 Dispose
以释放这些非托管资源。 虽然在 SKPaint
对象使用的内存被 .NET 垃圾回收器释放时,无论如何都会发生这种情况,但在动画代码中,最好以更有序的方式主动释放内存。
在这种特定情况下,更好的解决方法是一次创建两个 SKPaint
对象,并将其另存为字段。
这就是“扩展圆圈”动画的作用。 ExpandingCirclesPage
类首先定义多个字段,包括一个 SKPaint
对象:
public class ExpandingCirclesPage : ContentPage
{
const double cycleTime = 1000; // in milliseconds
SKCanvasView canvasView;
Stopwatch stopwatch = new Stopwatch();
bool pageIsActive;
float t;
SKPaint paint = new SKPaint
{
Style = SKPaintStyle.Stroke
};
public ExpandingCirclesPage()
{
Title = "Expanding Circles";
canvasView = new SKCanvasView();
canvasView.PaintSurface += OnCanvasViewPaintSurface;
Content = canvasView;
}
...
}
此程序使用基于 Xamarin.FormsDevice.StartTimer
方法的另一种动画处理方法。 t
字段每 cycleTime
毫秒从 0 到 1 之间变动:
public class ExpandingCirclesPage : ContentPage
{
...
protected override void OnAppearing()
{
base.OnAppearing();
pageIsActive = true;
stopwatch.Start();
Device.StartTimer(TimeSpan.FromMilliseconds(33), () =>
{
t = (float)(stopwatch.Elapsed.TotalMilliseconds % cycleTime / cycleTime);
canvasView.InvalidateSurface();
if (!pageIsActive)
{
stopwatch.Stop();
}
return pageIsActive;
});
}
protected override void OnDisappearing()
{
base.OnDisappearing();
pageIsActive = false;
}
...
}
PaintSurface
处理程序使用动画弧度绘制五个同心圆。 如果 baseRadius
变量计算结果为 100,则 t
从 0 变动到 1 时,五个圆的弧度分别从 0 增加到 100,从 100 增加到 200,从 200 增加到 300,从 300 增加到 400,从 400 增加到 500。 对于大多数圆,strokeWidth
为 50,但对于第一个圆,strokeWidth
从 0 变为 50。 对于大多数圆,颜色为蓝色,但对于最后一个圆,颜色从蓝色变为透明。 请注意指定不透明度的 SKColor
构造函数的第四个参数:
public class ExpandingCirclesPage : ContentPage
{
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
SKPoint center = new SKPoint(info.Width / 2, info.Height / 2);
float baseRadius = Math.Min(info.Width, info.Height) / 12;
for (int circle = 0; circle < 5; circle++)
{
float radius = baseRadius * (circle + t);
paint.StrokeWidth = baseRadius / 2 * (circle == 0 ? t : 1);
paint.Color = new SKColor(0, 0, 255,
(byte)(255 * (circle == 4 ? (1 - t) : 1)));
canvas.DrawCircle(center.X, center.Y, radius, paint);
}
}
}
结果是,图像在 t
等于 0 时与 t
等于 1 时看起来相同,并且圆似乎会永远在不断扩展: