Condividi tramite


Salvataggio di bitmap SkiaSharp in file

Dopo che un'applicazione SkiaSharp ha creato o modificato una bitmap, l'applicazione potrebbe voler salvare la bitmap nella raccolta foto dell'utente:

Salvataggio di bitmap

Questa attività include due passaggi:

  • Conversione della bitmap SkiaSharp in dati in un formato di file specifico, ad esempio JPEG o PNG.
  • Salvare il risultato nella raccolta foto usando codice specifico della piattaforma.

Formati e codec di file

La maggior parte dei formati di file bitmap più diffusi di oggi usa la compressione per ridurre lo spazio di archiviazione. Le due ampie categorie di tecniche di compressione sono denominate lossy e lossless. Questi termini indicano se l'algoritmo di compressione comporta o meno la perdita di dati.

Il formato più diffuso è stato sviluppato dal Joint Photographic Experts Group ed è chiamato JPEG. L'algoritmo di compressione JPEG analizza l'immagine usando uno strumento matematico denominato trasformazione del coseno discreto e tenta di rimuovere i dati che non sono cruciali per preservare la fedeltà visiva dell'immagine. Il grado di compressione può essere controllato con un'impostazione generalmente definita qualità. Le impostazioni di qualità più elevate comportano file di dimensioni maggiori.

Al contrario, un algoritmo di compressione senza perdita di dati analizza l'immagine per la ripetizione e i modelli di pixel che possono essere codificati in modo da ridurre i dati, ma non comportano la perdita di informazioni. I dati bitmap originali possono essere ripristinati interamente dal file compresso. Il formato primario di file compresso senza perdita di dati attualmente in uso è Portable Network Graphics (PNG).

In genere, JPEG viene usato per le fotografie, mentre PNG viene usato per le immagini che sono state generate manualmente o in modo algoritmico. Qualsiasi algoritmo di compressione senza perdita che riduce le dimensioni di alcuni file deve necessariamente aumentare le dimensioni di altri. Fortunatamente, questo aumento delle dimensioni si verifica in genere solo per i dati che contengono molte informazioni casuali (o apparentemente casuali).

Gli algoritmi di compressione sono sufficientemente complessi da giustificare due termini che descrivono i processi di compressione e decompressione:

  • decodifica : leggere un formato di file bitmap e decomprimerlo
  • encode : comprimere la bitmap e scrivere in un formato di file bitmap

La SKBitmap classe contiene diversi metodi denominati Decode che creano un oggetto da un'origine SKBitmap compressa. Tutto ciò che è necessario è fornire un nome file, un flusso o una matrice di byte. Il decodificatore può determinare il formato del file e consegnarlo alla funzione di decodifica interna appropriata.

Inoltre, la SKCodec classe dispone di due metodi denominati Create che possono creare un SKCodec oggetto da un'origine compressa e consentire a un'applicazione di essere più coinvolti nel processo di decodifica. La SKCodec classe è illustrata nell'articolo Animazione di bitmap SkiaSharp in relazione alla decodifica di un file GIF animato.

Quando si codifica una bitmap, sono necessarie altre informazioni: il codificatore deve conoscere il formato di file specifico che l'applicazione vuole usare (JPEG o PNG o qualcos'altro). Se si desidera un formato di perdita, la codifica deve conoscere anche il livello di qualità desiderato.

La SKBitmap classe definisce un Encode metodo con la sintassi seguente:

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

Questo metodo è descritto in modo più dettagliato a breve. La bitmap codificata viene scritta in un flusso scrivibile. ('W' in SKWStream sta per "scrivibile".) Il secondo e il terzo argomento specificano il formato di file e (per i formati di perdita) la qualità desiderata compresa tra 0 e 100.

Inoltre, le SKImage classi e SKPixmap definiscono Encode anche metodi che sono un po 'più versatili e che si potrebbe preferire. È possibile creare facilmente un SKImage oggetto da un SKBitmap oggetto usando il metodo statico SKImage.FromBitmap . È possibile ottenere un SKPixmap oggetto da un SKBitmap oggetto usando il PeekPixels metodo .

Uno dei Encode metodi definiti da SKImage non ha parametri e salva automaticamente in un formato PNG. Questo metodo senza parametri è molto facile da usare.

Codice specifico della piattaforma per il salvataggio di file bitmap

Quando si codifica un SKBitmap oggetto in un formato di file specifico, in genere si verrà lasciati con un oggetto flusso di qualche ordinamento o una matrice di dati. Alcuni dei Encode metodi (incluso quello senza parametri definiti da SKImage) restituiscono un SKData oggetto, che può essere convertito in una matrice di byte usando il ToArray metodo . Questi dati devono quindi essere salvati in un file.

Il salvataggio in un file nell'archiviazione locale dell'applicazione è piuttosto semplice perché è possibile usare classi e metodi standard System.IO per questa attività. Questa tecnica è illustrata nell'articolo Animare le bitmap SkiaSharp in relazione all'animazione di una serie di bitmap del set Di Mandelbrot.

Se si desidera che il file venga condiviso da altre applicazioni, deve essere salvato nella raccolta foto dell'utente. Questa attività richiede codice specifico della Xamarin.FormsDependencyServicepiattaforma e l'uso di .

Il progetto SkiaSharpFormsDemo nell'applicazione di esempio definisce un'interfaccia IPhotoLibrary usata con la DependencyService classe . In questo modo viene definita la sintassi di un SavePhotoAsync metodo:

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

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

Questa interfaccia definisce anche il PickPhotoAsync metodo , usato per aprire la selezione file specifica della piattaforma per la libreria di foto del dispositivo.

Per SavePhotoAsync, il primo argomento è una matrice di byte che contiene la bitmap già codificata in un formato di file specifico, ad esempio JPEG o PNG. È possibile che un'applicazione voglia isolare tutte le bitmap create in una determinata cartella, specificata nel parametro successivo, seguita dal nome del file. Il metodo restituisce un valore Boolean che indica o meno l'esito positivo.

Le sezioni seguenti illustrano come SavePhotoAsync viene implementato in ogni piattaforma.

Implementazione di iOS

L'implementazione di iOS di SavePhotoAsync usa il SaveToPhotosAlbum metodo di 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;
    }
}

Sfortunatamente, non è possibile specificare un nome file o una cartella per l'immagine.

Il file Info.plist nel progetto iOS richiede una chiave che indica che aggiunge immagini alla raccolta foto:

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

Attento! La chiave di autorizzazione per accedere semplicemente alla raccolta foto è molto simile ma non la stessa:

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

Implementazione di Android

L'implementazione android di SavePhotoAsync controlla innanzitutto se l'argomento folder è null o una stringa vuota. In tal caso, la bitmap viene salvata nella directory radice della raccolta foto. In caso contrario, la cartella viene ottenuta e, se non esiste, viene creata:

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

La chiamata a MediaScannerConnection.ScanFile non è strettamente necessaria, ma se si sta testando il programma controllando immediatamente la raccolta foto, è molto utile aggiornando la visualizzazione della raccolta librerie.

Il file AndroidManifest.xml richiede il tag di autorizzazione seguente:

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

Implementazione della piattaforma UWP

L'implementazione UWP di SavePhotoAsync è molto simile alla struttura dell'implementazione di 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;
    }
}

La sezione Capabilities del file Package.appxmanifest richiede la raccolta immagini.

Esplorazione dei formati di immagine

Ecco il Encode metodo di SKImage nuovo:

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

SKEncodedImageFormat è un'enumerazione con membri che fanno riferimento a undici formati di file bitmap, alcuni dei quali sono piuttosto oscuri:

  • Astc — Compressione a trama scalabile adattiva
  • Bmp — Bitmap di Windows
  • Dng — Adobe Digital Negative
  • Gif — Formato interscambio grafico
  • Ico — Immagini delle icone di Windows
  • Jpeg — Gruppo congiunto di esperti fotografici
  • Ktx — Formato di trama Khronos per OpenGL
  • Pkm — Formato personalizzato per GrafX2
  • Png — Grafica di rete portabile
  • Wbmp — Formato bitmap del protocollo applicazione wireless (1 bit per pixel)
  • Webp — Formato Google WebP

Come si vedrà a breve, solo tre di questi formati di file (Jpeg, Pnge Webp) sono effettivamente supportati da SkiaSharp.

Per salvare un SKBitmap oggetto denominato bitmap nella raccolta foto dell'utente, è necessario anche un membro dell'enumerazione SKEncodedImageFormat denominata imageFormat e (per i formati di perdita) di una variabile integer quality . È possibile usare il codice seguente per salvare tale bitmap in un file con il nome filename nella folder cartella :

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

La SKManagedWStream classe deriva da SKWStream (che sta per "flusso scrivibile"). Il Encode metodo scrive il file bitmap codificato in tale flusso. I commenti in tale codice fanno riferimento a un controllo degli errori che potrebbe essere necessario eseguire.

La pagina Salva formati di file nell'applicazione di esempio usa codice simile per consentire di sperimentare il salvataggio di una bitmap nei vari formati.

Il file XAML contiene un oggetto SKCanvasView che visualizza una bitmap, mentre il resto della pagina contiene tutto ciò che l'applicazione deve chiamare il Encode metodo di SKBitmap. Ha un Picker oggetto per un membro dell'enumerazione SKEncodedImageFormat , un Slider per l'argomento di qualità per i formati bitmap in perdita, due Entry visualizzazioni per un nome file e un nome di cartella e un Button per il salvataggio del file.

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

Il file code-behind carica una risorsa bitmap e usa per SKCanvasView visualizzarlo. La bitmap non cambia mai. Il SelectedIndexChanged gestore per l'oggetto Picker modifica il nome file con un'estensione uguale al membro di enumerazione:

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

Il Clicked gestore per l'oggetto Button esegue tutto il lavoro reale. Ottiene due argomenti per Encode da Picker e Slidere quindi usa il codice illustrato in precedenza per creare un SKManagedWStream oggetto per il Encode metodo . Le due Entry visualizzazioni forniscono nomi di file e cartelle per il SavePhotoAsync metodo .

La maggior parte di questo metodo è dedicata alla gestione di problemi o errori. Se Encode crea una matrice vuota, significa che il formato di file specifico non è supportato. Se SavePhotoAsync restituisce false, il file non è stato salvato correttamente.

Ecco il programma in esecuzione:

Salva formati di file

Screenshot che mostra gli unici tre formati supportati in queste piattaforme:

  • JPEG
  • PNG
  • Webp

Per tutti gli altri formati, il Encode metodo scrive nulla nel flusso e la matrice di byte risultante è vuota.

La bitmap salvata nella pagina Salva formati file è un quadrato di 600 pixel. Con 4 byte per pixel, questo è un totale di 1.440.000 byte in memoria. La tabella seguente illustra le dimensioni del file per varie combinazioni di formato e qualità del file:

Formato Qualità Dimensione
PNG N/D 492.000
JPEG 0 2,95 K
50 22.1K
100 206.000
Webp 0 2.71.000
50 11,9 K
100 101.000

È possibile sperimentare diverse impostazioni di qualità ed esaminare i risultati.

Salvataggio dell'arte con dita

Un uso comune di una bitmap è nei programmi di disegno, in cui funziona come qualcosa chiamato bitmap di ombreggiatura. Tutto il disegno viene conservato nella bitmap, che viene quindi visualizzato dal programma. La bitmap è utile anche per salvare il disegno.

L'articolo Finger Painting in SkiaSharp ha illustrato come usare il rilevamento del tocco per implementare un programma primitivo di pittura con dita. Il programma supportava un solo colore e una sola larghezza del tratto, ma conservava l'intero disegno in una raccolta di SKPath oggetti.

La pagina Finger Paint con Salva nell'esempio mantiene anche l'intero disegno in una raccolta di SKPath oggetti, ma esegue anche il rendering del disegno su una bitmap, che può essere salvata nella raccolta foto.

Gran parte di questo programma è simile al programma originale Finger Paint . Un miglioramento è che il file XAML crea ora un'istanza dei pulsanti con etichetta Cancella e Salva:

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

Il file code-behind gestisce un campo di tipo SKBitmap denominato saveBitmap. Questa bitmap viene creata o ricreata nel PaintSurface gestore ogni volta che cambiano le dimensioni della superficie di visualizzazione. Se la bitmap deve essere ricreata, il contenuto della bitmap esistente viene copiato nella nuova bitmap in modo che tutto venga mantenuto indipendentemente dal modo in cui la superficie di visualizzazione cambia di dimensione:

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

Il disegno eseguito dal PaintSurface gestore si verifica alla fine e consiste esclusivamente nel rendering della bitmap.

L'elaborazione del tocco è simile al programma precedente. Il programma gestisce due raccolte, inProgressPaths e completedPaths, che contengono tutto ciò che l'utente ha disegnato dall'ultima volta che la visualizzazione è stata cancellata. Per ogni evento di tocco, il OnTouchEffectAction gestore chiama 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();
    }
    ···
}

Il UpdateBitmap metodo viene ridisegnato saveBitmap creando un nuovo SKCanvasoggetto , cancellandolo e quindi eseguendo il rendering di tutti i percorsi nella bitmap. Termina invalidando canvasView in modo che la bitmap possa essere disegnata sullo schermo.

Ecco i gestori per i due pulsanti. Il pulsante Cancella cancella entrambe le raccolte di percorsi, gli aggiornamenti saveBitmap (che comportano la cancellazione della bitmap) e invalida :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");
            }
        }
    }
}

Il gestore del pulsante Salva usa il metodo semplificato Encode da SKImage. Questo metodo codifica usando il formato PNG. L'oggetto SKImage viene creato in base a saveBitmape l'oggetto SKData contiene il file PNG codificato.

Il ToArray metodo di SKData ottiene una matrice di byte. Questo è ciò che viene passato al metodo, insieme a SavePhotoAsync un nome di cartella fisso, e un nome file univoco costruito dalla data e dall'ora correnti.

Ecco il programma in azione:

Salva disegno con dito

Nell'esempio viene usata una tecnica molto simile. Si tratta anche di un programma di disegno con dita, ad eccezione del fatto che l'utente disegna su un disco rotante che riproduce quindi i disegni sugli altri quattro quadranti. Il colore della vernice del dito cambia man mano che il disco sta ruotando:

Disegno rotazione

Il pulsante Salva della SpinPaint classe è simile a Finger Paint in quanto salva l'immagine in un nome di cartella fisso (SpagnaPaint) e un nome file costruito dalla data e dall'ora.