Windows 8 – GIF animations … the RIGHT way

Long time since my last blog post – but hey, I’ve been busy. Busy keeping up with all the exciting Microsoft Technologies. Especially the Microsoft Windows Phone, for which I’ve been (and still are) developing “a soon to be launched” app.

In my Windows Phone app, I needed to be able to show animated images on hub tiles (using GIF images). No controls for that 🙁 However, lucky for me – I have the Windows Phone component from ComponentOne, that features such control. Though, I’ve been struggeling with memory leaks with this. But this might very well soon be history.

Last week I finally switched my IPad to a Windows Surface RT (why not the Pro? Well, I don’t need a Pro – in these scenarios by Lenovo notebook is ok). Developing an app for Windows Phone using MVVM Light, I thought … well why not develop a Windows 8 store app at the same time. What a nice idea, especially when I can reuse 90% of my code base and I only have to redo the views.

The Windows 8 – Windows RT – GIF nightmare begins …

But what about my GIF control?   No such thing on Windows 8 🙁   Searching the web, I came across several postings stating:  “Use the WebView control”.   Not being to happy about this, I tried it.   In 10 minutes  I had a decent mockup running and with a little tweaking,  it worked … I thought.   With great fear, I discovered that when swipping the page, the WebViewer lacked behind and even worse, it was displayed above my main heading.   Further more it had some loading delays, showing the default whte background color.   This was a no-go for me.

Here’s my first mockup draft, using the WebView control – don’t go down this way.  I’ve been here!   Below you find the code (XAML not included, this just contains a WebViewer control with the name “web”):

 

  public sealed partial class GifImage : UserControl
  {
    private static readonly DependencyProperty _imageUrlProperty = DependencyProperty.Register
    (
        "ImageUrl", typeof(string), typeof(GifImage), new PropertyMetadata(String.Empty, ImageUrlPropertyChanged)
    );


    #region Private Properties
    private double HtmlWidth
    {
      get;
      set;
    }

    private double HtmlHeight
    {
      get;
      set;
    }

    private string Html
    {
      get 
      {
        return String.Format("<html><body style=\"background: url('ms-appx-web://{0}') no-repeat scroll;background-size: {1} {2};\"/></html>", ImageUrl, MakeSize(HtmlWidth), MakeSize(HtmlHeight));
      }
    }
    #endregion

    #region Public Properties
    public string ImageUrl
    {
      get
      {
        return (string)GetValue(_imageUrlProperty);
      }
      set
      {
        SetValue(_imageUrlProperty, value);
      }
    }
    #endregion

    #region Constructors
    public GifImage()
    {
      this.InitializeComponent();
    }
    #endregion

    #region Private Methods
    private string MakeSize(double val)
    {
      return String.Format("{0}px", (int)val);
    }

    private void WebSizeChanged(object sender, SizeChangedEventArgs e)
    {
      HtmlWidth = e.NewSize.Width;
      HtmlHeight = e.NewSize.Height;

      RefreshWebView();
    }

    private void RefreshWebView()
    {
      web.Visibility = Windows.UI.Xaml.Visibility.Collapsed;
      web.NavigateToString(Html);
    }

    private static void ImageUrlPropertyChanged(object sender, DependencyPropertyChangedEventArgs args)
    {
      var control = (GifImage)sender;

      control.ImageUrl = (string)args.NewValue;
      control.RefreshWebView();
    }
    #endregion

    private void web_LoadCompleted(object sender, NavigationEventArgs e)
    {
      web.Visibility = Windows.UI.Xaml.Visibility.Visible;
    }
  }

Going Gif’ish – the right way

So I thought: “How hard can it be, to do a control yourself?” and after a couple of hours of hacking I finally made it work.   The secret is the BitmapDecoder class, that supports decoding GIF files, reading these into WriteableBitmap’s that an be shown in a storyboard animation. That’s it – and look ma’ – no browser control!

I’ve encapsulated the functionality in a user control (and I’ll most certainly create a Windows Phone version in near future).   There’s of course always room for improvement, but see this as an example.  Demonstrating features, that new Windows 8 developers easily can get stuck wih – such as how to read a local file, creating storyboard iems in code, using dependency obcts and so forth.

Please don’t copy this code to other sites, but rather link to this post.  Thanks!

The XAML part:  AnimationImage.xaml

<UserControl x:Class="TriGemini.Controls.AnimationImage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:MvvmLight4" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400">
    
    <Grid>
        <Grid.Resources>
            <Storyboard x:Name="storyboard" RepeatBehavior="Forever"/>
        </Grid.Resources>
        <Image x:Name="image"/>
    </Grid>
</UserControl>

The code-behind part: AnimationImage.xaml.cs

 

using System;
using System.Collections.Generic;
using System.Linq;
using Windows.Graphics.Imaging;
using Windows.Storage;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media.Imaging;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.UI.Xaml.Media.Animation;

namespace TriGemini.Controls
{
  /// 
<summary>
  ///  AnimationImage control
  ///  Written By Henrik Brinch (TriGemini)
  ///  www.trigemini.dk / www.henrikbrinch.dk
  ///  Check out www.linkedin.com/in/trigemini
  ///  
  /// License terms:  Feel free to use the code in your own projects, however if distributing
  /// the source code, you must preserve this heading.
  /// 
  /// Please do not post this code on other sites, but link to this blog-post instead.
  /// If you really find it usefull and use it in commercial application, I'd like to hear about
  /// it - and you're very welcome to mention me your application credits 🙂
  /// </summary>

  public sealed partial class AnimationImage : UserControl
  {
    #region Private Fields
    private static readonly DependencyProperty _imageUrlProperty = DependencyProperty.Register
    (
      "ImageUrl", typeof(string), typeof(AnimationImage), new PropertyMetadata(String.Empty, ImageUrlPropertyChanged)
    );

    private readonly List<WriteableBitmap> _bitmapFrames = new List<WriteableBitmap>();
    private bool _playOnLoad = true;
    #endregion

    #region Public Properties
    /// 
<summary>
    /// Gets or sets the url of the image e.g. "/Assets/MyAnimation.gif"
    /// </summary>

    public string ImageUrl
    {
      get
      {
        return (string)GetValue(_imageUrlProperty);
      }
      set
      {
        SetValue(_imageUrlProperty, value);
      }
    }

    /// 
<summary>
    /// Gets the number of frames in the gif animation
    /// </summary>

    public uint FrameCount
    {
      get;
      private set;
    }

    public bool PlayOnLoad
    {
      get
      {
        return _playOnLoad;
      }
      set
      {
        _playOnLoad = value;
      }
    }
    #endregion

    #region Constructors
    public AnimationImage()
    {
      this.InitializeComponent();
    }
    #endregion

    #region Private Methods
    private async void LoadImage()
    {
      if(String.IsNullOrEmpty(ImageUrl))
      {
        return;
      }

      try
      {
        // Get the file e.g. "/Assets/MyAnimation.gif"
        var storageFile = await StorageFile.GetFileFromApplicationUriAsync(new Uri(String.Format("ms-appx://{0}", ImageUrl)));

        using(var res = await storageFile.OpenAsync(FileAccessMode.Read))
        {
          // Get the GIF decoder, to perform the magic
          var decoder = await BitmapDecoder.CreateAsync(BitmapDecoder.GifDecoderId, res);
          
          // Now we know the number of frames
          FrameCount = decoder.FrameCount;

          //  Extract each frame and create a WriteableBitmap for each of these (store them in an internal list)
          for (uint frameIndex = 0; frameIndex < FrameCount; frameIndex++) { var frame = await decoder.GetFrameAsync(frameIndex); var writeableBitmap = new WriteableBitmap((int)decoder.OrientedPixelWidth, (int)decoder.OrientedPixelHeight); // Extract the pixel data and fill the WriteableBitmap with them var bitmapTransform = new BitmapTransform(); var pixelDataProvider = await frame.GetPixelDataAsync(BitmapPixelFormat.Bgra8, decoder.BitmapAlphaMode, bitmapTransform, ExifOrientationMode.IgnoreExifOrientation, ColorManagementMode.DoNotColorManage); var pixels = pixelDataProvider.DetachPixelData(); using (var stream = writeableBitmap.PixelBuffer.AsStream()) { stream.Write(pixels, 0, pixels.Length); } // Finally we have a frame (WriteableBitmap) that can internally be stored. _bitmapFrames.Add(writeableBitmap); } } // Fill out the story board for the animation magic BuildStoryBoard(); // Start the animation if needed and fire the event if(PlayOnLoad) { storyboard.Begin(); if(ImageLoaded != null) { ImageLoaded(this, null); } } } catch { // Yeah, I know this is kinda' "cowboyish" - but hey, I don't want it to fail in the designer! if (!Windows.ApplicationModel.DesignMode.DesignModeEnabled) { throw; } } } private void BuildStoryBoard() { // Clear the story board, if it has previously been filled if(storyboard.Children.Count > 0)
      {
        storyboard.Children.Clear();
      }

      //  Now create the animation as a set of ObjectAnimationUsingKeyFrames (I love this name!)
      var anim = new ObjectAnimationUsingKeyFrames();
      anim.BeginTime = TimeSpan.FromSeconds(0);

      var ts = new TimeSpan();
      var speed = TimeSpan.FromMilliseconds(100); // Standard GIF framerate 10 fps?

      // Create each DiscreteObjectKeyFrame and advance the KeyTime by 100 ms (=10 fps) and add it to 
      // the storyboard.
      for (int frameIndex = 0; frameIndex < _bitmapFrames.Count; frameIndex++)
      {
        var keyFrame = new DiscreteObjectKeyFrame();

        keyFrame.KeyTime = KeyTime.FromTimeSpan(ts);
        keyFrame.Value = _bitmapFrames[frameIndex];

        ts = ts.Add(speed);
        anim.KeyFrames.Add(keyFrame);
      }

      //  Connect the image control with the story board

      Storyboard.SetTarget(anim, image);
      Storyboard.SetTargetProperty(anim, "Source");

      //  And finally add the animation-set to the storyboard
      storyboard.Children.Add(anim);
    }

    private static void ImageUrlPropertyChanged(object sender, DependencyPropertyChangedEventArgs args)
    {
      var control = (AnimationImage)sender;
      control.LoadImage();
    }
    #endregion

    #region Public Events
    /// 
<summary>
    /// Fired whenever the image has loaded
    /// </summary>

    public EventHandler ImageLoaded;
    #endregion
  }
}

Example usage:

<TriGemini:AnimationImage ImageUrl="/Assets/18.gif" Width="100" Height="100"/>