SkiaSharp 中的位图基础知识

Download Sample下载示例

从各种源加载位图并显示它们。

SkiaSharp 中位图的支持相当广泛。 本文仅介绍基础知识 - 如何加载位图以及如何显示位图:

The display of two bitmaps

可以在 SkiaSharp 位图部分找到对位图的更深入的探索。

SkiaSharp 位图是 SKBitmap 类型的对象。 有许多方法可以创建位图,但本文将自身限制为 SKBitmap.Decode 方法,该方法从 .NET Stream 对象加载位图。

SkiaSharpFormsDemos 程序中基本位图页演示了如何从三个不同的源加载位图:

  • 从互联网
  • 从嵌入在可执行文件中的资源
  • 从用户的照片库

这三个源的三个 SKBitmap 对象定义为 BasicBitmapsPage 类中的字段:

public class BasicBitmapsPage : ContentPage
{
    SKCanvasView canvasView;
    SKBitmap webBitmap;
    SKBitmap resourceBitmap;
    SKBitmap libraryBitmap;

    public BasicBitmapsPage()
    {
        Title = "Basic Bitmaps";

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

从 Web 加载位图

若要基于 URL 加载位图,可以使用 HttpClient 类。 应仅实例化 HttpClient 的一个实例并重复使用它,从而将其存储为字段:

HttpClient httpClient = new HttpClient();

在 iOS 和 Android 应用程序中使用 HttpClient 时,需要按照传输层安全性 (TLS) 1.2 文档所述设置项目属性。

由于搭配 HttpClient 使用 await 运算符最方便,因此无法在 BasicBitmapsPage 构造函数中执行代码。 它其实是 OnAppearing 替代的一部分。 此处的 URL 指向包含一些示例位图的 Xamarin 网站上的区域。 网站上的包允许将位图大小调整为特定宽度的规范:

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

    // Load web bitmap.
    string url = "https://developer.xamarin.com/demo/IMG_3256.JPG?width=480";

    try
    {
        using (Stream stream = await httpClient.GetStreamAsync(url))
        using (MemoryStream memStream = new MemoryStream())
        {
            await stream.CopyToAsync(memStream);
            memStream.Seek(0, SeekOrigin.Begin);

            webBitmap = SKBitmap.Decode(memStream);
            canvasView.InvalidateSurface();
        };
    }
    catch
    {
    }
}

Android 操作系统在使用从 SKBitmap.Decode 方法中的 GetStreamAsync 返回的 Stream 时引发异常,因为它在主线程上执行较长的操作。 因此,位图文件的内容将使用 CopyToAsync 复制到 MemoryStream 对象。

静态 SKBitmap.Decode 方法负责解码位图文件。 它适用于 JPEG、PNG 和 GIF 位图格式,并将结果存储为内部 SkiaSharp 格式。 此时,需要使 SKCanvasView 失效,以允许 PaintSurface 处理程序更新显示。

加载位图资源

在代码方面,加载位图的最简单方法是直接在应用程序中包括位图资源。 SkiaSharpFormsDemos 程序包含一个名为 媒体 的文件夹,其中包含多个位图文件,包括一个名为 monkey.png 的文件。 对于存储为程序资源的位图,必须使用属性对话框为文件提供嵌入资源生成操作

每个嵌入资源都有一个资源 ID,其中包含项目名称、文件夹和文件名,全部以句点连接:SkiaSharpFormsDemos.Media.monkey.png。 可以通过将资源 ID 指定为 Assembly 类的 GetManifestResourceStream 方法的参数来访问此资源:

string resourceID = "SkiaSharpFormsDemos.Media.monkey.png";
Assembly assembly = GetType().GetTypeInfo().Assembly;

using (Stream stream = assembly.GetManifestResourceStream(resourceID))
{
    resourceBitmap = SKBitmap.Decode(stream);
}

可以将此 Stream 对象直接传递给 SKBitmap.Decode 方法。

从照片库加载位图

用户还可以从设备的图片库加载照片。 Xamarin.Forms 本身不提供此设施。 该作业需要依赖项服务,如文章从图片库选取照片中所述的服务。

SkiaSharpFormsDemos 项目中的 IPhotoLibrary.cs 文件以及平台项目中的三个 PhotoLibrary.cs 文件已从该文章改编。 此外,Android MainActivity.cs 文件已按照文章中所述进行了修改,并且 iOS 项目已获得访问照片库的权限,该库的底部有两行指向 info.plist 文件。

BasicBitmapsPage 构造函数向 SKCanvasView 添加一个 TapGestureRecognizer,以便收到点击通知。 在收到点击后,Tapped 处理程序可以访问图片选取器依赖项服务和调用 PickPhotoAsync。 如果返回 Stream 对象,则会将其传递给 SKBitmap.Decode 方法:

// Add tap gesture recognizer
TapGestureRecognizer tapRecognizer = new TapGestureRecognizer();
tapRecognizer.Tapped += async (sender, args) =>
{
    // Load bitmap from photo library
    IPhotoLibrary photoLibrary = DependencyService.Get<IPhotoLibrary>();

    using (Stream stream = await photoLibrary.PickPhotoAsync())
    {
        if (stream != null)
        {
            libraryBitmap = SKBitmap.Decode(stream);
            canvasView.InvalidateSurface();
        }
    }
};
canvasView.GestureRecognizers.Add(tapRecognizer);

请注意,Tapped 处理程序还调用 SKCanvasView 对象的 InvalidateSurface 方法。 这会生成对 PaintSurface 处理程序的新调用。

显示位图

PaintSurface 处理程序需要显示三个位图。 处理程序假定手机处于纵向模式,并将画布垂直划分为三个相等部分。

第一个位图以最简单的 DrawBitmap 方法显示。 只需指定位图左上角的 X 和 Y 坐标即可定位:

public void DrawBitmap (SKBitmap bitmap, Single x, Single y, SKPaint paint = null)

虽然定义了 SKPaint 参数,但它的默认值为 null,你可以忽略它。 位图的像素只是使用一对一映射传输到显示图面的像素。 在下一节中,你将看到有关 SkiaSharp Transparency 的此 SKPaint 参数的应用程序。

程序可以使用 WidthHeight 属性获取位图的像素尺寸。 这些属性允许程序计算坐标以将位图定位在画布上三分之一的中心:

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

    canvas.Clear();

    if (webBitmap != null)
    {
        float x = (info.Width - webBitmap.Width) / 2;
        float y = (info.Height / 3 - webBitmap.Height) / 2;
        canvas.DrawBitmap(webBitmap, x, y);
    }
    ...
}

其他两个位图以具有 SKRect 参数的 DrawBitmap 版本显示:

public void DrawBitmap (SKBitmap bitmap, SKRect dest, SKPaint paint = null)

第三个版本的 DrawBitmap 有两个 SKRect 参数,用于指定要显示的位图的矩形子集,但本文未使用该版本。

下面是用于显示从嵌入资源位图加载的位图的代码:

void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    ...
    if (resourceBitmap != null)
    {
        canvas.DrawBitmap(resourceBitmap,
            new SKRect(0, info.Height / 3, info.Width, 2 * info.Height / 3));
    }
    ...
}

位图拉伸到矩形的尺寸,这就是为什么猴子在这些屏幕截图中水平拉伸的原因:

A triple screenshot of the Basic Bitmaps page

仅当运行程序并从自己的图片库中—加载照片时,才会显示第三张图像,但该矩形的位置和大小会进行调整,以保持位图的纵横比。 此过程需要多一些计算,因为它需要根据位图和目标矩形的大小计算缩放因子,并将矩形居中位于该区域:

void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    ...
    if (libraryBitmap != null)
    {
        float scale = Math.Min((float)info.Width / libraryBitmap.Width,
                               info.Height / 3f / libraryBitmap.Height);

        float left = (info.Width - scale * libraryBitmap.Width) / 2;
        float top = (info.Height / 3 - scale * libraryBitmap.Height) / 2;
        float right = left + scale * libraryBitmap.Width;
        float bottom = top + scale * libraryBitmap.Height;
        SKRect rect = new SKRect(left, top, right, bottom);
        rect.Offset(0, 2 * info.Height / 3);

        canvas.DrawBitmap(libraryBitmap, rect);
    }
    else
    {
        using (SKPaint paint = new SKPaint())
        {
            paint.Color = SKColors.Blue;
            paint.TextAlign = SKTextAlign.Center;
            paint.TextSize = 48;

            canvas.DrawText("Tap to load bitmap",
                info.Width / 2, 5 * info.Height / 6, paint);
        }
    }
}

如果尚未从图片库中加载位图,则 else 块将显示一些文本,提示用户点击屏幕。

可以显示具有不同透明度的位图,下一篇文章介绍了 SkiaSharp 透明度