Udostępnij za pośrednictwem


Zapisywanie map bitowych SkiaSharp w plikach

Po utworzeniu lub zmodyfikowaniu mapy bitowej przez aplikację SkiaSharp aplikacja może chcieć zapisać mapę bitową w bibliotece zdjęć użytkownika:

Zapisywanie map bitowych

To zadanie obejmuje dwa kroki:

  • Konwertowanie mapy bitowej SkiaSharp na dane w określonym formacie pliku, takim jak JPEG lub PNG.
  • Zapisywanie wyniku w bibliotece zdjęć przy użyciu kodu specyficznego dla platformy.

Formaty plików i kodecze

Większość dzisiejszych popularnych formatów plików map bitowych używa kompresji, aby zmniejszyć ilość miejsca do magazynowania. Dwie szerokie kategorie technik kompresji są nazywane stratą i bezstratną. Te terminy wskazują, czy algorytm kompresji powoduje utratę danych.

Najbardziej popularny format strat został opracowany przez Joint Photographic Experts Group i jest nazywany JPEG. Algorytm kompresji JPEG analizuje obraz przy użyciu narzędzia matematycznego nazywanego dyskretnym transformacją cosinusu i próbuje usunąć dane, które nie mają kluczowego znaczenia dla zachowania wierności wizualnej obrazu. Stopień kompresji można kontrolować za pomocą ustawienia ogólnie określanego jako jakość. Ustawienia wyższej jakości powodują większe pliki.

Natomiast algorytm kompresji bezstratnej analizuje obraz pod kątem powtórzeń i wzorców pikseli, które mogą być kodowane w sposób, który zmniejsza dane, ale nie powoduje utraty żadnych informacji. Oryginalne dane mapy bitowej można przywrócić całkowicie z skompresowanego pliku. Podstawowy bezstratny skompresowany format pliku używany obecnie jest Portable Network Graphics (PNG).

Ogólnie rzecz biorąc, JPEG jest używany do zdjęć, podczas gdy PNG jest używany do obrazów, które zostały wygenerowane ręcznie lub algorytmicznie. Każdy bezstratny algorytm kompresji, który zmniejsza rozmiar niektórych plików, musi koniecznie zwiększyć rozmiar innych plików. Na szczęście ten wzrost rozmiaru zwykle występuje tylko w przypadku danych zawierających wiele losowych (lub pozornie losowych) informacji.

Algorytmy kompresji są wystarczająco złożone, aby uzasadnić dwa terminy opisujące procesy kompresji i dekompresji:

  • dekodowanie — odczytywanie formatu pliku mapy bitowej i dekompresowanie go
  • kodowanie — kompresowanie mapy bitowej i zapisywanie w formacie pliku mapy bitowej

Klasa SKBitmap zawiera kilka metod o nazwie Decode , które tworzą element SKBitmap ze skompresowanego źródła. Wszystko, co jest wymagane, to podanie nazwy pliku, strumienia lub tablicy bajtów. Dekoder może określić format pliku i przekazać go do odpowiedniej funkcji dekodowania wewnętrznego.

Ponadto SKCodec klasa ma dwie metody o nazwie Create , które mogą utworzyć SKCodec obiekt ze skompresowanego źródła i umożliwić aplikacji większe zaangażowanie w proces dekodowania. (Klasa SKCodec jest wyświetlana w artykule Animating SkiaSharp Bitmaps w związku z dekodowaniem animowanego pliku GIF).

Podczas kodowania mapy bitowej wymagane jest więcej informacji: koder musi znać określony format pliku, którego aplikacja chce użyć (JPEG lub PNG lub coś innego). Jeśli wymagany jest format straty, kodowanie musi również znać żądany poziom jakości.

Klasa SKBitmap definiuje jedną Encode metodę z następującą składnią:

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

Ta metoda została szczegółowo opisana wkrótce. Zakodowana mapa bitowa jest zapisywana w strumieniu zapisywalnym. ('W' w SKWStream oznacza "zapisywalny".) Drugie i trzecie argumenty określają format pliku i (dla formatów strat) żądaną jakość w zakresie od 0 do 100.

Ponadto SKImage klasy i SKPixmap definiują Encode również metody, które są nieco bardziej uniwersalne i które można preferować. Obiekt można łatwo utworzyć SKImage na podstawie SKBitmap obiektu przy użyciu metody statycznej SKImage.FromBitmap . Obiekt można uzyskać SKPixmap z SKBitmap obiektu przy użyciu PeekPixels metody .

Jedna z metod zdefiniowanych Encode przez SKImage program nie ma parametrów i automatycznie zapisuje je w formacie PNG. Ta metoda bez parametrów jest bardzo łatwa w użyciu.

Kod specyficzny dla platformy do zapisywania plików mapy bitowej

Gdy kodujesz SKBitmap obiekt w określonym formacie pliku, zazwyczaj pozostaniesz z obiektem strumienia pewnego rodzaju lub tablicą danych. Encode Niektóre metody (w tym te, które nie mają parametrów zdefiniowanych przez SKImage) zwracają SKData obiekt, który można przekonwertować na tablicę bajtów przy użyciu ToArray metody . Te dane należy następnie zapisać w pliku.

Zapisywanie w pliku w magazynie lokalnym aplikacji jest dość proste, ponieważ w tym zadaniu można używać standardowych System.IO klas i metod. Ta technika została pokazana w artykule Animating SkiaSharp Bitmaps w związku z animowaniem serii map bitowych zestawu Mandelbrot.

Jeśli chcesz, aby plik był udostępniany przez inne aplikacje, musi zostać zapisany w bibliotece zdjęć użytkownika. To zadanie wymaga kodu specyficznego dla platformy i użycia obiektu Xamarin.FormsDependencyService.

Projekt SkiaSharpFormsDemo w przykładowej aplikacji definiuje IPhotoLibrary interfejs używany z klasą DependencyService . Definiuje składnię SavePhotoAsync metody:

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

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

Ten interfejs definiuje również metodę PickPhotoAsync , która służy do otwierania selektora plików specyficznych dla platformy dla biblioteki zdjęć urządzenia.

W przypadku SavePhotoAsyncelementu pierwszy argument to tablica bajtów, która zawiera już mapę bitową zakodowaną w określonym formacie pliku, takim jak JPEG lub PNG. Możliwe, że aplikacja może chcieć odizolować wszystkie utworzone przez nią mapy bitowe do określonego folderu, który jest określony w następnym parametrze, a następnie nazwę pliku. Metoda zwraca wartość logiczną wskazującą powodzenie lub nie.

W poniższych sekcjach omówiono sposób SavePhotoAsync implementacji na każdej platformie.

Implementacja systemu iOS

Implementacja SavePhotoAsync systemu iOS używa SaveToPhotosAlbum metody 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;
    }
}

Niestety nie ma możliwości określenia nazwy pliku lub folderu dla obrazu.

Plik Info.plist w projekcie systemu iOS wymaga klucza wskazującego, że dodaje obrazy do biblioteki zdjęć:

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

Uważaj! Klucz uprawnień do uzyskiwania dostępu do biblioteki zdjęć jest bardzo podobny, ale nie taki sam:

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

Implementacja systemu Android

Implementacja systemu Android najpierw SavePhotoAsync sprawdza, czy folder argument jest null lub pusty ciąg. Jeśli tak, mapa bitowa zostanie zapisana w katalogu głównym biblioteki zdjęć. W przeciwnym razie zostanie uzyskany folder i jeśli nie istnieje, zostanie utworzony:

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

Wywołanie MediaScannerConnection.ScanFile metody nie jest ściśle wymagane, ale jeśli testujesz program, natychmiast sprawdzając bibliotekę zdjęć, znacznie pomaga to przez zaktualizowanie widoku galerii biblioteki.

Plik AndroidManifest.xml wymaga następującego tagu uprawnień:

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

Implementacja platformy UWP

Implementacja platformy UNIWERSALNEJ systemu SavePhotoAsync Windows jest bardzo podobna do implementacji systemu 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;
    }
}

Sekcja Możliwości pliku Package.appxmanifest wymaga biblioteki obrazów.

Eksplorowanie formatów obrazów

Encode Oto metoda ponownego:SKImage

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

SKEncodedImageFormat to wyliczenie z elementami członkowskimi, które odnoszą się do jedenastu formatów plików mapy bitowej, z których niektóre są raczej niejasne:

  • Astc — adaptacyjna kompresja tekstury skalowalnej
  • Bmp — Mapa bitowa systemu Windows
  • Dng — Adobe Digital Negative
  • Gif — Format wymiany grafiki
  • Ico — Obrazy ikon systemu Windows
  • Jpeg — Wspólna grupa ekspertów fotograficznych
  • Ktx — Format tekstury Khronos dla OpenGL
  • Pkm — Format niestandardowy dla programu GrafX2
  • Png — Przenośna grafika sieciowa
  • Wbmp — Format mapy bitowej protokołu aplikacji bezprzewodowej (1 bit na piksel)
  • Webp — format Google WebP

Jak zobaczysz wkrótce, tylko trzy z tych formatów plików (Jpeg, Pngi Webp) są rzeczywiście obsługiwane przez SkiaSharp.

Aby zapisać SKBitmap obiekt o nazwie bitmap w bibliotece zdjęć użytkownika, potrzebny jest również element członkowski SKEncodedImageFormat wyliczenia o nazwie imageFormat i (w przypadku formatów strat) zmiennej całkowitej quality . Możesz użyć następującego kodu, aby zapisać tę mapę bitową w pliku o nazwie filename w folderze 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!
}

Klasa SKManagedWStream pochodzi z SKWStream (co oznacza "strumień zapisywalny"). Metoda Encode zapisuje zakodowany plik mapy bitowej w tym strumieniu. Komentarze w tym kodzie odnoszą się do sprawdzania błędów, które może być konieczne.

Strona Zapisz formaty plików w przykładowej aplikacji używa podobnego kodu, aby umożliwić eksperymentowanie z zapisywaniem mapy bitowej w różnych formatach.

Plik XAML zawiera element, który wyświetla SKCanvasView mapę bitową, a pozostała część strony zawiera wszystkie elementy, których aplikacja potrzebuje do wywołania Encode metody SKBitmap. Ma Picker element członkowski SKEncodedImageFormat wyliczenia, argument Slider jakości dla formatów map bitowych stratowych, dwa Entry widoki nazwy pliku i nazwy folderu oraz dla Button zapisywania pliku.

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

Plik z kodem ładuje zasób mapy bitowej i używa go SKCanvasView do jego wyświetlenia. Ta mapa bitowa nigdy się nie zmienia. Procedura SelectedIndexChanged obsługi modyfikuje Picker nazwę pliku z rozszerzeniem, które jest takie samo jak element członkowski wyliczenia:

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

Procedura Clicked obsługi dla Button programu wykonuje całą rzeczywistą pracę. Uzyskuje dwa argumenty dla metody Encode z Picker Slideri , a następnie używa kodu pokazanego wcześniej do utworzenia SKManagedWStream Encode elementu dla metody . Entry Dwa widoki zapewniają nazwy folderów i plików dla SavePhotoAsync metody .

Większość tej metody jest poświęcona obsłudze problemów lub błędów. Jeśli Encode zostanie utworzona pusta tablica, oznacza to, że określony format pliku nie jest obsługiwany. Jeśli SavePhotoAsync zwraca wartość false, plik nie został pomyślnie zapisany.

Oto uruchomiony program:

Zapisz formaty plików

Ten zrzut ekranu przedstawia tylko trzy formaty obsługiwane na tych platformach:

  • JPEG
  • PNG
  • WebP

W przypadku wszystkich pozostałych Encode formatów metoda nie zapisuje nic w strumieniu, a wynikowa tablica bajtów jest pusta.

Mapa bitowa zapisywana na stronie Zapisz formaty plików to 600 pikseli kwadratowych. W przypadku 4 bajtów na piksel jest to łącznie 1440 000 bajtów w pamięci. W poniższej tabeli przedstawiono rozmiar pliku dla różnych kombinacji formatu i jakości pliku:

Formatuj Kontrola Rozmiar
PNG Nie dotyczy 492K
JPEG 0 2.95K
50 22.1K
100 206K
WebP 0 2.71K
50 11.9K
100 101K

Możesz eksperymentować z różnymi ustawieniami jakości i sprawdzać wyniki.

Zapisywanie sztuki malowania palcami

Jednym z typowych zastosowań mapy bitowej jest rysowanie programów, w których działa jako coś nazywanego mapą bitową cienia. Cały rysunek jest zachowywany na mapie bitowej, która jest następnie wyświetlana przez program. Mapa bitowa jest również przydatna do zapisywania rysunku.

W artykule Finger Painting in SkiaSharp pokazano, jak używać śledzenia dotykowego do implementowania pierwotnego programu malowania palcami. Program obsługiwał tylko jeden kolor i tylko jedną szerokość pociągnięcia, ale zachował cały rysunek w kolekcji SKPath obiektów.

Strona Finger Paint z zapisywaniem w przykładzie zachowuje również cały rysunek w kolekcji SKPath obiektów, ale również renderuje rysunek na mapie bitowej, którą można zapisać w bibliotece zdjęć.

Większość tego programu jest podobna do oryginalnego programu Finger Paint . Jednym z ulepszeń jest to, że plik XAML tworzy teraz wystąpienia przycisków oznaczonych etykietą Wyczyść i Zapisz:

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

Plik za kodem przechowuje pole typu SKBitmap o nazwie saveBitmap. Ta mapa bitowa jest tworzona lub tworzona ponownie w PaintSurface programie obsługi za każdym razem, gdy zmienia się rozmiar powierzchni wyświetlania. Jeśli mapa bitowa musi zostać ponownie utworzona, zawartość istniejącej mapy bitowej zostanie skopiowana do nowej mapy bitowej, aby wszystko było zachowywane bez względu na sposób zmiany rozmiaru powierzchni ekranu:

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

Rysunek wykonany przez PaintSurface program obsługi występuje na samym końcu i składa się wyłącznie z renderowania mapy bitowej.

Przetwarzanie dotykowe jest podobne do wcześniejszego programu. Program obsługuje dwie kolekcje i completedPaths, które zawierają wszystko, inProgressPaths co użytkownik narysował od czasu ostatniego wyczyszczenia wyświetlacza. Dla każdego zdarzenia dotykowego program obsługi wywołuje metodę 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();
    }
    ···
}

Metoda UpdateBitmap ponownie rysuje saveBitmap się przez utworzenie nowego SKCanvas, wyczyszczenie go, a następnie renderowanie wszystkich ścieżek na mapie bitowej. Kończy się to unieważnieniem canvasView , aby mapa bitowa mogła zostać narysowana na wyświetlaczu.

Oto programy obsługi dla dwóch przycisków. Przycisk Wyczyść czyści obie kolekcje ścieżek, aktualizacje saveBitmap (co powoduje wyczyszczenie mapy bitowej) i unieważnia element 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");
            }
        }
    }
}

Procedura obsługi przycisku Zapisz używa uproszczonej Encode metody z SKImageklasy . Ta metoda koduje przy użyciu formatu PNG. Obiekt SKImage jest tworzony na saveBitmappodstawie obiektu , a SKData obiekt zawiera zakodowany plik PNG.

Metoda ToArray uzyskiwania SKData tablicy bajtów. Jest to, co jest przekazywane do SavePhotoAsync metody wraz z stałą nazwą folderu i unikatową nazwą pliku skonstruowaną z bieżącej daty i godziny.

Oto program w działaniu:

Zapisywanie malowania palcami

Bardzo podobna technika jest używana w przykładzie. Jest to również program malowania palcami, z wyjątkiem tego, że użytkownik maluje na przędzenia dysku, który następnie odtwarza projekty na pozostałych czterech ćwiartkach. Kolor farby palcem zmienia się w miarę wirowania dysku:

Spin Paint

Przycisk Zapisz klasy SpinPaint jest podobny do Finger Paint, ponieważ zapisuje obraz w stałej nazwie folderu (SpainPaint) i nazwie pliku skonstruowanej z daty i godziny.