SkiaSharp bit eşlemlerini animasyonlama
SkiaSharp grafiklerine animasyon uygulayan uygulamalar genellikle sabit bir hızda (genellikle her 16 milisaniyede bir) öğesini çağırır InvalidateSurface
SKCanvasView
. Yüzeyin geçersiz kılınmış olması, görüntüyü yeniden çizmek için PaintSurface
işleyiciye bir çağrı tetikler. Görseller saniyede 60 kez yeniden çizildiğinden, düzgün bir şekilde animasyonlu görünüyorlar.
Ancak, grafikler 16 milisaniye içinde işlenemeyecek kadar karmaşıksa animasyon dalgalı hale gelebilir. Programcı yenileme hızını saniyede 30 veya 15 kata düşürmeyi seçebilir, ancak bazen bu bile yeterli olmaz. Bazen grafikler o kadar karmaşıktır ki gerçek zamanlı olarak işlenemez.
Çözümlerden biri, animasyonun tek tek karelerini bir bit eşlem dizisinde işleyerek animasyona önceden hazırlanmaktır. Animasyonu görüntülemek için bu bit eşlemleri yalnızca saniyede 60 kez sırayla görüntülemek gerekir.
Elbette, bu büyük olasılıkla çok fazla bit eşlemdir, ancak büyük bütçeli 3B animasyonlu filmler bu şekilde yapılır. 3B grafikler gerçek zamanlı olarak işlenemeyecek kadar karmaşıktır. Her çerçeveyi işlemek için çok fazla işlem süresi gerekir. Filmi izlerken gördükleriniz temelde bir bit eşlem dizisidir.
SkiaSharp'ta benzer bir şey yapabilirsiniz. Bu makalede iki bit eşlem animasyonu türü gösterilmektedir. İlk örnek, Mandelbrot Kümesinin bir animasyonudur:
İkinci örnekte, skiaSharp kullanarak animasyonlu GIF dosyasının nasıl işlenme şekli gösterilmektedir.
Bit eşlem animasyonu
Mandelbrot Seti görsel olarak büyüleyicidir ancak işlemsel olarak uzundur. (Mandelbrot Kümesi ve burada kullanılan matematik hakkında bir tartışma için bkz.Sayfa 666'dan başlayarak Mobil Uygulama Xamarin.Forms Oluşturma'nın 20. bölümü. Aşağıdaki açıklama, arka plan bilgisinin olduğunu varsayar.)
Örnek, Mandelbrot Kümesindeki sabit bir noktanın sürekli yakınlaştırmasını simüle etmek için bit eşlem animasyonu kullanır. Yakınlaştırmanın ardından uzaklaştırılır ve ardından döngü sonsuza kadar veya programı sonlandırana kadar yinelenir.
Program, uygulama yerel depolama alanında depoladığı en fazla 50 bit eşlem oluşturarak bu animasyona hazırlanır. Her bit eşlem, karmaşık düzlemin genişliğinin ve yüksekliğinin yarısını önceki bit eşlem olarak kapsar. (Programda, bu bit eşlemlerin integral yakınlaştırma düzeylerini temsil ettiğini söylenir.) Bit eşlemler sırayla görüntülenir. Her bit eşlemi ölçeklendirme, bir bit eşlemden diğerine sorunsuz bir ilerleme sağlamak için animasyonludur.
ile Xamarin.FormsMobil Uygulama Oluşturma'nın 20. Bölümünde açıklanan son program gibi, Mandelbrot Animasyonunda Mandelbrot Kümesi hesaplaması sekiz parametreli zaman uyumsuz bir yöntemdir. Parametreler karmaşık bir merkez noktası ve bu orta noktayı çevreleyen karmaşık düzlemin genişliğini ve yüksekliğini içerir. Sonraki üç parametre, oluşturulacak bit eşlemin piksel genişliği ve yüksekliği ile özyinelemeli hesaplama için en fazla yineleme sayısıdır. progress
parametresi, bu hesaplamanın ilerleme durumunu görüntülemek için kullanılır. cancelToken
parametresi bu programda kullanılmaz:
static class Mandelbrot
{
public static Task<BitmapInfo> CalculateAsync(Complex center,
double width, double height,
int pixelWidth, int pixelHeight,
int iterations,
IProgress<double> progress,
CancellationToken cancelToken)
{
return Task.Run(() =>
{
int[] iterationCounts = new int[pixelWidth * pixelHeight];
int index = 0;
for (int row = 0; row < pixelHeight; row++)
{
progress.Report((double)row / pixelHeight);
cancelToken.ThrowIfCancellationRequested();
double y = center.Imaginary + height / 2 - row * height / pixelHeight;
for (int col = 0; col < pixelWidth; col++)
{
double x = center.Real - width / 2 + col * width / pixelWidth;
Complex c = new Complex(x, y);
if ((c - new Complex(-1, 0)).Magnitude < 1.0 / 4)
{
iterationCounts[index++] = -1;
}
// http://www.reenigne.org/blog/algorithm-for-mandelbrot-cardioid/
else if (c.Magnitude * c.Magnitude * (8 * c.Magnitude * c.Magnitude - 3) < 3.0 / 32 - c.Real)
{
iterationCounts[index++] = -1;
}
else
{
Complex z = 0;
int iteration = 0;
do
{
z = z * z + c;
iteration++;
}
while (iteration < iterations && z.Magnitude < 2);
if (iteration == iterations)
{
iterationCounts[index++] = -1;
}
else
{
iterationCounts[index++] = iteration;
}
}
}
}
return new BitmapInfo(pixelWidth, pixelHeight, iterationCounts);
}, cancelToken);
}
}
yöntemi, bit eşlem oluşturmak için bilgi sağlayan türde BitmapInfo
bir nesne döndürür:
class BitmapInfo
{
public BitmapInfo(int pixelWidth, int pixelHeight, int[] iterationCounts)
{
PixelWidth = pixelWidth;
PixelHeight = pixelHeight;
IterationCounts = iterationCounts;
}
public int PixelWidth { private set; get; }
public int PixelHeight { private set; get; }
public int[] IterationCounts { private set; get; }
}
Mandelbrot Animasyon XAML dosyası iki Label
görünüm içerir:ProgressBar
Button
SKCanvasView
<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"
x:Class="MandelAnima.MainPage"
Title="Mandelbrot Animation">
<StackLayout>
<Label x:Name="statusLabel"
HorizontalTextAlignment="Center" />
<ProgressBar x:Name="progressBar" />
<skia:SKCanvasView x:Name="canvasView"
VerticalOptions="FillAndExpand"
PaintSurface="OnCanvasViewPaintSurface" />
<StackLayout Orientation="Horizontal"
Padding="5">
<Label x:Name="storageLabel"
VerticalOptions="Center" />
<Button x:Name="deleteButton"
Text="Delete All"
HorizontalOptions="EndAndExpand"
Clicked="OnDeleteButtonClicked" />
</StackLayout>
</StackLayout>
</ContentPage>
Arka planda kod dosyası üç önemli sabit ve bir bit eşlem dizisi tanımlayarak başlar:
public partial class MainPage : ContentPage
{
const int COUNT = 10; // The number of bitmaps in the animation.
// This can go up to 50!
const int BITMAP_SIZE = 1000; // Program uses square bitmaps exclusively
// Uncomment just one of these, or define your own
static readonly Complex center = new Complex(-1.17651152924355, 0.298520986549558);
// static readonly Complex center = new Complex(-0.774693089457127, 0.124226621261617);
// static readonly Complex center = new Complex(-0.556624880053304, 0.634696788141351);
SKBitmap[] bitmaps = new SKBitmap[COUNT]; // array of bitmaps
···
}
Bir noktada, animasyonun COUNT
tüm aralığını görmek için büyük olasılıkla değeri 50 olarak değiştirmek isteyeceksiniz. 50'nin üzerindeki değerler yararlı değildir. 48 veya daha fazla yakınlaştırma düzeyinde çift duyarlıklı kayan noktalı sayıların çözünürlüğü Mandelbrot Kümesi hesaplaması için yetersiz hale gelir. Bu sorun, ile Xamarin.FormsMobil Uygulama Oluşturma sayfasının 684. sayfasında açıklandı.
Değer center
çok önemlidir. Bu, animasyon yakınlaştırmasının odağıdır. Dosyadaki üç değer, Sayfa 684'teki Mobile Apps Xamarin.Forms Oluşturma'nın 20. Bölümü'ndeki son üç ekran görüntüsünde kullanılanlardır, ancak bu bölümdeki programla denemeler yapabilir ve kendi değerlerinizden birini elde edebilirsiniz.
Mandelbrot Animasyon örneği bu COUNT
bit eşlemleri yerel uygulama depolama alanında depolar. Elli bit eşlem, cihazınızda 20 megabayttan fazla depolama alanı gerektirir, bu nedenle bu bit eşlemlerin ne kadar depolama alanı kapsadığından emin olmak isteyebilirsiniz ve bir noktada hepsini silmek isteyebilirsiniz. Sınıfın en altındaki bu iki yöntemin amacı da bu şekildedir MainPage
:
public partial class MainPage : ContentPage
{
···
void TallyBitmapSizes()
{
long fileSize = 0;
foreach (string filename in Directory.EnumerateFiles(FolderPath()))
{
fileSize += new FileInfo(filename).Length;
}
storageLabel.Text = $"Total storage: {fileSize:N0} bytes";
}
void OnDeleteButtonClicked(object sender, EventArgs args)
{
foreach (string filepath in Directory.EnumerateFiles(FolderPath()))
{
File.Delete(filepath);
}
TallyBitmapSizes();
}
}
Program bunları bellekte tuttuğundan, program aynı bit eşlemleri animasyonu oluştururken yerel depolamadaki bit eşlemleri silebilirsiniz. Ancak programı bir sonraki çalıştırmanızda bit eşlemleri yeniden oluşturması gerekir.
Yerel uygulama depolama alanında depolanan bit eşlemler, dosya adlarına değer ekler center
, dolayısıyla ayarı değiştirirseniz center
mevcut bit eşlemler depolama alanında değiştirilmez ve yer kaplamaya devam eder.
Dosya adlarını oluşturmak için kullanılan yöntemlerin MainPage
yanı MakePixel
sıra renk bileşenlerine dayalı bir piksel değeri tanımlama yöntemi aşağıdadır:
public partial class MainPage : ContentPage
{
···
// File path for storing each bitmap in local storage
string FolderPath() =>
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
string FilePath(int zoomLevel) =>
Path.Combine(FolderPath(),
String.Format("R{0}I{1}Z{2:D2}.png", center.Real, center.Imaginary, zoomLevel));
// Form bitmap pixel for Rgba8888 format
uint MakePixel(byte alpha, byte red, byte green, byte blue) =>
(uint)((alpha << 24) | (blue << 16) | (green << 8) | red);
···
}
zoomLevel
parametresi FilePath
0 COUNT
ile sabit eksi 1 arasında değişir.
MainPage
Oluşturucu yöntemini LoadAndStartAnimation
çağırır:
public partial class MainPage : ContentPage
{
···
public MainPage()
{
InitializeComponent();
LoadAndStartAnimation();
}
···
}
LoadAndStartAnimation
yöntemi, program daha önce çalıştırıldığında oluşturulmuş olabilecek bit eşlemleri yüklemek için uygulama yerel depolamasına erişmekle sorumludur. 0 COUNT
ile arasında zoomLevel
döngüler oluşturur. Dosya varsa, diziye bitmaps
yükler. Aksi takdirde, çağrısı Mandelbrot.CalculateAsync
yaparak belirli center
ve zoomLevel
değerler için bir bit eşlem oluşturması gerekir. Bu yöntem her piksel için yineleme sayılarını elde eder ve bu yöntem renklere dönüştürülür:
public partial class MainPage : ContentPage
{
···
async void LoadAndStartAnimation()
{
// Show total bitmap storage
TallyBitmapSizes();
// Create progressReporter for async operation
Progress<double> progressReporter =
new Progress<double>((double progress) => progressBar.Progress = progress);
// Create (unused) CancellationTokenSource for async operation
CancellationTokenSource cancelTokenSource = new CancellationTokenSource();
// Loop through all the zoom levels
for (int zoomLevel = 0; zoomLevel < COUNT; zoomLevel++)
{
// If the file exists, load it
if (File.Exists(FilePath(zoomLevel)))
{
statusLabel.Text = $"Loading bitmap for zoom level {zoomLevel}";
using (Stream stream = File.OpenRead(FilePath(zoomLevel)))
{
bitmaps[zoomLevel] = SKBitmap.Decode(stream);
}
}
// Otherwise, create a new bitmap
else
{
statusLabel.Text = $"Creating bitmap for zoom level {zoomLevel}";
CancellationToken cancelToken = cancelTokenSource.Token;
// Do the (generally lengthy) Mandelbrot calculation
BitmapInfo bitmapInfo =
await Mandelbrot.CalculateAsync(center,
4 / Math.Pow(2, zoomLevel),
4 / Math.Pow(2, zoomLevel),
BITMAP_SIZE, BITMAP_SIZE,
(int)Math.Pow(2, 10), progressReporter, cancelToken);
// Create bitmap & get pointer to the pixel bits
SKBitmap bitmap = new SKBitmap(BITMAP_SIZE, BITMAP_SIZE, SKColorType.Rgba8888, SKAlphaType.Opaque);
IntPtr basePtr = bitmap.GetPixels();
// Set pixel bits to color based on iteration count
for (int row = 0; row < bitmap.Width; row++)
for (int col = 0; col < bitmap.Height; col++)
{
int iterationCount = bitmapInfo.IterationCounts[row * bitmap.Width + col];
uint pixel = 0xFF000000; // black
if (iterationCount != -1)
{
double proportion = (iterationCount / 32.0) % 1;
byte red = 0, green = 0, blue = 0;
if (proportion < 0.5)
{
red = (byte)(255 * (1 - 2 * proportion));
blue = (byte)(255 * 2 * proportion);
}
else
{
proportion = 2 * (proportion - 0.5);
green = (byte)(255 * proportion);
blue = (byte)(255 * (1 - proportion));
}
pixel = MakePixel(0xFF, red, green, blue);
}
// Calculate pointer to pixel
IntPtr pixelPtr = basePtr + 4 * (row * bitmap.Width + col);
unsafe // requires compiling with unsafe flag
{
*(uint*)pixelPtr.ToPointer() = pixel;
}
}
// Save as PNG file
SKData data = SKImage.FromBitmap(bitmap).Encode();
try
{
File.WriteAllBytes(FilePath(zoomLevel), data.ToArray());
}
catch
{
// Probably out of space, but just ignore
}
// Store in array
bitmaps[zoomLevel] = bitmap;
// Show new bitmap sizes
TallyBitmapSizes();
}
// Display the bitmap
bitmapIndex = zoomLevel;
canvasView.InvalidateSurface();
}
// Now start the animation
stopwatch.Start();
Device.StartTimer(TimeSpan.FromMilliseconds(16), OnTimerTick);
}
···
}
Programın bu bit eşlemleri cihazın fotoğraf kitaplığı yerine yerel uygulama depolama alanında depoladığını fark edin. .NET Standard 2.0 kitaplığı, bu görev için tanıdık File.OpenRead
ve File.WriteAllBytes
yöntemlerin kullanılmasına izin verir.
Tüm bit eşlemler oluşturulduktan veya belleğe yüklendikten sonra yöntemi bir Stopwatch
nesnesi başlatır ve çağırır Device.StartTimer
. OnTimerTick
yöntemi her 16 milisaniyede bir çağrılır.
OnTimerTick
0 ile 6000 kez COUNT
arasında değişen milisaniye cinsinden bir time
değer hesaplar. Bu değer, her bit eşlemin görüntülenmesi için altı saniyelik bir değerdir. progress
değeri, döngünün Math.Sin
başında daha yavaş ve yönü tersine çevirdiği için sonunda daha yavaş olacak bir sinüsoid animasyon oluşturmak için değerini kullanır.
progress
Değer 0 COUNT
ile arasında değişir. Bu, öğesinin tamsayı bölümünün progress
dizideki bitmaps
bir dizin olduğu, kesirli bölümünün progress
ise söz konusu bit eşlem için yakınlaştırma düzeyini gösterdiği anlamına gelir. Bu değerler ve alanlarında depolanır bitmapIndex
ve tarafından Label
ve Slider
XAML dosyasında bitmapProgress
görüntülenir. SKCanvasView
bit eşlem görüntüsünü güncelleştirmek için geçersiz kılındı:
public partial class MainPage : ContentPage
{
···
Stopwatch stopwatch = new Stopwatch(); // for the animation
int bitmapIndex;
double bitmapProgress = 0;
···
bool OnTimerTick()
{
int cycle = 6000 * COUNT; // total cycle length in milliseconds
// Time in milliseconds from 0 to cycle
int time = (int)(stopwatch.ElapsedMilliseconds % cycle);
// Make it sinusoidal, including bitmap index and gradation between bitmaps
double progress = COUNT * 0.5 * (1 + Math.Sin(2 * Math.PI * time / cycle - Math.PI / 2));
// These are the field values that the PaintSurface handler uses
bitmapIndex = (int)progress;
bitmapProgress = progress - bitmapIndex;
// It doesn't often happen that we get up to COUNT, but an exception would be raised
if (bitmapIndex < COUNT)
{
// Show progress in UI
statusLabel.Text = $"Displaying bitmap for zoom level {bitmapIndex}";
progressBar.Progress = bitmapProgress;
// Update the canvas
canvasView.InvalidateSurface();
}
return true;
}
···
}
Son olarak, PaintSurface
işleyicisi SKCanvasView
en boy oranını korurken bit eşlemi olabildiğince büyük görüntülemek için bir hedef dikdörtgen hesaplar. Kaynak dikdörtgen değeri temel bitmapProgress
alır. fraction
Burada hesaplanan değer, 0 olduğunda bitmapProgress
bit eşlemin tamamını görüntülemek için 0'dan 0,25'e, bit eşlem genişliğinin ve yüksekliğinin yarısını görüntülemek için 0,25'e bitmapProgress
kadar değişir ve etkin bir şekilde yakınlaştırılır:
public partial class MainPage : ContentPage
{
···
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
if (bitmaps[bitmapIndex] != null)
{
// Determine destination rect as square in canvas
int dimension = Math.Min(info.Width, info.Height);
float x = (info.Width - dimension) / 2;
float y = (info.Height - dimension) / 2;
SKRect destRect = new SKRect(x, y, x + dimension, y + dimension);
// Calculate source rectangle based on fraction:
// bitmapProgress == 0: full bitmap
// bitmapProgress == 1: half of length and width of bitmap
float fraction = 0.5f * (1 - (float)Math.Pow(2, -bitmapProgress));
SKBitmap bitmap = bitmaps[bitmapIndex];
int width = bitmap.Width;
int height = bitmap.Height;
SKRect sourceRect = new SKRect(fraction * width, fraction * height,
(1 - fraction) * width, (1 - fraction) * height);
// Display the bitmap
canvas.DrawBitmap(bitmap, sourceRect, destRect);
}
}
···
}
Çalışan program şu şekildedir:
GIF animasyonu
Grafik Değişim Biçimi (GIF) belirtimi, tek bir GIF dosyasının bir sahnenin birden çok sıralı çerçevesini içermesine olanak tanıyan ve genellikle döngüde, ardışık olarak görüntülenebilen bir özellik içerir. Bu dosyalar animasyonlu GIF'ler olarak bilinir. Web tarayıcıları animasyonlu GIF'leri yürütebilir ve SkiaSharp bir uygulamanın animasyonlu GIF dosyasından kareleri ayıklamasına ve bunları sırayla görüntülemesine olanak tanır.
Örnek, DemonDeLuxe tarafından oluşturulan ve Wikipedia'daki Newton'un Beşiği sayfasından indirilen Newtons_cradle_animation_book_2.gif adlı animasyonlu bir GIF kaynağı içerir. Animasyonlu GIF sayfası, bu bilgileri sağlayan ve bir örneği oluşturan bir SKCanvasView
XAML dosyası içerir:
<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"
x:Class="SkiaSharpFormsDemos.Bitmaps.AnimatedGifPage"
Title="Animated GIF">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<skia:SKCanvasView x:Name="canvasView"
Grid.Row="0"
PaintSurface="OnCanvasViewPaintSurface" />
<Label Text="GIF file by DemonDeLuxe from Wikipedia Newton's Cradle page"
Grid.Row="1"
Margin="0, 5"
HorizontalTextAlignment="Center" />
</Grid>
</ContentPage>
Arka planda kod dosyası herhangi bir animasyonlu GIF dosyasını oynatmak için genelleştirilmemiştir. Kullanılabilir bilgilerin bazılarını, özellikle de yineleme sayısını yoksayar ve animasyonlu GIF'i döngüde oynatması yeterlidir.
SkisSharp'ın animasyonlu GIF dosyasının çerçevelerini ayıklamak için kullanılması herhangi bir yerde belgelenmemiştir, bu nedenle aşağıdaki kodun açıklaması normalden daha ayrıntılıdır:
Animasyonlu GIF dosyasının kodunun çözülmesi sayfanın oluşturucusunda gerçekleşir ve bit eşlem başvuran nesnenin Stream
bir nesne ve sonra bir SKManagedStream
SKCodec
nesne oluşturmak için kullanılmasını gerektirir. FrameCount
özelliği, animasyonu oluşturan kare sayısını gösterir.
Bu çerçeveler sonunda tek tek bit eşlemler olarak kaydedilir, bu nedenle oluşturucu her karenin süresi için iki int
dizi ve birikmiş süreleri (animasyon mantığını kolaylaştırmak için) bir tür SKBitmap
dizisi ayırmak için kullanırFrameCount
.
FrameInfo
sınıfının özelliğiSKCodec
, her çerçeve için bir değer dizisidirSKCodecFrameInfo
, ancak bu programın bu yapıdan Duration
aldığı tek şey milisaniye cinsinden çerçevedir.
SKCodec
türünde SKImageInfo
bir Info
özellik tanımlar, ancak bu SKImageInfo
değer renk türünün SKColorType.Index8
olduğunu (en azından bu resim için) belirtir. Bu, her pikselin bir renk türüne ait bir dizin olduğu anlamına gelir. Renk tablolarıyla uğraşmaktan kaçınmak için program, kendi tam renk ImageInfo
değerini oluşturmak için bu yapıdaki ve Height
bilgilerini kullanırWidth
. Her SKBitmap
biri bundan oluşturulur.
yöntemiSKBitmap
, GetPixels
bu bit eşlemin piksel bitlerine başvuran bir IntPtr
döndürür. Bu piksel bitleri henüz ayarlanmadı. Bu IntPtr
, yöntemlerinden GetPixels
SKCodec
birine geçirilir. Bu yöntem, GIF dosyasındaki çerçeveyi tarafından IntPtr
başvuruda bulunılan bellek alanına kopyalar. Oluşturucu çerçeve SKCodecOptions
numarasını gösterir:
public partial class AnimatedGifPage : ContentPage
{
SKBitmap[] bitmaps;
int[] durations;
int[] accumulatedDurations;
int totalDuration;
···
public AnimatedGifPage ()
{
InitializeComponent ();
string resourceID = "SkiaSharpFormsDemos.Media.Newtons_cradle_animation_book_2.gif";
Assembly assembly = GetType().GetTypeInfo().Assembly;
using (Stream stream = assembly.GetManifestResourceStream(resourceID))
using (SKManagedStream skStream = new SKManagedStream(stream))
using (SKCodec codec = SKCodec.Create(skStream))
{
// Get frame count and allocate bitmaps
int frameCount = codec.FrameCount;
bitmaps = new SKBitmap[frameCount];
durations = new int[frameCount];
accumulatedDurations = new int[frameCount];
// Note: There's also a RepetitionCount property of SKCodec not used here
// Loop through the frames
for (int frame = 0; frame < frameCount; frame++)
{
// From the FrameInfo collection, get the duration of each frame
durations[frame] = codec.FrameInfo[frame].Duration;
// Create a full-color bitmap for each frame
SKImageInfo imageInfo = code.new SKImageInfo(codec.Info.Width, codec.Info.Height);
bitmaps[frame] = new SKBitmap(imageInfo);
// Get the address of the pixels in that bitmap
IntPtr pointer = bitmaps[frame].GetPixels();
// Create an SKCodecOptions value to specify the frame
SKCodecOptions codecOptions = new SKCodecOptions(frame, false);
// Copy pixels from the frame into the bitmap
codec.GetPixels(imageInfo, pointer, codecOptions);
}
// Sum up the total duration
for (int frame = 0; frame < durations.Length; frame++)
{
totalDuration += durations[frame];
}
// Calculate the accumulated durations
for (int frame = 0; frame < durations.Length; frame++)
{
accumulatedDurations[frame] = durations[frame] +
(frame == 0 ? 0 : accumulatedDurations[frame - 1]);
}
}
}
···
}
IntPtr
değerine rağmen, unsafe
hiçbir zaman C# işaretçi değerine dönüştürülemediğinden kod gerekmezIntPtr
.
Her çerçeve ayıklandıktan sonra, oluşturucu tüm çerçevelerin sürelerini toplar ve sonra birikmiş sürelerle başka bir dizi başlatır.
Arka planda kod dosyasının geri kalanı animasyona ayrılmıştır. Device.StartTimer
yöntemi bir süreölçer başlatmak için kullanılır ve OnTimerTick
geri çağırma milisaniye cinsinden geçen süreyi belirlemek için bir Stopwatch
nesnesi kullanır. Birikmiş süreler dizisinde döngü yapmak geçerli çerçeveyi bulmak için yeterlidir:
public partial class AnimatedGifPage : ContentPage
{
SKBitmap[] bitmaps;
int[] durations;
int[] accumulatedDurations;
int totalDuration;
Stopwatch stopwatch = new Stopwatch();
bool isAnimating;
int currentFrame;
···
protected override void OnAppearing()
{
base.OnAppearing();
isAnimating = true;
stopwatch.Start();
Device.StartTimer(TimeSpan.FromMilliseconds(16), OnTimerTick);
}
protected override void OnDisappearing()
{
base.OnDisappearing();
stopwatch.Stop();
isAnimating = false;
}
bool OnTimerTick()
{
int msec = (int)(stopwatch.ElapsedMilliseconds % totalDuration);
int frame = 0;
// Find the frame based on the elapsed time
for (frame = 0; frame < accumulatedDurations.Length; frame++)
{
if (msec < accumulatedDurations[frame])
{
break;
}
}
// Save in a field and invalidate the SKCanvasView.
if (currentFrame != frame)
{
currentFrame = frame;
canvasView.InvalidateSurface();
}
return isAnimating;
}
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear(SKColors.Black);
// Get the bitmap and center it
SKBitmap bitmap = bitmaps[currentFrame];
canvas.DrawBitmap(bitmap,info.Rect, BitmapStretch.Uniform);
}
}
Değişken her değiştiğinde currentframe
geçersiz SKCanvasView
kılınıp yeni çerçeve görüntülenir:
Tabii ki, animasyonu görmek için programı kendiniz çalıştırmak isteyeceksiniz.