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:
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 SavePhotoAsync
dem 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 TexturkomprimierungBmp
— Windows-BitmapDng
— Adobe Digital NegativGif
— GrafikaustauschformatIco
— Windows-SymbolbilderJpeg
— Gemeinsame Gruppe fototechnischer ExpertenKtx
— Khronos-Texturformat für OpenGLPkm
— Benutzerdefiniertes Format für GrafX2Png
— Portable NetzwerkgrafikenWbmp
— 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
, Png
und 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 SKBitmap
benö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 Slider
aus und verwendet dann den code, der zuvor gezeigt wurde, um eine SKManagedWStream
für die Encode
Picker
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 false
wird, wurde die Datei nicht erfolgreich gespeichert.
Dies ist das Programm, das ausgeführt wird:
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 inProgressPaths
completedPaths
enthä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 saveBitmap
SKCanvas
neu 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 saveBitmap
dem 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:
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:
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.