Freigeben über


Speichern von SkiaSharp-Bitmaps in Dateien

Nachdem eine SkiaSharp-Anwendung eine Bitmap erstellt oder geändert hat, möchte die Anwendung die Bitmap möglicherweise in der Fotobibliothek des Benutzers speichern:

Speichern von Bitmaps

Diese Aufgabe umfasst zwei Schritte:

  • Konvertieren der SkiaSharp-Bitmap in Daten in einem bestimmten Dateiformat, z. B. JPEG oder PNG.
  • Das Ergebnis wird mithilfe von plattformspezifischem Code in der Fotobibliothek gespeichert.

Dateiformate und Codecs

Die meisten gängigen Bitmapdateiformate verwenden Komprimierung, um Speicherplatz zu reduzieren. Die beiden breiten Kategorien von Komprimierungstechniken werden als verlustfrei und verlustlos bezeichnet. Diese Begriffe geben an, ob der Komprimierungsalgorithmus zu Einem Datenverlust führt.

Das beliebteste Verlustformat wurde von der Joint Photographic Experts Group entwickelt und heißt JPEG. Der JPEG-Komprimierungsalgorithmus analysiert das Bild mithilfe eines mathematischen Tools, das als diskrete Kosinustransformation bezeichnet wird, und versucht, Daten zu entfernen, die für die Beibehaltung der Bildtreue nicht von entscheidender Bedeutung sind. Der Grad der Komprimierung kann mit einer Einstellung gesteuert werden, die im Allgemeinen als Qualität bezeichnet wird. Höhere Qualitätseinstellungen führen zu größeren Dateien.

Im Gegensatz dazu analysiert ein verlustloser Komprimierungsalgorithmus das Bild auf Wiederholungen und Muster von Pixeln, die so codiert werden können, dass die Daten reduziert werden, aber nicht zu einem Verlust von Informationen führen. Die ursprünglichen Bitmapdaten können vollständig aus der komprimierten Datei wiederhergestellt werden. Das primäre verlustlose komprimierte Dateiformat, das heute verwendet wird, ist PORTABLE Network Graphics (PNG).

Im Allgemeinen wird JPEG für Fotos verwendet, während PNG für Bilder verwendet wird, die manuell oder algorithmisch generiert wurden. Jeder verlustlose Komprimierungsalgorithmus, der die Größe einiger Dateien reduziert, muss die Größe anderer Dateien unbedingt erhöhen. Glücklicherweise erfolgt diese Zunahme in der Regel nur für Daten, die viele zufällige (oder scheinbar zufällige) Informationen enthalten.

Die Komprimierungsalgorithmen sind komplex genug, um zwei Begriffe zu rechtfertigen, die die Komprimierungs- und Dekomprimierungsprozesse beschreiben:

  • Decodieren – Lesen eines Bitmapdateiformats und Dekomprimieren
  • codieren – Komprimieren der Bitmap und Schreiben in ein Bitmapdateiformat

Die SKBitmap Klasse enthält mehrere Methoden, die eine Decode aus einer komprimierten Quelle erstellen SKBitmap . Alles, was erforderlich ist, ist das Bereitstellen eines Dateinamens, Eines Datenstroms oder eines Bytearrays. Der Decoder kann das Dateiformat bestimmen und an die richtige interne Decodierungsfunktion übergeben.

Darüber hinaus verfügt die SKCodec Klasse über zwei Methoden mit dem Namen Create , mit denen ein SKCodec Objekt aus einer komprimierten Quelle erstellt werden kann, und eine Anwendung die Einbindung in den Decodierungsprozess ermöglichen kann. (Die SKCodec Klasse wird im Artikel Animieren von SkiaSharp Bitmaps in Verbindung mit der Decodierung einer animierten GIF-Datei gezeigt.)

Bei der Codierung einer Bitmap sind weitere Informationen erforderlich: Der Encoder muss das bestimmte Dateiformat kennen, das die Anwendung verwenden möchte (JPEG oder PNG oder etwas anderes). Wenn ein verlustbehaftetes Format gewünscht wird, muss die Codierung auch das gewünschte Qualitätsniveau kennen.

Die SKBitmap Klasse definiert eine Encode Methode mit der folgenden Syntax:

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

Diese Methode wird kurz beschrieben. Die codierte Bitmap wird in einen schreibbaren Datenstrom geschrieben. (Das "W" steht SKWStream für "schreibbar".) Die zweiten und dritten Argumente geben das Dateiformat und (für Verlustformate) die gewünschte Qualität zwischen 0 und 100 an.

Darüber hinaus definieren Encode die SKImage Klassen SKPixmap auch Methoden, die etwas vielseitiger sind und die Sie bevorzugen. Sie können ein SKImage Objekt mithilfe der statischen SKImage.FromBitmap Methode ganz einfach aus einem SKBitmap Objekt erstellen. Mit der PeekPixels Methode können Sie ein SKPixmap Objekt aus einem SKBitmap Objekt abrufen.

Eine der Encode von SKImage ihnen definierten Methoden weist keine Parameter auf und speichert automatisch in einem PNG-Format. Diese parameterlose Methode ist sehr einfach zu verwenden.

Plattformspezifischer Code zum Speichern von Bitmapdateien

Wenn Sie ein SKBitmap Objekt in ein bestimmtes Dateiformat codieren, bleiben Sie in der Regel mit einem Datenstromobjekt einer Art oder einem Datenarray erhalten. Einige der Encode Methoden (einschließlich der Methode ohne Parameter SKImage), geben ein SKData Objekt zurück, das mithilfe der ToArray Methode in ein Bytearray konvertiert werden kann. Diese Daten müssen dann in einer Datei gespeichert werden.

Das Speichern in einer Datei im lokalen Anwendungsspeicher ist ganz einfach, da Sie Standardklassen System.IO und Methoden für diese Aufgabe verwenden können. Diese Technik wird im Artikel Animieren von SkiaSharp Bitmaps im Zusammenhang mit der Animation einer Reihe von Bitmaps des Mandelbrot-Satzes veranschaulicht.

Wenn die Datei von anderen Anwendungen freigegeben werden soll, muss sie in der Fotobibliothek des Benutzers gespeichert werden. Diese Aufgabe erfordert plattformspezifischen Code und die Verwendung der Xamarin.FormsDependencyService.

Das SkiaSharpFormsDemo-Projekt in der Beispielanwendung definiert eine IPhotoLibrary Schnittstelle, die mit der DependencyService Klasse verwendet wird. Dadurch wird die Syntax einer SavePhotoAsync Methode definiert:

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

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

Diese Schnittstelle definiert auch die PickPhotoAsync Methode, die zum Öffnen der plattformspezifischen Dateiauswahl für die Fotobibliothek des Geräts verwendet wird.

Bei SavePhotoAsyncdem ersten Argument handelt es sich um ein Bytearray, das die Bitmap enthält, die bereits in einem bestimmten Dateiformat codiert ist, z. B. JPEG oder PNG. Es ist möglich, dass eine Anwendung möglicherweise alle bitmaps isolieren möchte, die sie in einem bestimmten Ordner erstellt, der im nächsten Parameter angegeben ist, gefolgt vom Dateinamen. Die Methode gibt einen Wert vom Typ Boolean zurück, der den Erfolg angibt.

In den folgenden Abschnitten wird erläutert, wie SavePhotoAsync auf jeder Plattform implementiert wird.

Die iOS-Implementierung

Die iOS-Implementierung der SavePhotoAsync Verwendung der SaveToPhotosAlbum Methode von UIImage:

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;
    }
}

Leider gibt es keine Möglichkeit, einen Dateinamen oder Ordner für das Bild anzugeben.

Für die Datei "Info.plist " im iOS-Projekt ist ein Schlüssel erforderlich, der angibt, dass der Fotobibliothek Bilder hinzugefügt werden:

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

Vorsicht! Der Berechtigungsschlüssel für den einfachen Zugriff auf die Fotobibliothek ist sehr ähnlich, aber nicht identisch:

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

Die Android-Implementierung

Die Android-Implementierung der SavePhotoAsync ersten Überprüfung, ob das folder Argument oder eine leere Zeichenfolge ist null . Wenn ja, wird die Bitmap im Stammverzeichnis der Fotobibliothek gespeichert. Andernfalls wird der Ordner abgerufen, und wenn er nicht vorhanden ist, wird er erstellt:

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;
    }
}

Der Aufruf an MediaScannerConnection.ScanFile ist nicht unbedingt erforderlich, aber wenn Sie Ihr Programm testen, indem Sie sofort die Fotobibliothek überprüfen, hilft es viel, indem Sie die Bibliothekskatalogansicht aktualisieren.

Für die datei AndroidManifest.xml ist das folgende Berechtigungstag erforderlich:

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

Die UWP-Implementierung

Die UWP-Implementierung von SavePhotoAsync ist in der Struktur der Android-Implementierung sehr ähnlich:

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;
    }
}

Der Abschnitt "Funktionen" der Datei "Package.appxmanifest " erfordert die Bildbibliothek.

Untersuchen der Bildformate

Hier ist die Encode Methode von SKImage erneut:

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

SKEncodedImageFormat ist eine Aufzählung mit Elementen, die auf elf Bitmapdateiformate verweisen, von denen einige eher verdeckt sind:

  • Astc — Adaptive skalierbare Texturkomprimierung
  • Bmp — Windows-Bitmap
  • Dng — Adobe Digital Negativ
  • Gif — Grafikaustauschformat
  • Ico — Windows-Symbolbilder
  • Jpeg — Gemeinsame Gruppe fototechnischer Experten
  • Ktx — Khronos-Texturformat für OpenGL
  • Pkm — Benutzerdefiniertes Format für GrafX2
  • Png — Portable Netzwerkgrafiken
  • Wbmp — Bitmapformat für drahtlosanwendungsprotokoll (1 Bit pro Pixel)
  • Webp — Google WebP-Format

Wie Sie in Kürze sehen werden, werden nur drei dieser Dateiformate (Jpeg, Pngund Webp) tatsächlich von SkiaSharp unterstützt.

Zum Speichern eines SKBitmap Objekts, das in der Fotobibliothek des Benutzers benannt ist bitmap , benötigen Sie auch ein Element der SKEncodedImageFormat Enumeration namens imageFormat und (für Verlustformate) eine ganzzahlige quality Variable. Sie können den folgenden Code verwenden, um diese Bitmap in einer Datei mit dem Namen filename im folder Ordner zu speichern:

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!
}

Die SKManagedWStream Klasse wird von SKWStream (die für "schreibbarer Datenstrom" steht) abgeleitet. Die Encode Methode schreibt die codierte Bitmapdatei in diesen Datenstrom. Die Kommentare in diesem Code beziehen sich auf einige Fehlerüberprüfungen, die Sie möglicherweise ausführen müssen.

Die Seite "Dateiformate speichern" in der Beispielanwendung verwendet ähnlichen Code, damit Sie mit dem Speichern einer Bitmap in den verschiedenen Formaten experimentieren können.

Die XAML-Datei enthält eine SKCanvasView Bitmap, während der Rest der Seite alles enthält, was die Anwendung zum Aufrufen der Encode Methode SKBitmapbenötigt. Es verfügt über ein Picker Element der SKEncodedImageFormat Enumeration, ein Slider für das Qualitätsargument für verlustbehaftete Bitmapformate, zwei Entry Ansichten für einen Dateinamen und Ordnernamen und einen Button zum Speichern der Datei.

<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>

Die CodeBehind-Datei lädt eine Bitmapressource und verwendet die SKCanvasView Datei, um sie anzuzeigen. Diese Bitmap ändert sich nie. Der SelectedIndexChanged Handler für den Picker Dateinamen ändert den Dateinamen mit einer Erweiterung, die dem Enumerationselement entspricht:

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!";
                }
            }
        }
    }
}

Der Clicked Handler für die Button eigentliche Arbeit. Sie ruft zwei Argumente für Encode das Und und Slideraus und verwendet dann den code, der zuvor gezeigt wurde, um eine SKManagedWStream für die EncodePicker Methode zu erstellen. Die beiden Entry Ansichten enthalten Ordner- und Dateinamen für die SavePhotoAsync Methode.

Die meisten dieser Methode widmen sich der Behandlung von Problemen oder Fehlern. Wenn Encode ein leeres Array erstellt wird, bedeutet dies, dass das bestimmte Dateiformat nicht unterstützt wird. Wenn SavePhotoAsync die Datei zurückgegeben falsewird, wurde die Datei nicht erfolgreich gespeichert.

Dies ist das Programm, das ausgeführt wird:

Dateiformate speichern

Dieser Screenshot zeigt die einzigen drei Formate, die auf diesen Plattformen unterstützt werden:

  • JPEG
  • PNG
  • WebP

Für alle anderen Formate schreibt die Encode Methode nichts in den Datenstrom, und das resultierende Bytearray ist leer.

Die Bitmap, die die Seite "Dateiformate speichern" speichert, ist 600 Pixel quadratisch. Bei 4 Bytes pro Pixel beträgt das insgesamt 1.440.000 Bytes im Arbeitsspeicher. Die folgende Tabelle zeigt die Dateigröße für verschiedene Kombinationen aus Dateiformat und Qualität:

Format Quality Size
PNG N/V 492K
JPEG 0 2.95K
50 22,1 Tsd.
100 206K
WebP 0 2.71K
50 11.9K
100 101K

Sie können mit verschiedenen Qualitätseinstellungen experimentieren und die Ergebnisse untersuchen.

Speichern von Fingerfarben

Eine häufige Verwendung einer Bitmap ist in Zeichenprogrammen, in denen sie als eine Schattenbitmap fungiert. Die gesamte Zeichnung wird auf der Bitmap beibehalten, die dann vom Programm angezeigt wird. Die Bitmap ist auch praktisch zum Speichern der Zeichnung.

In dem Artikel "Finger Painting" in SkiaSharp wurde gezeigt, wie Sie die Touchverfolgung verwenden, um ein primitives Finger-Painting-Programm zu implementieren. Das Programm unterstützt nur eine Farbe und nur eine Strichbreite, die gesamte Zeichnung wurde jedoch in einer Auflistung von SKPath Objekten beibehalten.

Das Zeichenblatt "Finger Paint mit Speichern " im Beispiel behält auch die gesamte Zeichnung in einer Sammlung von SKPath Objekten bei, rendert aber auch die Zeichnung auf einer Bitmap, die sie in Ihrer Fotobibliothek speichern kann.

Ein Großteil dieses Programms ähnelt dem ursprünglichen Finger Paint-Programm . Eine Verbesserung besteht darin, dass die XAML-Datei jetzt Schaltflächen mit der Bezeichnung "Löschen " und "Speichern" instanziiert:

<?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>

Die CodeBehind-Datei Standard ein Feld vom Typ SKBitmap "benanntsaveBitmap" enthält. Diese Bitmap wird im PaintSurface Handler erstellt oder neu erstellt, wenn sich die Größe der Anzeigeoberfläche ändert. Wenn die Bitmap neu erstellt werden muss, werden die Inhalte der vorhandenen Bitmap in die neue Bitmap kopiert, damit alles beibehalten wird, unabhängig davon, wie sich die Anzeigeoberfläche in der Größe ändert:

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);
    }
    ···
}

Die vom PaintSurface Handler vorgenommene Zeichnung erfolgt am Ende und besteht ausschließlich aus dem Rendern der Bitmap.

Die Touchverarbeitung ähnelt dem früheren Programm. Das Programm Standard enthält zwei Auflistungen und inProgressPathscompletedPathsenthält alles, was der Benutzer seit dem letzten Löschen der Anzeige gezeichnet hat. Für jedes Touchereignis ruft der OnTouchEffectAction Handler Folgendes auf 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();
    }
    ···
}

Die UpdateBitmap Methode wird saveBitmapSKCanvasneu erstellt, gelöscht und anschließend alle Pfade in der Bitmap gerendert. Es wird durch Ungültiges canvasView beendet, sodass die Bitmap auf der Anzeige gezeichnet werden kann.

Hier sind die Handler für die beiden Schaltflächen. Die Schaltfläche "Löschen" löscht beide Pfadauflistungen, Aktualisierungen saveBitmap (was dazu führt, dass die Bitmap gelöscht wird), und die :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");
            }
        }
    }
}

Der Schaltflächenhandler "Speichern " verwendet die vereinfachte Encode Methode aus SKImage. Diese Methode codiert das PNG-Format. Das SKImage Objekt wird basierend auf saveBitmapdem Objekt erstellt, und das SKData Objekt enthält die codierte PNG-Datei.

Die ToArray Methode des SKData Abrufens eines Bytearrays. Dies ist, was an die SavePhotoAsync Methode übergeben wird, zusammen mit einem festen Ordnernamen und einem eindeutigen Dateinamen, der aus dem aktuellen Datum und der aktuellen Uhrzeit erstellt wurde.

Hier sehen Sie das Programm bei der Ausführung:

Finger Paint Speichern

Im Beispiel wird eine sehr ähnliche Technik verwendet. Dies ist auch ein Fingermalprogramm, mit dem Ausnahme, dass der Benutzer auf einer sich drehenden Scheibe zeichnet, die dann die Designs auf seinen anderen vier Quadranten reproduziert. Die Farbe der Fingerfarbe ändert sich, wenn sich der Datenträger dreht:

Spin Paint

Die Schaltfläche "Speichern " der SpinPaint Klasse ähnelt Finger Paint darin, dass das Bild in einem festen Ordnernamen (SpainPaint) und einem Dateinamen gespeichert wird, der aus dem Datum und der Uhrzeit erstellt wurde.