Поделиться через


Базовая анимация в 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. Эти значения используются для рисования и заполнения многоточия:

Тройной снимок экрана страницы Pulsating Ellipse

Обратите внимание, что SKPaint объект создается в блоке using . Как и многие классы SKPaint SkiaSharp, являются производными от SKObjectSKNativeObject, от которых реализуется 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);
        }
    }
}

Результатом является то, что изображение выглядит так же, как tt если равно 1, и круги, кажется, продолжают расширяться навсегда:

Тройной снимок экрана страницы