SkiaSharp 位图平铺

如前两篇文章所示,该 SKShader 类可以创建线性或圆形渐变。 本文重点介绍使用位图平铺区域的 SKShader 对象。 位图可以水平和垂直重复,无论是在其原始方向上还是水平和垂直翻转。 翻转可避免图块之间的不连续:

位图平铺示例

创建此着色器的静态 SKShader.CreateBitmap 方法具有 SKBitmap 参数和 SKShaderTileMode 枚举的两个成员:

public static SKShader CreateBitmap (SKBitmap src, SKShaderTileMode tmx, SKShaderTileMode tmy)

这两个参数指示用于水平平铺和垂直平铺的模式。 这是与渐变方法一起使用的相同 SKShaderTileMode 枚举。

CreateBitmap 重载包括用于对平铺位图执行转换的 SKMatrix 参数:

public static SKShader CreateBitmap (SKBitmap src, SKShaderTileMode tmx, SKShaderTileMode tmy, SKMatrix localMatrix)

本文包含将此矩阵转换与平铺位图一起使用的几个示例。

浏览图块模式

示例的“着色器和其他效果”页的“位图平铺”部分中的第一个程序演示了两个 SKShaderTileMode 参数的效果。 位图图块翻转模式 XAML 文件实例化一个 SKCanvasView 和两个 Picker 视图,使你可以为水平平铺和垂直平铺选择 SKShaderTilerMode 值。 请注意,SKShaderTileMode 成员的数组在 Resources 节中定义:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:skia="clr-namespace:SkiaSharp;assembly=SkiaSharp"
             xmlns:skiaforms="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             x:Class="SkiaSharpFormsDemos.Effects.BitmapTileFlipModesPage"
             Title="Bitmap Tile Flip Modes">

    <ContentPage.Resources>
        <x:Array x:Key="tileModes"
                 Type="{x:Type skia:SKShaderTileMode}">
            <x:Static Member="skia:SKShaderTileMode.Clamp" />
            <x:Static Member="skia:SKShaderTileMode.Repeat" />
            <x:Static Member="skia:SKShaderTileMode.Mirror" />
        </x:Array>
    </ContentPage.Resources>

    <StackLayout>
        <skiaforms:SKCanvasView x:Name="canvasView"
                                VerticalOptions="FillAndExpand"
                                PaintSurface="OnCanvasViewPaintSurface" />

        <Picker x:Name="xModePicker"
                Title="Tile X Mode"
                Margin="10, 0"
                ItemsSource="{StaticResource tileModes}"
                SelectedIndex="0"
                SelectedIndexChanged="OnPickerSelectedIndexChanged" />

        <Picker x:Name="yModePicker"
                Title="Tile Y Mode"
                Margin="10, 10"
                ItemsSource="{StaticResource tileModes}"
                SelectedIndex="0"
                SelectedIndexChanged="OnPickerSelectedIndexChanged" />

    </StackLayout>
</ContentPage>

代码隐藏文件的构造函数在位图资源中加载,其中显示了一个猴子坐着。 它首先使用 SKBitmapExtractSubset 方法裁剪图像,以便头部和脚触摸位图的边缘。 然后,构造函数使用 Resize 方法创建另一个大小为一半的位图。 这些更改使位图更适合平铺:

public partial class BitmapTileFlipModesPage : ContentPage
{
    SKBitmap bitmap;

    public BitmapTileFlipModesPage ()
    {
        InitializeComponent ();

        SKBitmap origBitmap = BitmapExtensions.LoadBitmapResource(
            GetType(), "SkiaSharpFormsDemos.Media.SeatedMonkey.jpg");

        // Define cropping rect
        SKRectI cropRect = new SKRectI(5, 27, 296, 260);

        // Get the cropped bitmap
        SKBitmap croppedBitmap = new SKBitmap(cropRect.Width, cropRect.Height);
        origBitmap.ExtractSubset(croppedBitmap, cropRect);

        // Resize to half the width and height
        SKImageInfo info = new SKImageInfo(cropRect.Width / 2, cropRect.Height / 2);
        bitmap = croppedBitmap.Resize(info, SKBitmapResizeMethod.Box);
    }

    void OnPickerSelectedIndexChanged(object sender, EventArgs args)
    {
        canvasView.InvalidateSurface();
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        // Get tile modes from Pickers
        SKShaderTileMode xTileMode =
            (SKShaderTileMode)(xModePicker.SelectedIndex == -1 ?
                                        0 : xModePicker.SelectedItem);
        SKShaderTileMode yTileMode =
            (SKShaderTileMode)(yModePicker.SelectedIndex == -1 ?
                                        0 : yModePicker.SelectedItem);

        using (SKPaint paint = new SKPaint())
        {
            paint.Shader = SKShader.CreateBitmap(bitmap, xTileMode, yTileMode);
            canvas.DrawRect(info.Rect, paint);
        }
    }
}

PaintSurface 处理程序从两个 Picker 视图获取 SKShaderTileMode 设置,并根据位图和这两个值创建 SKShader 对象。 此着色器用于填充画布:

位图图块翻转模式

左侧的 iOS 屏幕显示 SKShaderTileMode.Clamp 的默认值的效果。 位图位于左上角。 在位图下方,像素的下行一直重复。 在位图右侧,最右侧的像素列一直重复。 画布的其余部分由位图右下角的深棕色像素着色。 很明显,Clamp 选项几乎永远不会用于位图平铺!

中心的 Android 屏幕显示两个参数的 SKShaderTileMode.Repeat 结果。 图块水平和垂直重复。 通用 Windows 平台屏幕显示 SKShaderTileMode.Mirror。 图块重复,但可横向和垂直翻转。 此选项的优点是图块之间没有不连续。

请记住,可以将不同的选项用于水平和垂直重复。 可以将 SKShaderTileMode.Mirror 指定为 CreateBitmap 的第二个参数,但 SKShaderTileMode.Repeat 为第三个参数。 每一行,猴子仍然在正常图像和镜子图像之间交替,但没有猴子是倒置的。

图案背景

位图平铺通常用于从相对较小的位图创建图案背景。 经典示例是砖墙。

“算法砖墙”页创建一个小位图,似于一整块砖和被灰泥隔开的两半砖。 由于此砖块也用于下一个示例,因此它由静态构造函数创建,并使用静态属性公开:

public class AlgorithmicBrickWallPage : ContentPage
{
    static AlgorithmicBrickWallPage()
    {
        const int brickWidth = 64;
        const int brickHeight = 24;
        const int morterThickness = 6;
        const int bitmapWidth = brickWidth + morterThickness;
        const int bitmapHeight = 2 * (brickHeight + morterThickness);

        SKBitmap bitmap = new SKBitmap(bitmapWidth, bitmapHeight);

        using (SKCanvas canvas = new SKCanvas(bitmap))
        using (SKPaint brickPaint = new SKPaint())
        {
            brickPaint.Color = new SKColor(0xB2, 0x22, 0x22);

            canvas.Clear(new SKColor(0xF0, 0xEA, 0xD6));
            canvas.DrawRect(new SKRect(morterThickness / 2,
                                       morterThickness / 2,
                                       morterThickness / 2 + brickWidth,
                                       morterThickness / 2 + brickHeight),
                                       brickPaint);

            int ySecondBrick = 3 * morterThickness / 2 + brickHeight;

            canvas.DrawRect(new SKRect(0,
                                       ySecondBrick,
                                       bitmapWidth / 2 - morterThickness / 2,
                                       ySecondBrick + brickHeight),
                                       brickPaint);

            canvas.DrawRect(new SKRect(bitmapWidth / 2 + morterThickness / 2,
                                       ySecondBrick,
                                       bitmapWidth,
                                       ySecondBrick + brickHeight),
                                       brickPaint);
        }

        // Save as public property for other programs
        BrickWallTile = bitmap;
    }

    public static SKBitmap BrickWallTile { private set; get; }
    ···
}

生成的位图宽 70 像素,高 60 像素:

算法砖墙图块

“算法砖墙”页的其余部分创建一个 SKShader 对象,该对象水平和垂直重复此图像:

public class AlgorithmicBrickWallPage : ContentPage
{
    ···
    public AlgorithmicBrickWallPage ()
    {
        Title = "Algorithmic Brick Wall";

        // Create SKCanvasView
        SKCanvasView canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        using (SKPaint paint = new SKPaint())
        {
            // Create bitmap tiling
            paint.Shader = SKShader.CreateBitmap(BrickWallTile,
                                                 SKShaderTileMode.Repeat,
                                                 SKShaderTileMode.Repeat);
            // Draw background
            canvas.DrawRect(info.Rect, paint);
        }
    }
}

结果如下:

算法砖墙

你可能更喜欢更现实的东西。 在这种情况下,你可以拍摄实际砖墙的照片,然后裁剪它。 此位图宽 300 像素,高 150 像素:

砖墙图块

此位图用于“摄影砖墙”页:

public class PhotographicBrickWallPage : ContentPage
{
    SKBitmap bitmap = BitmapExtensions.LoadBitmapResource(
                        typeof(PhotographicBrickWallPage),
                        "SkiaSharpFormsDemos.Media.BrickWallTile.jpg");

    public PhotographicBrickWallPage()
    {
        Title = "Photographic Brick Wall";

        SKCanvasView canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        using (SKPaint paint = new SKPaint())
        {
            // Create bitmap tiling
            paint.Shader = SKShader.CreateBitmap(bitmap,
                                                 SKShaderTileMode.Mirror,
                                                 SKShaderTileMode.Mirror);
            // Draw background
            canvas.DrawRect(info.Rect, paint);
        }
    }
}

请注意,CreateBitmapSKShaderTileMode 参数都是 Mirror。 使用从真实图像创建的图块时,通常需要使用此选项。 镜像图块可避免中断:

摄影砖墙

需要执行一些工作才能获取图块的合适位图。 这一块效果不好,因为更深的砖块太突出了。 它经常出现在重复的图像中,揭示了这个砖墙是从较小的位图构造的。

示例的 Media 文件夹还包括石墙的此图像:

石墙图块

但是,原始位图对于图块来说稍大一点。 它可以调整大小,但 SKShader.CreateBitmap 方法也可以通过向其应用转换来调整图块的大小。 “石墙”页中演示了此选项:

public class StoneWallPage : ContentPage
{
    SKBitmap bitmap = BitmapExtensions.LoadBitmapResource(
                        typeof(StoneWallPage),
                        "SkiaSharpFormsDemos.Media.StoneWallTile.jpg");

    public StoneWallPage()
    {
        Title = "Stone Wall";

        SKCanvasView canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        using (SKPaint paint = new SKPaint())
        {
            // Create scale transform
            SKMatrix matrix = SKMatrix.MakeScale(0.5f, 0.5f);

            // Create bitmap tiling
            paint.Shader = SKShader.CreateBitmap(bitmap,
                                                 SKShaderTileMode.Mirror,
                                                 SKShaderTileMode.Mirror,
                                                 matrix);
            // Draw background
            canvas.DrawRect(info.Rect, paint);
        }
    }
}

创建 SKMatrix 值,将图像缩放为原始大小的一半:

石墙

转换是否对 CreateBitmap 方法中使用的原始位图进行操作? 或者,它是否会转换图块的结果数组?

回答此问题的一种简单方法是在转换过程中包括旋转:

SKMatrix matrix = SKMatrix.MakeScale(0.5f, 0.5f);
SKMatrix.PostConcat(ref matrix, SKMatrix.MakeRotationDegrees(15));

如果转换应用于单个图块,则应旋转图块的每个重复图像,结果将包含许多不连续。 但从此屏幕截图中很明显,图块的复合数组已转换:

石墙旋转

在“图块对齐”部分中,你将看到应用于着色器的转换转换示例。

该示例基于此 240 像素方形位图使用位图平铺模拟木纹背景:

木纹

这是一张木质地板的照片。 SKShaderTileMode.Mirror 选项允许它显示为更大的木材区域:

Cat Clock

图块对齐

到目前为止显示的所有示例都使用 SKShader.CreateBitmap 创建的着色器覆盖整个画布。 在大多数情况下,你将使用位图平铺来归档较小的区域,或者(更很少)用于填充粗线的内部。 下面是用于较小矩形的摄影砖墙图块:

图块对齐

你可能觉得这很好,也可能觉得不好。 也许你感到不安的是,平铺图案不以矩形左上角的全砖开头。 这是因为着色器与画布对齐,而不是它们装饰的图形对象。

修复很简单。 基于平移转换创建 SKMatrix 值。 该转换有效地将平铺图案移动到希望平铺左上角对齐的点。 此方法在“图块对齐”页中演示,该页创建了上面所示未对齐图块的图像:

public class TileAlignmentPage : ContentPage
{
    bool isAligned;

    public TileAlignmentPage()
    {
        Title = "Tile Alignment";

        SKCanvasView canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;

        // Add tap handler
        TapGestureRecognizer tap = new TapGestureRecognizer();
        tap.Tapped += (sender, args) =>
        {
            isAligned ^= true;
            canvasView.InvalidateSurface();
        };
        canvasView.GestureRecognizers.Add(tap);

        Content = canvasView;
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        using (SKPaint paint = new SKPaint())
        {
            SKRect rect = new SKRect(info.Width / 7,
                                     info.Height / 7,
                                     6 * info.Width / 7,
                                     6 * info.Height / 7);

            // Get bitmap from other program
            SKBitmap bitmap = AlgorithmicBrickWallPage.BrickWallTile;

            // Create bitmap tiling
            if (!isAligned)
            {
                paint.Shader = SKShader.CreateBitmap(bitmap,
                                                     SKShaderTileMode.Repeat,
                                                     SKShaderTileMode.Repeat);
            }
            else
            {
                SKMatrix matrix = SKMatrix.MakeTranslation(rect.Left, rect.Top);

                paint.Shader = SKShader.CreateBitmap(bitmap,
                                                     SKShaderTileMode.Repeat,
                                                     SKShaderTileMode.Repeat,
                                                     matrix);
            }

            // Draw rectangle
            canvas.DrawRect(rect, paint);
        }
    }
}

“图块对齐”页包含一个 TapGestureRecognizer。 点击或单击屏幕,程序使用 SKMatrix 参数切换到 SKShader.CreateBitmap 方法。 此转换将移动模式,使左上角包含一个完整的砖块:

点击图块对齐

还可以使用此技术确保平铺位图图案在其绘制区域内居中。 在“居中图块”页中,PaintSurface 处理程序首先计算坐标,就好像要在画布中心显示单个位图一样。 然后,它使用这些坐标为 SKShader.CreateBitmap 创建平移转换。 此转换会移动整个模式,使图块居中:

public class CenteredTilesPage : ContentPage
{
    SKBitmap bitmap = BitmapExtensions.LoadBitmapResource(
                        typeof(CenteredTilesPage),
                        "SkiaSharpFormsDemos.Media.monkey.png");

    public CenteredTilesPage ()
    {
        Title = "Centered Tiles";

        SKCanvasView canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        // Find coordinates to center bitmap in canvas...
        float x = (info.Width - bitmap.Width) / 2f;
        float y = (info.Height - bitmap.Height) / 2f;

        using (SKPaint paint = new SKPaint())
        {
            // ... but use them to create a translate transform
            SKMatrix matrix = SKMatrix.MakeTranslation(x, y);
            paint.Shader = SKShader.CreateBitmap(bitmap,
                                                 SKShaderTileMode.Repeat,
                                                 SKShaderTileMode.Repeat,
                                                 matrix);

            // Use that tiled bitmap pattern to fill a circle
            canvas.DrawCircle(info.Rect.MidX, info.Rect.MidY,
                              Math.Min(info.Width, info.Height) / 2,
                              paint);
        }
    }
}

PaintSurface 处理程序通过在画布中心绘制一个圆来结束。 果然,其中一个图块正好位于圆中心,其他图块采用对称模式排列:

居中图块

另一种居中方法实际上要容易一点。 你可以将平铺图案的角居中,而不是构建平铺转换。 在 SKMatrix.MakeTranslation 调用中,对画布中心使用参数:

SKMatrix matrix = SKMatrix.MakeTranslation(info.Rect.MidX, info.Rect.MidY);

模式仍居中且对称,但中心中没有图块:

居中图块备用

通过旋转简化

有时在 SKShader.CreateBitmap 方法中使用旋转转换可以简化位图图块。 当尝试为链链接围栏定义图块时,这变得很明显。 ChainLinkTile.cs 文件创建此处显示的图块(为了清楚起见,背景为粉红色):

硬链状图块

图块需要包含两个链接,以便代码将图块划分为四个象限。 左上角和右下象限相同,但它们不完整。 电线上有一些小凹槽,必须在右上角和左下角的象限内再绘制一些。 执行所有这些工作的文件长度为 174 行。

事实证明,创建此图块要容易得多:

更简易的链状图块

如果位图平铺着色器旋转 90 度,则视觉对象几乎相同。

创建更简单的链环图块的代码是“链环图块”页的一部分。 构造函数根据程序正在运行的设备类型确定图块大小,然后调用 CreateChainLinkTile,该类型使用线条、路径和渐变着色器在位图上绘制:

public class ChainLinkFencePage : ContentPage
{
    ···
    SKBitmap tileBitmap;

    public ChainLinkFencePage ()
    {
        Title = "Chain-Link Fence";

        // Create bitmap for chain-link tiling
        int tileSize = Device.Idiom == TargetIdiom.Desktop ? 64 : 128;
        tileBitmap = CreateChainLinkTile(tileSize);

        SKCanvasView canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;
    }

    SKBitmap CreateChainLinkTile(int tileSize)
    {
        tileBitmap = new SKBitmap(tileSize, tileSize);
        float wireThickness = tileSize / 12f;

        using (SKCanvas canvas = new SKCanvas(tileBitmap))
        using (SKPaint paint = new SKPaint())
        {
            canvas.Clear();
            paint.Style = SKPaintStyle.Stroke;
            paint.StrokeWidth = wireThickness;
            paint.IsAntialias = true;

            // Draw straight wires first
            paint.Shader = SKShader.CreateLinearGradient(new SKPoint(0, 0),
                                                         new SKPoint(0, tileSize),
                                                         new SKColor[] { SKColors.Silver, SKColors.Black },
                                                         new float[] { 0.4f, 0.6f },
                                                         SKShaderTileMode.Clamp);

            canvas.DrawLine(0, tileSize / 2,
                            tileSize / 2, tileSize / 2 - wireThickness / 2, paint);

            canvas.DrawLine(tileSize, tileSize / 2,
                            tileSize / 2, tileSize / 2 + wireThickness / 2, paint);

            // Draw curved wires
            using (SKPath path = new SKPath())
            {
                path.MoveTo(tileSize / 2, 0);
                path.LineTo(tileSize / 2 - wireThickness / 2, tileSize / 2);
                path.ArcTo(wireThickness / 2, wireThickness / 2,
                           0,
                           SKPathArcSize.Small,
                           SKPathDirection.CounterClockwise,
                           tileSize / 2, tileSize / 2 + wireThickness / 2);

                paint.Shader = SKShader.CreateLinearGradient(new SKPoint(0, 0),
                                                             new SKPoint(0, tileSize),
                                                             new SKColor[] { SKColors.Silver, SKColors.Black },
                                                             null,
                                                             SKShaderTileMode.Clamp);
                canvas.DrawPath(path, paint);

                path.Reset();
                path.MoveTo(tileSize / 2, tileSize);
                path.LineTo(tileSize / 2 + wireThickness / 2, tileSize / 2);
                path.ArcTo(wireThickness / 2, wireThickness / 2,
                           0,
                           SKPathArcSize.Small,
                           SKPathDirection.CounterClockwise,
                           tileSize / 2, tileSize / 2 - wireThickness / 2);

                paint.Shader = SKShader.CreateLinearGradient(new SKPoint(0, 0),
                                                             new SKPoint(0, tileSize),
                                                             new SKColor[] { SKColors.White, SKColors.Silver },
                                                             null,
                                                             SKShaderTileMode.Clamp);
                canvas.DrawPath(path, paint);
            }
            return tileBitmap;
        }
    }
    ···
}

除了电线外,图块是透明的,这意味着你可以在其他内容上显示它。 程序加载其中一个位图资源,显示它以填充画布,然后在顶部绘制着色器:

public class ChainLinkFencePage : ContentPage
{
    SKBitmap monkeyBitmap = BitmapExtensions.LoadBitmapResource(
        typeof(ChainLinkFencePage), "SkiaSharpFormsDemos.Media.SeatedMonkey.jpg");
    ···

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        canvas.DrawBitmap(monkeyBitmap, info.Rect, BitmapStretch.UniformToFill,
                            BitmapAlignment.Center, BitmapAlignment.Start);

        using (SKPaint paint = new SKPaint())
        {
            paint.Shader = SKShader.CreateBitmap(tileBitmap,
                                                 SKShaderTileMode.Repeat,
                                                 SKShaderTileMode.Repeat,
                                                 SKMatrix.MakeRotationDegrees(45));
            canvas.DrawRect(info.Rect, paint);
        }
    }
}

请注意,着色器旋转 45 度,因此其方向类似于真正的链环围栏:

链状围栏

对位图图块进行动画处理

可以通过对矩阵转换进行动画处理,对整个位图图块模式进行动画处理。 也许你希望模式水平或垂直移动或同时移动。 为此,可以根据移动坐标创建一个平移变换。

还可以在小位图上绘制,或者以每秒 60 次的速度操作位图的像素位。 然后,该位图可用于平铺,整个平铺模式似乎可以进行动画处理。

“动画位图图块”页演示了此方法。 位图将实例化为 64 像素正方形的字段。 构造函数调用 DrawBitmap,使其具有初始外观。 如果 angle 字段为零(就像首次调用方法时一样),则位图包含两行作为 X 交叉。无论 angle 值如何,行都足够长,始终到达位图边缘:

public class AnimatedBitmapTilePage : ContentPage
{
    const int SIZE = 64;

    SKCanvasView canvasView;
    SKBitmap bitmap = new SKBitmap(SIZE, SIZE);
    float angle;
    ···

    public AnimatedBitmapTilePage ()
    {
        Title = "Animated Bitmap Tile";

        // Initialize bitmap prior to animation
        DrawBitmap();

        // Create SKCanvasView
        canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;
    }
    ···
    void DrawBitmap()
    {
        using (SKCanvas canvas = new SKCanvas(bitmap))
        using (SKPaint paint = new SKPaint())
        {
            paint.Style = SKPaintStyle.Stroke;
            paint.Color = SKColors.Blue;
            paint.StrokeWidth = SIZE / 8;

            canvas.Clear();
            canvas.Translate(SIZE / 2, SIZE / 2);
            canvas.RotateDegrees(angle);
            canvas.DrawLine(-SIZE, -SIZE, SIZE, SIZE, paint);
            canvas.DrawLine(-SIZE, SIZE, SIZE, -SIZE, paint);
        }
    }
    ···
}

动画开销发生在 OnAppearing 中,且 OnDisappearing 会替代。 OnTimerTick 方法每 10 秒将 angle 值从 0 度设置为 360 度,以旋转位图中的 X 图:

public class AnimatedBitmapTilePage : ContentPage
{
    ···
    // For animation
    bool isAnimating;
    Stopwatch stopwatch = new Stopwatch();
    ···

    protected override void OnAppearing()
    {
        base.OnAppearing();

        isAnimating = true;
        stopwatch.Start();
        Device.StartTimer(TimeSpan.FromMilliseconds(16), OnTimerTick);
    }

    protected override void OnDisappearing()
    {
        base.OnDisappearing();

        stopwatch.Stop();
        isAnimating = false;
    }

    bool OnTimerTick()
    {
        const int duration = 10;     // seconds
        angle = (float)(360f * (stopwatch.Elapsed.TotalSeconds % duration) / duration);
        DrawBitmap();
        canvasView.InvalidateSurface();

        return isAnimating;
    }
    ···
}

由于 X 图的对称性,这与每 2.5 秒将 angle 值从 0 度旋转到 90 度相同。

PaintSurface 处理程序从位图创建着色器,并使用画图对象为整个画布着色:

public class AnimatedBitmapTilePage : ContentPage
{
    ···
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        using (SKPaint paint = new SKPaint())
        {
            paint.Shader = SKShader.CreateBitmap(bitmap,
                                                 SKShaderTileMode.Mirror,
                                                 SKShaderTileMode.Mirror);
            canvas.DrawRect(info.Rect, paint);
        }
    }
}

SKShaderTileMode.Mirror 选项可确保每个位图中的 X 的臂与相邻位图中的 X 联接,以创建看起来比简单动画更复杂的整体动画模式:

动画位图图块