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:
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 SavePhotoAsync
elementu 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 skalowalnejBmp
— Mapa bitowa systemu WindowsDng
— Adobe Digital NegativeGif
— Format wymiany grafikiIco
— Obrazy ikon systemu WindowsJpeg
— Wspólna grupa ekspertów fotograficznychKtx
— Format tekstury Khronos dla OpenGLPkm
— Format niestandardowy dla programu GrafX2Png
— Przenośna grafika sieciowaWbmp
— 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
, Png
i 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
Slider
i , 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:
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 SKImage
klasy . Ta metoda koduje przy użyciu formatu PNG. Obiekt SKImage
jest tworzony na saveBitmap
podstawie 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:
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:
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.