Skip to content

Commit

Permalink
Add animated GIF support for Image
Browse files Browse the repository at this point in the history
  • Loading branch information
mattleibow committed Jan 10, 2024
1 parent e1e0328 commit 744aa56
Show file tree
Hide file tree
Showing 16 changed files with 503 additions and 67 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,15 @@
Minimum="0"
Maximum="60"
Value =" 10"/>
<Label
Text="Animated GIF"
Style="{StaticResource Headline}"/>
<ImageButton
x:Name="AnimatedGifImage"
Source="animated_heart.gif" />
<Button
Text="Use Online Source"
Clicked="UseOnlineSource_Clicked"/>
</VerticalStackLayout>
</ScrollView>
</views:BasePage.Content>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ void OnResizeImageButtonClicked(object sender, EventArgs e)
ResizeImageButton.HeightRequest = 100;
ResizeImageButton.WidthRequest = 100;
}

void UseOnlineSource_Clicked(object sender, EventArgs e)
{
AnimatedGifImage.Source =
ImageSource.FromUri(new Uri("https://news.microsoft.com/wp-content/uploads/prod/2022/07/hexagon_print.gif"));
}
}

public class ImageButtonPageViewModel : BindableObject
Expand Down
13 changes: 13 additions & 0 deletions src/Controls/samples/Controls.Sample/Pages/Controls/ImagePage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,19 @@
Background="Black"
Source="dotnet_bot.png"
/>
<Label
Text="Animated GIF"
Style="{StaticResource Headline}"/>
<Image
x:Name="AnimatedGifImage"
Source="animated_heart.gif"
IsAnimationPlaying="True" />
<Button
Text="Start/Stop"
Clicked="AnimationStartStop_Clicked"/>
<Button
Text="Use Online Source"
Clicked="UseOnlineSource_Clicked"/>
</VerticalStackLayout>
</ScrollView>
</views:BasePage.Content>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,16 @@ protected override void OnAppearing()
var imageBytes = Convert.FromBase64String(Base64EncodedImage);
StreamSourceImage.Source = ImageSource.FromStream(() => new MemoryStream(imageBytes));
}

void AnimationStartStop_Clicked(object sender, EventArgs e)
{
AnimatedGifImage.IsAnimationPlaying = !AnimatedGifImage.IsAnimationPlaying;
}

void UseOnlineSource_Clicked(object sender, EventArgs e)
{
AnimatedGifImage.Source =
ImageSource.FromUri(new Uri("https://news.microsoft.com/wp-content/uploads/prod/2022/07/hexagon_print.gif"));
}
}
}
21 changes: 18 additions & 3 deletions src/Core/src/Handlers/Image/ImageHandler.iOS.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#nullable enable
using System.Threading.Tasks;
using System.Threading.Tasks;
using UIKit;

namespace Microsoft.Maui.Handlers
Expand Down Expand Up @@ -51,7 +50,23 @@ public override void SetImageSource(UIImage? platformImage)
if (Handler?.PlatformView is not UIImageView imageView)
return;

imageView.Image = platformImage;
if (platformImage?.Images is not null)
{
imageView.Image = platformImage.Images[0];

imageView.AnimationImages = platformImage.Images;
imageView.AnimationDuration = platformImage.Duration;
}
else
{
imageView.AnimationImages = null;
imageView.AnimationDuration = 0.0;

imageView.Image = platformImage;
}

Handler?.UpdateValue(nameof(IImage.IsAnimationPlaying));

if (Handler?.VirtualView is IImage image && image.Source is IStreamImageSource)
imageView.InvalidateMeasure(image);
}
Expand Down
3 changes: 3 additions & 0 deletions src/Core/src/Handlers/ImageButton/ImageButtonHandler.iOS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ public override void SetImageSource(UIImage? platformImage)
if (Handler?.PlatformView is not UIButton button)
return;

if (platformImage?.Images is not null && platformImage.Images.Length > 0)
platformImage = platformImage.Images[0];

platformImage = platformImage?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal);

button.SetImage(platformImage, UIControlState.Normal);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@ public partial class FileImageSourceService

try
{
var image = imageSource.GetPlatformImage();

if (image == null)
using var cgImageSource = imageSource.GetPlatformImageSource();
if (cgImageSource is null)
throw new InvalidOperationException("Unable to load image file.");

var image = cgImageSource.GetPlatformImage();

var result = new ImageSourceServiceResult(image, () => image.Dispose());

return FromResult(result);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,12 @@ public partial class StreamImageSourceService

try
{
var stream = await imageSource.GetStreamAsync(cancellationToken).ConfigureAwait(false);
using var cgImageSource =
await imageSource.GetPlatformImageSourceAsync(cancellationToken).ConfigureAwait(false);
if (cgImageSource is null)
throw new InvalidOperationException("Unable to load image file.");

if (stream == null)
throw new InvalidOperationException("Unable to load image stream.");

using var data = NSData.FromStream(stream);
if (data == null)
throw new InvalidOperationException("Unable to load image stream data.");

// We do not need to pass the scale in here as the image file is not scaled to the screen scale.
var image = UIImage.LoadFromData(data);

if (image == null)
throw new InvalidOperationException("Unable to decode image from stream.");
var image = cgImageSource.GetPlatformImage();

var result = new ImageSourceServiceResult(image, () => image.Dispose());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace Microsoft.Maui
{
public partial class UriImageSourceService
{
internal string CacheDirectory = Path.Combine(FileSystem.CacheDirectory, "com.microsoft.maui", "MauiUriImages");
internal static readonly string CacheDirectory = Path.Combine(FileSystem.CacheDirectory, "com.microsoft.maui", "MauiUriImages");

public override Task<IImageSourceServiceResult<UIImage>?> GetImageAsync(IImageSource imageSource, float scale = 1, CancellationToken cancellationToken = default) =>
GetImageAsync((IUriImageSource)imageSource, scale, cancellationToken);
Expand All @@ -24,40 +24,13 @@ public partial class UriImageSourceService

try
{
var hash = Crc64.ComputeHashString(imageSource.Uri.OriginalString);
var pathToImageCache = CacheDirectory + hash + ".png";
var imageData = await DownloadAndCacheImageAsync(imageSource, cancellationToken);

NSData? imageData;

if (imageSource.CachingEnabled && IsImageCached(pathToImageCache))
{
imageData = GetCachedImage(pathToImageCache);
}
else
{
// TODO: use a real caching library with the URI
if (imageSource is not IStreamImageSource streamImageSource)
return null;

using var stream = await streamImageSource.GetStreamAsync(cancellationToken).ConfigureAwait(false);

if (stream == null)
throw new InvalidOperationException($"Unable to load image stream from URI '{imageSource.Uri}'.");

imageData = NSData.FromStream(stream);

if (imageData == null)
throw new InvalidOperationException("Unable to load image stream data.");

if (imageSource.CachingEnabled)
CacheImage(imageData, pathToImageCache);
}

// We do not need to pass the scale in here as the image file is not scaled to the screen scale.
var image = UIImage.LoadFromData(imageData);

if (image == null)
throw new InvalidOperationException($"Unable to decode image from URI '{imageSource.Uri}'.");
using var cgImageSource = imageData.GetPlatformImageSource();
if (cgImageSource is null)
throw new InvalidOperationException("Unable to load image file.");

var image = cgImageSource.GetPlatformImage();

var result = new ImageSourceServiceResult(image, () => image.Dispose());

Expand All @@ -70,6 +43,48 @@ public partial class UriImageSourceService
}
}

internal async Task<NSData> DownloadAndCacheImageAsync(IUriImageSource imageSource, CancellationToken cancellationToken)
{
// TODO: use a real caching library with the URI

var hash = Crc64.ComputeHashString(imageSource.Uri.OriginalString);
var ext = Path.GetExtension(imageSource.Uri.OriginalString);
var filename = $"{hash}{ext}";
var pathToImageCache = Path.Combine(CacheDirectory, filename);

NSData? imageData;

if (imageSource.CachingEnabled && IsImageCached(pathToImageCache))
{
imageData = GetCachedImage(pathToImageCache);
}
else
{
imageData = await DownloadImageAsync(imageSource, cancellationToken);
if (imageSource.CachingEnabled)
CacheImage(imageData, pathToImageCache);
}

return imageData;
}

internal async Task<NSData> DownloadImageAsync(IUriImageSource imageSource, CancellationToken cancellationToken)
{
if (imageSource is not IStreamImageSource streamImageSource)
throw new InvalidOperationException($"Unable to load image stream from image source type '{imageSource.GetType()}'.");

using var stream = await streamImageSource.GetStreamAsync(cancellationToken).ConfigureAwait(false);
if (stream is null)
throw new InvalidOperationException($"Unable to load image stream from URI '{imageSource.Uri}'.");

var imageData = NSData.FromStream(stream);

if (imageData is null)
throw new InvalidOperationException("Unable to load image stream data.");

return imageData;
}

public void CacheImage(NSData imageData, string path)
{
var directory = Path.GetDirectoryName(path);
Expand Down Expand Up @@ -102,5 +117,4 @@ public NSData GetCachedImage(string path)
return imageData;
}
}

}
}
Loading

0 comments on commit 744aa56

Please sign in to comment.