Базовая анимация в SkiaSharp
Узнайте, как анимировать графику SkiaSharp
Вы можете анимировать графику Xamarin.Forms SkiaSharp, вызывая PaintSurface
метод периодически, каждый раз при рисовании графики немного иначе. Ниже приведена анимация, показанная далее в этой статье с концентрическими кругами, которые, казалось бы, расширяются с центра:
Страница Pulsating Ellipse в примере программы анимирует две оси многоточия, чтобы она, как представляется, пульсирует, и вы даже можете контролировать частоту этой пульсации. Файл PulsatingEllipsePage.xaml создает экземпляр и Xamarin.FormsSlider
отображает Label
текущее значение ползунка. Это распространенный способ интеграции SKCanvasView
с другими Xamarin.Forms представлениями:
<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
запускается Stopwatch
, а затем циклы во время pageIsActive
выполнения true
. Это по сути "бесконечный цикл" во время активности страницы, но это не приводит к зависанию программы, так как цикл завершается вызовом Task.Delay
оператора await
, который позволяет другим частям функции программы. Аргумент, который 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
от 0 до 1 каждые 5 секунд. Аргумент Math.Sin
функции во втором операторе составляет от 0 до 2π каждые 5 секунд. Функция Math.Sin
возвращает значение от 0 до 1 обратно до 0, а затем до –1 и 0 каждые 5 секунд, но со значениями, которые изменяются медленнее, когда значение почти 1 или –1. Значение 1 добавляется так, что значения всегда положительны, а затем делятся на 2, поэтому значения варьируются от 1/2 до 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, поэтому метод использует его для вычисления xRadius
и yRadius
диапазонов между minRadius
и maxRadius
. Эти значения используются для рисования и заполнения многоточия:
Обратите внимание, что SKPaint
объект создается в блоке using
. Как и многие классы SKPaint
SkiaSharp, являются производными от SKObject
SKNativeObject
, от которых реализуется IDisposable
интерфейс. 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
анимировано от 0 до 1 миллисекунда cycleTime
:
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
t
если равно 1, и круги, кажется, продолжают расширяться навсегда: