Share via


將 SkiaSharp 位圖儲存至檔案

在 SkiaSharp 應用程式建立或修改點陣圖之後,應用程式可能會想要將點陣圖儲存到使用者的照片庫:

儲存點圖

此工作包含兩個步驟:

  • 將 SkiaSharp 位圖轉換成特定檔案格式的數據,例如 JPEG 或 PNG。
  • 使用平臺特定程式代碼將結果儲存至相片媒體櫃。

檔案格式和編解碼器

現今大部分熱門的點陣圖檔格式都會使用壓縮來減少儲存空間。 壓縮技術的兩大類別稱為 遺失損失。 這些詞彙會指出壓縮演算法是否會導致數據遺失。

最受歡迎的損失格式是由聯合攝影專家組開發的,被稱為JPEG。 JPEG 壓縮演算法會使用稱為離散餘弦轉換數學工具來分析影像,並嘗試移除對保留影像視覺逼真度而言不重要的數據。 壓縮的程度可以透過通常稱為 質量的設定來控制。 品質較高的設定會導致較大的檔案。

相反地,無遺失壓縮演算法會分析影像,以重複和圖元模式進行編碼,以降低數據的方式編碼,但不會導致任何資訊遺失。 原始點圖數據可以完全從壓縮檔還原。 目前使用的主要無遺失壓縮檔格式是可攜式網路圖形 (PNG)。

一般而言,JPEG 用於相片,而 PNG 則用於手動或演算法產生的影像。 任何減少某些檔案大小的無遺失壓縮演算法,都必須增加其他檔案的大小。 幸運的是,此大小增加通常只會針對包含大量隨機(或看似隨機)信息的數據發生。

壓縮演算法相當複雜,足以保證描述壓縮和解壓縮程式的兩個詞彙:

  • 譯碼 - 讀取位圖檔格式並解壓縮
  • encode - 壓縮點陣圖並寫入點陣圖檔案格式

類別 SKBitmap 包含數個從 Decode 壓縮來源建立 SKBitmap 的方法。 只需要提供檔名、數據流或位元組陣列。 譯碼器可以判斷檔格式,並將它交給適當的內部譯碼函式。

此外,類別 SKCodec 有兩個名為 Create 的方法,可從壓縮的來源建立 SKCodec 物件,並允許應用程式更參與譯碼程式。 (類別 SKCodec 會顯示在 動畫顯示 SkiaSharp 位圖 與譯碼動畫 GIF 檔案有關的文章中。

編碼點陣圖時,需要更多資訊:編碼器必須知道應用程式想要使用的特定檔格式(JPEG 或 PNG 或其他專案)。 如果需要遺失格式,編碼也必須知道所需的品質層級。

類別 SKBitmap 會使用下列語法定義一個 Encode 方法:

public Boolean Encode (SKWStream dst, SKEncodedImageFormat format, Int32 quality)

這個方法稍後會更詳細地說明。 編碼的點陣圖會寫入可寫入數據流。 (中的'W' SKWStream 代表 “可寫入” 。第二個和第三個自變數會指定檔格式和 (若為遺失格式),所需的品質範圍從 0 到 100。

此外, SKImageSKPixmap 類別也會定義 Encode 稍微多用途的方法,以及您可能偏好的方法。 您可以使用靜態SKImage.FromBitmap方法,輕鬆地從 SKBitmap 物件建立SKImage物件。 您可以使用 方法,從 SKBitmap 物件取得 SKPixmap 物件PeekPixels

SKImage定義的其中Encode一個方法沒有參數,而且會自動儲存為 PNG 格式。 這個無參數方法很容易使用。

儲存點圖檔案的平臺特定程序代碼

當您將 SKBitmap 物件編碼為特定檔案格式時,通常會保留某種類型的數據流物件或數據陣列。 某些 Encode 方法(包括未定義 SKImage任何參數的 方法)會傳回 SKData 物件,這個物件可以使用 方法轉換成位元組 ToArray 數位數組。 然後,此數據必須儲存至檔案。

儲存至應用程式本機記憶體中的檔案相當容易,因為您可以針對這項工作使用標準 System.IO 類別和方法。 這項技術示範於動畫顯示SkiaSharp位圖一中,以動畫顯示Mandelbrot集合的一系列點陣圖。

如果您想要讓檔案由其他應用程式共用,它必須儲存到使用者的相片媒體櫃。 這個工作需要平臺特定的程式代碼,並使用 Xamarin.FormsDependencyService

範例應用程式中的 SkiaSharpFormsDemo 專案會IPhotoLibrary定義與 類別搭配DependencyService使用的介面。 這會定義方法的 SavePhotoAsync 語法:

public interface IPhotoLibrary
{
    Task<Stream> PickPhotoAsync();

    Task<bool> SavePhotoAsync(byte[] data, string folder, string filename);
}

這個介面也會定義 PickPhotoAsync 方法,用來開啟裝置相片媒體櫃的平臺特定檔案選擇器。

對於 SavePhotoAsync,第一個自變數是位元組數位,其中包含已編碼為特定檔格式的點陣圖,例如 JPEG 或 PNG。 應用程式可能會想要將它建立的所有位圖隔離到特定資料夾中,該資料夾會在下一個參數中指定,後面接著檔名。 方法會傳回布爾值,指出成功或否。

下列各節將討論如何在 SavePhotoAsync 每個平台上實作。

iOS 實作

SavePhotoAsync iOS 實作會使用 SaveToPhotosAlbumUIImage方法:

public class PhotoLibrary : IPhotoLibrary
{
    ···
    public Task<bool> SavePhotoAsync(byte[] data, string folder, string filename)
    {
        NSData nsData = NSData.FromArray(data);
        UIImage image = new UIImage(nsData);
        TaskCompletionSource<bool> taskCompletionSource = new TaskCompletionSource<bool>();

        image.SaveToPhotosAlbum((UIImage img, NSError error) =>
        {
            taskCompletionSource.SetResult(error == null);
        });

        return taskCompletionSource.Task;
    }
}

不幸的是,無法指定映像的檔名或資料夾。

iOS 專案中的 Info.plist 檔案需要一個索引鍵,指出它會將影像新增至相片庫:

<key>NSPhotoLibraryAddUsageDescription</key>
<string>SkiaSharp Forms Demos adds images to your photo library</string>

小心! 直接存取相片媒體櫃的許可權密鑰非常類似,但不同:

<key>NSPhotoLibraryUsageDescription</key>
<string>SkiaSharp Forms Demos accesses your photo library</string>

Android 實作

的Android實作 SavePhotoAsync 會先檢查自變數是否 foldernull 或空字串。 如果是,則點陣圖會儲存在相片媒體櫃的根目錄中。 否則,會取得資料夾,如果資料夾不存在,則會建立它:

public class PhotoLibrary : IPhotoLibrary
{
    ···
    public async Task<bool> SavePhotoAsync(byte[] data, string folder, string filename)
    {
        try
        {
            File picturesDirectory = Environment.GetExternalStoragePublicDirectory(Environment.DirectoryPictures);
            File folderDirectory = picturesDirectory;

            if (!string.IsNullOrEmpty(folder))
            {
                folderDirectory = new File(picturesDirectory, folder);
                folderDirectory.Mkdirs();
            }

            using (File bitmapFile = new File(folderDirectory, filename))
            {
                bitmapFile.CreateNewFile();

                using (FileOutputStream outputStream = new FileOutputStream(bitmapFile))
                {
                    await outputStream.WriteAsync(data);
                }

                // Make sure it shows up in the Photos gallery promptly.
                MediaScannerConnection.ScanFile(MainActivity.Instance,
                                                new string[] { bitmapFile.Path },
                                                new string[] { "image/png", "image/jpeg" }, null);
            }
        }
        catch
        {
            return false;
        }

        return true;
    }
}

MediaScannerConnection.ScanFile呼叫 並非絕對必要,但如果您要立即檢查相片媒體櫃來測試程式,則藉由更新文檔庫庫檢視來協助很多。

AndroidManifest.xml檔案需要下列許可權標籤:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

UWP 實作

的UWP實 SavePhotoAsync 作在結構中與Android實作非常類似:

public class PhotoLibrary : IPhotoLibrary
{
    ···
    public async Task<bool> SavePhotoAsync(byte[] data, string folder, string filename)
    {
        StorageFolder picturesDirectory = KnownFolders.PicturesLibrary;
        StorageFolder folderDirectory = picturesDirectory;

        // Get the folder or create it if necessary
        if (!string.IsNullOrEmpty(folder))
        {
            try
            {
                folderDirectory = await picturesDirectory.GetFolderAsync(folder);
            }
            catch
            { }

            if (folderDirectory == null)
            {
                try
                {
                    folderDirectory = await picturesDirectory.CreateFolderAsync(folder);
                }
                catch
                {
                    return false;
                }
            }
        }

        try
        {
            // Create the file.
            StorageFile storageFile = await folderDirectory.CreateFileAsync(filename,
                                                CreationCollisionOption.GenerateUniqueName);

            // Convert byte[] to Windows buffer and write it out.
            IBuffer buffer = WindowsRuntimeBuffer.Create(data, 0, data.Length, data.Length);
            await FileIO.WriteBufferAsync(storageFile, buffer);
        }
        catch
        {
            return false;
        }

        return true;
    }
}

Package.appxmanifest 檔案的 [功能] 區段需要圖片庫

探索影像格式

以下是 Encode 的再次方法 SKImage

public Boolean Encode (SKWStream dst, SKEncodedImageFormat format, Int32 quality)

SKEncodedImageFormat 是列舉,其中包含參考十一個點陣圖檔格式的成員,其中有些相當模糊:

  • Astc — 調適性可調整紋理壓縮
  • Bmp — Windows 位圖
  • Dng — Adobe Digital Negative
  • Gif — 圖形交換格式
  • Ico — Windows 圖示影像
  • Jpeg — 聯合攝影專家組
  • Ktx — OpenGL 的 Khronos 紋理格式
  • Pkm — GrafX2 的自訂格式
  • Png — 可攜式網路圖形
  • Wbmp — 無線應用程式通訊協定位陣圖格式(每像素 1 位)
  • Webp — Google WebP 格式

如您很快就會看到,SkiaSharp 實際上只支援這三種檔案格式 (JpegPngWebp) 。

若要將名為 的物件儲存SKBitmap到使用者相片庫,您也需要名為 imageFormat 和的列舉成員SKEncodedImageFormat(若為遺失格式)整數qualitybitmap變數。 您可以使用下列程式代碼,將該點陣圖儲存到資料夾中名稱 filename 為的 folder 檔案:

using (MemoryStream memStream = new MemoryStream())
using (SKManagedWStream wstream = new SKManagedWStream(memStream))
{
    bitmap.Encode(wstream, imageFormat, quality);
    byte[] data = memStream.ToArray();

    // Check the data array for content!

    bool success = await DependencyService.Get<IPhotoLibrary>().SavePhotoAsync(data, folder, filename);

    // Check return value for success!
}

類別 SKManagedWStream 衍生自 SKWStream (代表「可寫入數據流」)。 方法 Encode 會將編碼的位圖檔案寫入該數據流。 該程式代碼中的批註是指您可能需要執行的一些錯誤檢查。

範例應用程式中的 [ 儲存檔案格式 ] 頁面會使用類似的程式代碼,讓您實驗以各種格式儲存點陣圖。

XAML 檔案包含 SKCanvasView 顯示點陣圖的 ,而頁面的其餘部分則包含應用程式呼叫 方法SKBitmap所需的所有Encode專案。 它具有 Picker 列舉成員的 ,適用於遺失點圖格式的品質 SKEncodedImageFormat 自變數、 Slider 檔名和資料夾名稱的兩 Entry 個檢視,以及 Button 用於儲存盤案的 。

<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.Bitmaps.SaveFileFormatsPage"
             Title="Save Bitmap Formats">

    <StackLayout Margin="10">
        <skiaforms:SKCanvasView PaintSurface="OnCanvasViewPaintSurface"
                                VerticalOptions="FillAndExpand" />

        <Picker x:Name="formatPicker"
                Title="image format"
                SelectedIndexChanged="OnFormatPickerChanged">
            <Picker.ItemsSource>
                <x:Array Type="{x:Type skia:SKEncodedImageFormat}">
                    <x:Static Member="skia:SKEncodedImageFormat.Astc" />
                    <x:Static Member="skia:SKEncodedImageFormat.Bmp" />
                    <x:Static Member="skia:SKEncodedImageFormat.Dng" />
                    <x:Static Member="skia:SKEncodedImageFormat.Gif" />
                    <x:Static Member="skia:SKEncodedImageFormat.Ico" />
                    <x:Static Member="skia:SKEncodedImageFormat.Jpeg" />
                    <x:Static Member="skia:SKEncodedImageFormat.Ktx" />
                    <x:Static Member="skia:SKEncodedImageFormat.Pkm" />
                    <x:Static Member="skia:SKEncodedImageFormat.Png" />
                    <x:Static Member="skia:SKEncodedImageFormat.Wbmp" />
                    <x:Static Member="skia:SKEncodedImageFormat.Webp" />
                </x:Array>
            </Picker.ItemsSource>
        </Picker>

        <Slider x:Name="qualitySlider"
                Maximum="100"
                Value="50" />

        <Label Text="{Binding Source={x:Reference qualitySlider},
                              Path=Value,
                              StringFormat='Quality = {0:F0}'}"
               HorizontalTextAlignment="Center" />

        <StackLayout Orientation="Horizontal">
            <Label Text="Folder Name: "
                   VerticalOptions="Center" />

            <Entry x:Name="folderNameEntry"
                   Text="SaveFileFormats"
                   HorizontalOptions="FillAndExpand" />
        </StackLayout>

        <StackLayout Orientation="Horizontal">
            <Label Text="File Name: "
                   VerticalOptions="Center" />

            <Entry x:Name="fileNameEntry"
                   Text="Sample.xxx"
                   HorizontalOptions="FillAndExpand" />
        </StackLayout>

        <Button Text="Save"
                Clicked="OnButtonClicked">
            <Button.Triggers>
                <DataTrigger TargetType="Button"
                             Binding="{Binding Source={x:Reference formatPicker},
                                               Path=SelectedIndex}"
                             Value="-1">
                    <Setter Property="IsEnabled" Value="False" />
                </DataTrigger>

                <DataTrigger TargetType="Button"
                             Binding="{Binding Source={x:Reference fileNameEntry},
                                               Path=Text.Length}"
                             Value="0">
                    <Setter Property="IsEnabled" Value="False" />
                </DataTrigger>
            </Button.Triggers>
        </Button>

        <Label x:Name="statusLabel"
               Text="OK"
               Margin="10, 0" />
    </StackLayout>
</ContentPage>

程序代碼後置檔案會載入點陣圖資源,並使用 SKCanvasView 來顯示它。 該位圖永遠不會變更。 處理 SelectedIndexChanged 程式 Picker 會使用與列舉成員相同的擴展名來修改檔名:

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

    public SaveFileFormatsPage ()
    {
        InitializeComponent ();
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        args.Surface.Canvas.DrawBitmap(bitmap, args.Info.Rect, BitmapStretch.Uniform);
    }

    void OnFormatPickerChanged(object sender, EventArgs args)
    {
        if (formatPicker.SelectedIndex != -1)
        {
            SKEncodedImageFormat imageFormat = (SKEncodedImageFormat)formatPicker.SelectedItem;
            fileNameEntry.Text = Path.ChangeExtension(fileNameEntry.Text, imageFormat.ToString());
            statusLabel.Text = "OK";
        }
    }

    async void OnButtonClicked(object sender, EventArgs args)
    {
        SKEncodedImageFormat imageFormat = (SKEncodedImageFormat)formatPicker.SelectedItem;
        int quality = (int)qualitySlider.Value;

        using (MemoryStream memStream = new MemoryStream())
        using (SKManagedWStream wstream = new SKManagedWStream(memStream))
        {
            bitmap.Encode(wstream, imageFormat, quality);
            byte[] data = memStream.ToArray();

            if (data == null)
            {
                statusLabel.Text = "Encode returned null";
            }
            else if (data.Length == 0)
            {
                statusLabel.Text = "Encode returned empty array";
            }
            else
            {
                bool success = await DependencyService.Get<IPhotoLibrary>().
                    SavePhotoAsync(data, folderNameEntry.Text, fileNameEntry.Text);

                if (!success)
                {
                    statusLabel.Text = "SavePhotoAsync return false";
                }
                else
                {
                    statusLabel.Text = "Success!";
                }
            }
        }
    }
}

Clicked 處理程式 Button 會執行所有實際工作。 它會從 Picker 與取得的兩個自變數Encode,然後使用稍早所示的程式代碼來建立 SKManagedWStream 方法的 EncodeSlider。 這兩 Entry 個檢視會提供 方法的資料夾和檔名 SavePhotoAsync

大部分的這個方法都致力於處理問題或錯誤。 如果 Encode 建立空陣列,表示不支援特定的檔格式。 如果 SavePhotoAsyncfalse回 ,則不會成功儲存盤案。

以下是執行中的程式:

儲存檔案格式

該螢幕快照顯示這些平臺上唯一支援的三種格式:

  • JPEG
  • PNG
  • WebP

對於所有其他格式, Encode 方法不會將任何內容寫入數據流,而結果位元組陣列是空的。

[儲存檔案格式] 頁面儲存的點陣圖是 600 像素平方。 每個圖元有 4 個字節,記憶體中總共有 1,440,000 個字節。 下表顯示各種檔案格式和品質組合的檔案大小:

格式 品質 大小
PNG N/A 492K
JPEG 0 2.95K
50 22.1K
100 206K
WebP 0 2.71K
50 11.9K
100 101K

您可以實驗各種質量設定,並檢查結果。

儲存手指油漆藝術

位圖的其中一個常見用法是在繪圖程式中,其運作為稱為陰影位圖的專案。 所有繪圖都會保留在點陣圖上,然後由程序顯示。 點陣圖也方便儲存繪圖。

SkiaSharp 中的手指 小畫家 文章示範如何使用觸控追蹤來實作原始的手指繪製程式。 程式只支援一種色彩,而且只支援一個筆劃寬度,但它會保留物件集合 SKPath 中的整個繪圖。

在範例中使用 [儲存] 頁面的 [手指] 小畫家 也會保留物件集合SKPath中的整個繪圖,但它也會在點圖上轉譯繪圖,而該繪圖可以儲存到相片庫。

此程式的大部分都類似於原始的 Finger 小畫家 程式。 其中一項增強功能是 XAML 檔案現在會具現化標示為 [清除] 和 [儲存] 的按鈕:

<?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.Views.Forms;assembly=SkiaSharp.Views.Forms"
             xmlns:tt="clr-namespace:TouchTracking"
             x:Class="SkiaSharpFormsDemos.Bitmaps.FingerPaintSavePage"
             Title="Finger Paint Save">

    <StackLayout>
        <Grid BackgroundColor="White"
              VerticalOptions="FillAndExpand">
            <skia:SKCanvasView x:Name="canvasView"
                               PaintSurface="OnCanvasViewPaintSurface" />
            <Grid.Effects>
                <tt:TouchEffect Capture="True"
                                TouchAction="OnTouchEffectAction" />
            </Grid.Effects>
        </Grid>

        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>
        </Grid>

        <Button Text="Clear"
                Grid.Row="0"
                Margin="50, 5"
                Clicked="OnClearButtonClicked" />

        <Button Text="Save"
                Grid.Row="1"
                Margin="50, 5"
                Clicked="OnSaveButtonClicked" />

    </StackLayout>
</ContentPage>

程序代碼後置檔案會維護名為 saveBitmap類型的SKBitmap欄位。 每當顯示介面的大小變更時,就會在處理程式中 PaintSurface 建立或重新建立此點陣圖。 如果需要重新建立位圖,現有點陣圖的內容會複製到新的點陣圖,讓無論顯示介面大小如何變更,都會保留所有專案:

public partial class FingerPaintSavePage : ContentPage
{
    ···
    SKBitmap saveBitmap;

    public FingerPaintSavePage ()
    {
        InitializeComponent ();
    }

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

        // Create bitmap the size of the display surface
        if (saveBitmap == null)
        {
            saveBitmap = new SKBitmap(info.Width, info.Height);
        }
        // Or create new bitmap for a new size of display surface
        else if (saveBitmap.Width < info.Width || saveBitmap.Height < info.Height)
        {
            SKBitmap newBitmap = new SKBitmap(Math.Max(saveBitmap.Width, info.Width),
                                              Math.Max(saveBitmap.Height, info.Height));

            using (SKCanvas newCanvas = new SKCanvas(newBitmap))
            {
                newCanvas.Clear();
                newCanvas.DrawBitmap(saveBitmap, 0, 0);
            }

            saveBitmap = newBitmap;
        }

        // Render the bitmap
        canvas.Clear();
        canvas.DrawBitmap(saveBitmap, 0, 0);
    }
    ···
}

處理程式完成的 PaintSurface 繪圖會在結尾發生,而且只包含轉譯位圖。

觸控處理類似於先前的程式。 程式會維護兩個集合, inProgressPaths 以及 completedPaths,其中包含使用者自上次清除顯示以來所繪製的所有專案。 針對每個觸控事件,處理程式會 OnTouchEffectAction 呼叫 UpdateBitmap

public partial class FingerPaintSavePage : ContentPage
{
    Dictionary<long, SKPath> inProgressPaths = new Dictionary<long, SKPath>();
    List<SKPath> completedPaths = new List<SKPath>();

    SKPaint paint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        Color = SKColors.Blue,
        StrokeWidth = 10,
        StrokeCap = SKStrokeCap.Round,
        StrokeJoin = SKStrokeJoin.Round
    };
    ···
    void OnTouchEffectAction(object sender, TouchActionEventArgs args)
    {
        switch (args.Type)
        {
            case TouchActionType.Pressed:
                if (!inProgressPaths.ContainsKey(args.Id))
                {
                    SKPath path = new SKPath();
                    path.MoveTo(ConvertToPixel(args.Location));
                    inProgressPaths.Add(args.Id, path);
                    UpdateBitmap();
                }
                break;

            case TouchActionType.Moved:
                if (inProgressPaths.ContainsKey(args.Id))
                {
                    SKPath path = inProgressPaths[args.Id];
                    path.LineTo(ConvertToPixel(args.Location));
                    UpdateBitmap();
                }
                break;

            case TouchActionType.Released:
                if (inProgressPaths.ContainsKey(args.Id))
                {
                    completedPaths.Add(inProgressPaths[args.Id]);
                    inProgressPaths.Remove(args.Id);
                    UpdateBitmap();
                }
                break;

            case TouchActionType.Cancelled:
                if (inProgressPaths.ContainsKey(args.Id))
                {
                    inProgressPaths.Remove(args.Id);
                    UpdateBitmap();
                }
                break;
        }
    }

    SKPoint ConvertToPixel(Point pt)
    {
        return new SKPoint((float)(canvasView.CanvasSize.Width * pt.X / canvasView.Width),
                            (float)(canvasView.CanvasSize.Height * pt.Y / canvasView.Height));
    }

    void UpdateBitmap()
    {
        using (SKCanvas saveBitmapCanvas = new SKCanvas(saveBitmap))
        {
            saveBitmapCanvas.Clear();

            foreach (SKPath path in completedPaths)
            {
                saveBitmapCanvas.DrawPath(path, paint);
            }

            foreach (SKPath path in inProgressPaths.Values)
            {
                saveBitmapCanvas.DrawPath(path, paint);
            }
        }

        canvasView.InvalidateSurface();
    }
    ···
}

方法UpdateBitmap會藉由建立新的SKCanvas、清除方法,然後轉譯點陣圖上的所有路徑來重新繪製saveBitmap。 最後,它會使 canvasView 位圖失效,以便在顯示器上繪製位圖。

以下是兩個按鈕的處理程式。 [ 清除 ] 按鈕會清除這兩個路徑集合、更新 saveBitmap (這會導致清除位圖),並使 SKCanvasView無效:

public partial class FingerPaintSavePage : ContentPage
{
    ···
    void OnClearButtonClicked(object sender, EventArgs args)
    {
        completedPaths.Clear();
        inProgressPaths.Clear();
        UpdateBitmap();
        canvasView.InvalidateSurface();
    }

    async void OnSaveButtonClicked(object sender, EventArgs args)
    {
        using (SKImage image = SKImage.FromBitmap(saveBitmap))
        {
            SKData data = image.Encode();
            DateTime dt = DateTime.Now;
            string filename = String.Format("FingerPaint-{0:D4}{1:D2}{2:D2}-{3:D2}{4:D2}{5:D2}{6:D3}.png",
                                            dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second, dt.Millisecond);

            IPhotoLibrary photoLibrary = DependencyService.Get<IPhotoLibrary>();
            bool result = await photoLibrary.SavePhotoAsync(data.ToArray(), "FingerPaint", filename);

            if (!result)
            {
                await DisplayAlert("FingerPaint", "Artwork could not be saved. Sorry!", "OK");
            }
        }
    }
}

[儲存] 按鈕處理程式會使用 的SKImage簡化Encode方法。 這個方法會使用 PNG 格式進行編碼。 對象 SKImage 會根據 saveBitmap建立, SKData 而且 物件包含編碼的 PNG 檔案。

ToArraySKData 方法會取得位元組陣列。 這是傳遞至 SavePhotoAsync 方法的內容,以及固定的資料夾名稱,以及從目前日期和時間建構的唯一檔名。

以下是運作中的程式:

手指 小畫家 儲存

範例中會使用非常類似的技術。 這也是一個手指繪製程式,除了用戶畫在旋轉磁碟上,然後重現其他四象限的設計。 手指油漆的色彩隨著磁碟旋轉而變更:

微調 小畫家

類別的 [儲存] 按鈕類似於 Finger 小畫家,因為它會將影像儲存到固定的資料夾名稱(西班牙 小畫家),以及從日期和時間建SpinPaint構的檔名。