Wednesday, January 16, 2013

Animating an Image in WPF

How to animate an Image in WPF?

In WPF, the Image control is a lightweight control that always display the first frame of the image file. If you are displaying a GIF file with multiple frames, you will notice that Image control only display the first image frame of that file.

This is because WPF drives this control to be more specific and let the user do its own technique how to do the animation. Also, this is the reason why WPF developers introduced a thing/classes of type Animation. The most common class that address this issue is the Int32Animation class of System.Windows.Media.Animation namespace.

Below are our explanation how to animate GIF file in WPF Image control.

First you need to create a UserControl that inherits the Image (from System.Windows.Controls) namespace. See the codes below.

In XAML

<Image x:Class="CodesDirectory.Controls.AnimatableImage"
       xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
       xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
       xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
       Stretch="Fill">
</Image>

In Code Behind

namespace CodesDirectory.Controls
{
    public partial class AnimatableImage : Image
    {
        public AnimatableImage()
        {
            InitializeComponent();
        }
    }
}

In order for you to determine what frame are you going to display in a span of time, you need to get the frame counts and the Image object per frame. So all you need to do is a variable/property that will handle the frame count values.

public int FrameCount { get; private set; }

private bool IsAnimating { getprivate set; }

private bool IsFramesInitiated { get; set; }

You will be using FrameCount property to handle the value of how many frames are there in the image, you will be using IsFramesInitiated property to handle a value whether the frames already initiated and you will be using IsAnimating property to handle a value whether the current image is animating.

The codes below will address how to get the frames and its length.

private void InitializeImageFrames()
{
    BitmapFrame bf = this.Source as BitmapFrame;
    if (object.ReferenceEquals(bf, null)) bf = BitmapFrame.Create((BitmapSource)this.Source);
    if (!object.ReferenceEquals(bf.Decoder, null))
    {
        this.FrameCount = bf.Decoder.Frames.Count;
        this.IsFramesInitiated = true;
    }
}

With the help of BitmapFrame (of System.Windows.Media.Imaging) object, we can drill down how many frame the image object has.

After that, you need to declare a new DependencyProperty that implements a PropertyMetaData to listen the events made on every changed. Below is the code on how to do it.

private static readonly DependencyProperty CurrentFrameImageProperty =
    DependencyProperty.Register("CurrentFrameImage",
    typeof(int),
    typeof(Image),
    new PropertyMetadata(0, OnCurrentFrameImageChanged));

You will notice there is a callback named OnCurrentFrameImageChanged. You need to create a new method with proper signature, this method will be triggered everytime the value of this property changed.

private static void OnCurrentFrameImageChanged(DependencyObject dpo, DependencyPropertyChangedEventArgs e)
{
    Image animatedImage = (Image)dpo;
    animatedImage.Source = ((BitmapFrame)animatedImage.Source).Decoder.Frames[(int)e.NewValue];
}

So what the code is doing there is, in every moment this event is triggered, its loaded the image object base on the current frame index handled.  Thank you for BitmapImage (of System.Windows.Media.Imaging)class.

Also, you need to  provide a facility to check whether the Source property has been changed. In this case, you need to override the OnPropertyChanged event and check whether the property Source has been changed. To do this, please see codes below.

protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
    if (object.Equals(e.Property, Image.SourceProperty))
    {
        this.InitializeImageFrames();
        base.OnPropertyChanged(e);
    }
}

To be more control like implementation, you should create two additional method to manage the animation effect of the control. Let's call them StartAnimate and StopAnimation.

StartAnimate Method

For StartAnimate method, please see the implementation below.

public void StartAnimate()
{
    if (!this.IsAnimating && this.IsFramesInitiated)
    {
        Int32Animation animation = new Int32Animation(0, this.FrameCount - 1,
            new Duration(TimeSpan.FromMilliseconds(AnimatableImage.TIME_MILLISECONDS_PER_IMAGEFRAME * this.FrameCount)));
        animation.RepeatBehavior = RepeatBehavior.Forever;
        this.BeginAnimation(CurrentFrameImageProperty, animation, HandoffBehavior.SnapshotAndReplace);
        this.IsAnimating = true;
    }
}

There, calling the BeginAnimation will do the job. You will notice there is a constant value named TIME_MILLISECONDS_PER_IMAGEFRAME. You can declare this at the top of your class as a constant with value of 75. There we also use the CurrentFrameImageProperty dependency property we created earlier to command that this property and its value will be changed and whatever event connected to it will be triggered. In this case, the PropertyMetaData that holds the signature of OnCurrentFrameImageChanged will be triggered.

If you notice that we set the repeat behavior to Forever as it will be looping the animation forever, as long as the user commands to start the animation. Only the user know when to stop it, that is the reason why we also need to create a corresponding StopAnimation method.

StopAnimation Method

For StopAnimation method, please see the implementation below.

public void StopAnimation()
{
    if (this.IsAnimating)
    {
        this.BeginAnimation(Image.SourceProperty, null);
        this.IsAnimating = false;
    }
}

Now, calling the BeginAnimation method again to set the property value of Source property to NULL will stop it.

Actual Control Implementation

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Media.Animation;

namespace CodesDirectory.Controls
{
    public partial class AnimatableImage : Image
    {
        #region Fields

        private static readonly DependencyProperty CurrentFrameImageProperty =
            DependencyProperty.Register("CurrentFrameImage",
            typeof(int),
            typeof(AnimatableImage),
            new PropertyMetadata(0, AnimatableImage.OnCurrentFrameImageChanged));

        #endregion

        #region Privates

        private const int TIME_MILLISECONDS_PER_IMAGEFRAME = 75;

        #endregion

        public AnimatableImage()
        {
            InitializeComponent();
        }

        #region Methods

        private void InitializeImageFrames()
        {
            if (object.ReferenceEquals(this.Source, null)) return;
            BitmapFrame bf = this.Source as BitmapFrame;
            if (object.ReferenceEquals(bf, null)) bf = BitmapFrame.Create((BitmapSource)this.Source);
            if (!object.ReferenceEquals(bf.Decoder, null))
            {
                this.FrameCount = bf.Decoder.Frames.Count;
                this.IsFramesInitiated = true;
            }
        }

        protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
        {
            if (object.Equals(e.Property, Image.SourceProperty))
            {
                this.InitializeImageFrames();
                base.OnPropertyChanged(e);
            }
        }

        public void StartAnimate()
        {
            if (!this.IsAnimating && this.IsFramesInitiated)
            {
                Int32Animation animation = new Int32Animation(0, this.FrameCount - 1, new Duration(TimeSpan.FromMilliseconds(AnimatableImage.TIME_MILLISECONDS_PER_IMAGEFRAME * this.FrameCount)));
                animation.RepeatBehavior = RepeatBehavior.Forever;
                this.BeginAnimation(CurrentFrameImageProperty, animation, HandoffBehavior.SnapshotAndReplace);
                this.IsAnimating = true;
            }
        }

        public void StopAnimation()
        {
            if (this.IsAnimating)
            {
                this.BeginAnimation(Image.SourceProperty, null);
                this.IsAnimating = false;
            }
        }

        private static void OnCurrentFrameImageChanged(DependencyObject dpo, DependencyPropertyChangedEventArgs e)
        {
            AnimatableImage image = (AnimatableImage)dpo;
            image.Source = ((BitmapFrame)image.Source).Decoder.Frames[(int)e.NewValue];
        }

        #endregion

        #region Properties

        public bool IsAnimating { get; private set; }

        public int FrameCount { get; private set; }

        private bool IsFramesInitiated { get; set; }

        #endregion
    }
}

For further details and implementation please visit Animating an Image in WPF using Timer.

No comments:

Post a Comment

Place your comments and ideas