diff --git a/WpfAnimatedGif.Demo/MainWindow.xaml b/WpfAnimatedGif.Demo/MainWindow.xaml
index 5a24b32..fcbe105 100644
--- a/WpfAnimatedGif.Demo/MainWindow.xaml
+++ b/WpfAnimatedGif.Demo/MainWindow.xaml
@@ -28,6 +28,8 @@
+
+
@@ -57,21 +59,51 @@
Click="btnOpenUrl_Click"/>
-
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -94,6 +126,8 @@
Stretch="None"
gif:ImageBehavior.AnimatedSource="{Binding SelectedImage}"
gif:ImageBehavior.RepeatBehavior="{Binding RepeatBehavior}"
+ gif:ImageBehavior.AnimationSpeedRatio="{Binding ActualSpeedRatio}"
+ gif:ImageBehavior.AnimationDuration="{Binding ActualDuration}"
gif:ImageBehavior.AutoStart="{Binding AutoStart}"
gif:ImageBehavior.AnimationCompleted="AnimationCompleted" />
diff --git a/WpfAnimatedGif.Demo/MainWindow.xaml.cs b/WpfAnimatedGif.Demo/MainWindow.xaml.cs
index 105c6ef..c6b4373 100644
--- a/WpfAnimatedGif.Demo/MainWindow.xaml.cs
+++ b/WpfAnimatedGif.Demo/MainWindow.xaml.cs
@@ -177,6 +177,80 @@ public RepeatBehavior RepeatBehavior
}
}
+ private double _speedRatio = 1.0;
+ public double SpeedRatio
+ {
+ get => _speedRatio;
+ set
+ {
+ _speedRatio = value;
+ OnPropertyChanged(nameof(SpeedRatio));
+ OnPropertyChanged(nameof(ActualSpeedRatio));
+ }
+ }
+
+ private Duration _duration = TimeSpan.FromSeconds(3);
+ public Duration Duration
+ {
+ get => _duration;
+ set
+ {
+ _duration = value;
+ OnPropertyChanged(nameof(Duration));
+ OnPropertyChanged(nameof(ActualDuration));
+ }
+ }
+
+ private bool _useDefaultDuration = true;
+ public bool UseDefaultDuration
+ {
+ get => _useDefaultDuration;
+ set
+ {
+ _useDefaultDuration = value;
+ OnPropertyChanged(nameof(UseDefaultDuration));
+ OnPropertyChanged(nameof(ActualDuration));
+ OnPropertyChanged(nameof(ActualSpeedRatio));
+ }
+ }
+
+ private bool _useSpeedRatio = false;
+ public bool UseSpeedRatio
+ {
+ get => _useSpeedRatio;
+ set
+ {
+ _useSpeedRatio = value;
+ if (value)
+ {
+ UseDuration = false;
+ }
+ OnPropertyChanged(nameof(UseSpeedRatio));
+ OnPropertyChanged(nameof(ActualSpeedRatio));
+ OnPropertyChanged(nameof(ActualDuration));
+ }
+ }
+
+ private bool _useDuration = false;
+ public bool UseDuration
+ {
+ get => _useDuration;
+ set
+ {
+ _useDuration = value;
+ if (value)
+ {
+ UseSpeedRatio = false;
+ }
+ OnPropertyChanged(nameof(UseDuration));
+ OnPropertyChanged(nameof(ActualDuration));
+ OnPropertyChanged(nameof(ActualSpeedRatio));
+ }
+ }
+
+ public Duration? ActualDuration => UseDuration ? Duration : default(Duration?);
+ public double? ActualSpeedRatio => UseSpeedRatio ? SpeedRatio : default(double?);
+
private bool _autoStart = true;
public bool AutoStart
{
diff --git a/WpfAnimatedGif/AnimationCache.cs b/WpfAnimatedGif/AnimationCache.cs
index 177a92f..b6babf4 100644
--- a/WpfAnimatedGif/AnimationCache.cs
+++ b/WpfAnimatedGif/AnimationCache.cs
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
+using System.Windows;
+using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
@@ -8,21 +10,18 @@ namespace WpfAnimatedGif
{
static class AnimationCache
{
- private class CacheKey
+ private struct CacheKey
{
private readonly ImageSource _source;
- private readonly RepeatBehavior _repeatBehavior;
- public CacheKey(ImageSource source, RepeatBehavior repeatBehavior)
+ public CacheKey(ImageSource source)
{
_source = source;
- _repeatBehavior = repeatBehavior;
}
private bool Equals(CacheKey other)
{
- return ImageEquals(_source, other._source)
- && Equals(_repeatBehavior, other._repeatBehavior);
+ return ImageEquals(_source, other._source);
}
public override bool Equals(object obj)
@@ -35,10 +34,7 @@ public override bool Equals(object obj)
public override int GetHashCode()
{
- unchecked
- {
- return (ImageGetHashCode(_source) * 397) ^ _repeatBehavior.GetHashCode();
- }
+ return ImageGetHashCode(_source);
}
private static int ImageGetHashCode(ImageSource image)
@@ -99,53 +95,67 @@ private static Uri GetUri(ImageSource image)
}
}
- private static readonly Dictionary _animationCache = new Dictionary();
- private static readonly Dictionary _referenceCount = new Dictionary();
+ private static readonly Dictionary _animationCache = new Dictionary();
+ private static readonly Dictionary> _imageControls = new Dictionary>();
- public static void IncrementReferenceCount(ImageSource source, RepeatBehavior repeatBehavior)
+ public static void AddControlForSource(ImageSource source, Image imageControl)
{
- var cacheKey = new CacheKey(source, repeatBehavior);
- int count;
- _referenceCount.TryGetValue(cacheKey, out count);
- count++;
- _referenceCount[cacheKey] = count;
+ var cacheKey = new CacheKey(source);
+ if (!_imageControls.TryGetValue(cacheKey, out var controls))
+ {
+ _imageControls[cacheKey] = controls = new HashSet();
+ }
+
+ controls.Add(imageControl);
}
- public static void DecrementReferenceCount(ImageSource source, RepeatBehavior repeatBehavior)
+ public static void RemoveControlForSource(ImageSource source, Image imageControl)
{
- var cacheKey = new CacheKey(source, repeatBehavior);
- int count;
- _referenceCount.TryGetValue(cacheKey, out count);
- if (count > 0)
+ var cacheKey = new CacheKey(source);
+ if (_imageControls.TryGetValue(cacheKey, out var controls))
{
- count--;
- _referenceCount[cacheKey] = count;
- }
- if (count == 0)
- {
- _animationCache.Remove(cacheKey);
- _referenceCount.Remove(cacheKey);
+ if (controls.Remove(imageControl))
+ {
+ if (controls.Count == 0)
+ {
+ _animationCache.Remove(cacheKey);
+ _imageControls.Remove(cacheKey);
+ }
+ }
}
}
- public static void AddAnimation(ImageSource source, RepeatBehavior repeatBehavior, ObjectAnimationUsingKeyFrames animation)
+ public static void Add(ImageSource source, AnimationCacheEntry entry)
{
- var key = new CacheKey(source, repeatBehavior);
- _animationCache[key] = animation;
+ var key = new CacheKey(source);
+ _animationCache[key] = entry;
}
- public static void RemoveAnimation(ImageSource source, RepeatBehavior repeatBehavior, ObjectAnimationUsingKeyFrames animation)
+ public static void Remove(ImageSource source)
{
- var key = new CacheKey(source, repeatBehavior);
+ var key = new CacheKey(source);
_animationCache.Remove(key);
}
- public static ObjectAnimationUsingKeyFrames GetAnimation(ImageSource source, RepeatBehavior repeatBehavior)
+ public static AnimationCacheEntry Get(ImageSource source)
+ {
+ var key = new CacheKey(source);
+ _animationCache.TryGetValue(key, out var entry);
+ return entry;
+ }
+ }
+
+ internal class AnimationCacheEntry
+ {
+ public AnimationCacheEntry(ObjectKeyFrameCollection keyFrames, Duration duration, int repeatCountFromMetadata)
{
- var key = new CacheKey(source, repeatBehavior);
- ObjectAnimationUsingKeyFrames animation;
- _animationCache.TryGetValue(key, out animation);
- return animation;
+ KeyFrames = keyFrames;
+ Duration = duration;
+ RepeatCountFromMetadata = repeatCountFromMetadata;
}
+
+ public ObjectKeyFrameCollection KeyFrames { get; }
+ public Duration Duration { get; }
+ public int RepeatCountFromMetadata { get; }
}
}
\ No newline at end of file
diff --git a/WpfAnimatedGif/ImageBehavior.cs b/WpfAnimatedGif/ImageBehavior.cs
index 27063df..e74afc8 100644
--- a/WpfAnimatedGif/ImageBehavior.cs
+++ b/WpfAnimatedGif/ImageBehavior.cs
@@ -52,7 +52,7 @@ public static void SetAnimatedSource(Image obj, ImageSource value)
"AnimatedSource",
typeof(ImageSource),
typeof(ImageBehavior),
- new UIPropertyMetadata(
+ new PropertyMetadata(
null,
AnimatedSourceChanged));
@@ -85,9 +85,75 @@ public static void SetRepeatBehavior(Image obj, RepeatBehavior value)
"RepeatBehavior",
typeof(RepeatBehavior),
typeof(ImageBehavior),
- new UIPropertyMetadata(
+ new PropertyMetadata(
default(RepeatBehavior),
- RepeatBehaviorChanged));
+ AnimationPropertyChanged));
+
+ ///
+ /// Gets the value of the AnimationSpeedRatio attached property for the specified object.
+ ///
+ /// The element from which to read the property value.
+ /// The speed ratio for the animated image.
+ public static double? GetAnimationSpeedRatio(DependencyObject obj)
+ {
+ return (double?)obj.GetValue(AnimationSpeedRatioProperty);
+ }
+
+ ///
+ /// Sets the value of the AnimationSpeedRatio attached property for the specified object.
+ ///
+ /// The element on which to set the property value.
+ /// The speed ratio of the animated image.
+ /// The AnimationSpeedRatio and AnimationDuration properties are mutually exclusive, only one can be set at a time.
+ public static void SetAnimationSpeedRatio(DependencyObject obj, double? value)
+ {
+ obj.SetValue(AnimationSpeedRatioProperty, value);
+ }
+
+ ///
+ /// Identifies the AnimationSpeedRatio attached property.
+ ///
+ public static readonly DependencyProperty AnimationSpeedRatioProperty =
+ DependencyProperty.RegisterAttached(
+ "AnimationSpeedRatio",
+ typeof(double?),
+ typeof(ImageBehavior),
+ new PropertyMetadata(
+ null,
+ AnimationPropertyChanged));
+
+ ///
+ /// Gets the value of the AnimationDuration attached property for the specified object.
+ ///
+ /// The element from which to read the property value.
+ /// The duration for the animated image.
+ public static Duration? GetAnimationDuration(DependencyObject obj)
+ {
+ return (Duration?)obj.GetValue(AnimationDurationProperty);
+ }
+
+ ///
+ /// Sets the value of the AnimationDuration attached property for the specified object.
+ ///
+ /// The element on which to set the property value.
+ /// The duration of the animated image.
+ /// The AnimationSpeedRatio and AnimationDuration properties are mutually exclusive, only one can be set at a time.
+ public static void SetAnimationDuration(DependencyObject obj, Duration? value)
+ {
+ obj.SetValue(AnimationDurationProperty, value);
+ }
+
+ ///
+ /// Identifies the AnimationDuration attached property.
+ ///
+ public static readonly DependencyProperty AnimationDurationProperty =
+ DependencyProperty.RegisterAttached(
+ "AnimationDuration",
+ typeof(Duration?),
+ typeof(ImageBehavior),
+ new PropertyMetadata(
+ null,
+ AnimationPropertyChanged));
///
/// Gets the value of the AnimateInDesignMode attached property for the specified object.
@@ -292,7 +358,7 @@ private static void AnimatedSourceChanged(DependencyObject o, DependencyProperty
imageControl.Unloaded -= ImageControlUnloaded;
imageControl.IsVisibleChanged -= VisibilityChanged;
- AnimationCache.DecrementReferenceCount(oldValue, GetRepeatBehavior(imageControl));
+ AnimationCache.RemoveControlForSource(oldValue, imageControl);
var controller = GetAnimationController(imageControl);
if (controller != null)
controller.Dispose();
@@ -342,13 +408,13 @@ static void ImageControlUnloaded(object sender, RoutedEventArgs e)
return;
var source = GetAnimatedSource(imageControl);
if (source != null)
- AnimationCache.DecrementReferenceCount(source, GetRepeatBehavior(imageControl));
+ AnimationCache.RemoveControlForSource(source, imageControl);
var controller = GetAnimationController(imageControl);
if (controller != null)
controller.Dispose();
}
- private static void RepeatBehaviorChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
+ private static void AnimationPropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
Image imageControl = o as Image;
if (imageControl == null)
@@ -357,8 +423,6 @@ private static void RepeatBehaviorChanged(DependencyObject o, DependencyProperty
ImageSource source = GetAnimatedSource(imageControl);
if (source != null)
{
- if (!Equals(e.OldValue, e.NewValue))
- AnimationCache.DecrementReferenceCount(source, (RepeatBehavior)e.OldValue);
if (imageControl.IsLoaded)
InitAnimationOrImage(imageControl);
}
@@ -445,62 +509,97 @@ private static void InitAnimationOrImage(Image imageControl)
private static ObjectAnimationUsingKeyFrames GetAnimation(Image imageControl, BitmapSource source)
{
- var animation = AnimationCache.GetAnimation(source, GetRepeatBehavior(imageControl));
- if (animation != null)
- return animation;
- GifFile gifMetadata;
- var decoder = GetDecoder(source, imageControl, out gifMetadata) as GifBitmapDecoder;
- if (decoder != null && decoder.Frames.Count > 1)
- {
- var fullSize = GetFullSize(decoder, gifMetadata);
- int index = 0;
- animation = new ObjectAnimationUsingKeyFrames();
- var totalDuration = TimeSpan.Zero;
- BitmapSource baseFrame = null;
- foreach (var rawFrame in decoder.Frames)
+ var cacheEntry = AnimationCache.Get(source);
+ if (cacheEntry == null)
+ {
+ var decoder = GetDecoder(source, imageControl, out GifFile gifMetadata) as GifBitmapDecoder;
+ if (decoder != null && decoder.Frames.Count > 1)
{
- var metadata = GetFrameMetadata(decoder, gifMetadata, index);
-
- var frame = MakeFrame(fullSize, rawFrame, metadata, baseFrame);
- var keyFrame = new DiscreteObjectKeyFrame(frame, totalDuration);
- animation.KeyFrames.Add(keyFrame);
-
- totalDuration += metadata.Delay;
-
- switch (metadata.DisposalMethod)
+ var fullSize = GetFullSize(decoder, gifMetadata);
+ int index = 0;
+ var keyFrames = new ObjectKeyFrameCollection();
+ var totalDuration = TimeSpan.Zero;
+ BitmapSource baseFrame = null;
+ foreach (var rawFrame in decoder.Frames)
{
- case FrameDisposalMethod.None:
- case FrameDisposalMethod.DoNotDispose:
- baseFrame = frame;
- break;
- case FrameDisposalMethod.RestoreBackground:
- if (IsFullFrame(metadata, fullSize))
- {
- baseFrame = null;
- }
- else
- {
- baseFrame = ClearArea(frame, metadata);
- }
- break;
- case FrameDisposalMethod.RestorePrevious:
- // Reuse same base frame
- break;
+ var metadata = GetFrameMetadata(decoder, gifMetadata, index);
+
+ var frame = MakeFrame(fullSize, rawFrame, metadata, baseFrame);
+ var keyFrame = new DiscreteObjectKeyFrame(frame, totalDuration);
+ keyFrames.Add(keyFrame);
+
+ totalDuration += metadata.Delay;
+
+ switch (metadata.DisposalMethod)
+ {
+ case FrameDisposalMethod.None:
+ case FrameDisposalMethod.DoNotDispose:
+ baseFrame = frame;
+ break;
+ case FrameDisposalMethod.RestoreBackground:
+ if (IsFullFrame(metadata, fullSize))
+ {
+ baseFrame = null;
+ }
+ else
+ {
+ baseFrame = ClearArea(frame, metadata);
+ }
+ break;
+ case FrameDisposalMethod.RestorePrevious:
+ // Reuse same base frame
+ break;
+ }
+
+ index++;
}
-
- index++;
+
+ int repeatCount = GetRepeatCountFromMetadata(decoder, gifMetadata);
+ cacheEntry = new AnimationCacheEntry(keyFrames, totalDuration, repeatCount);
+ AnimationCache.Add(source, cacheEntry);
}
- animation.Duration = totalDuration;
+ }
- animation.RepeatBehavior = GetActualRepeatBehavior(imageControl, decoder, gifMetadata);
+ if (cacheEntry != null)
+ {
+ var animation = new ObjectAnimationUsingKeyFrames
+ {
+ KeyFrames = cacheEntry.KeyFrames,
+ Duration = cacheEntry.Duration,
+ RepeatBehavior = GetActualRepeatBehavior(imageControl, cacheEntry.RepeatCountFromMetadata),
+ SpeedRatio = GetActualSpeedRatio(imageControl, cacheEntry.Duration)
+ };
- AnimationCache.AddAnimation(source, GetRepeatBehavior(imageControl), animation);
- AnimationCache.IncrementReferenceCount(source, GetRepeatBehavior(imageControl));
+ AnimationCache.AddControlForSource(source, imageControl);
return animation;
}
+
return null;
}
+ private static double GetActualSpeedRatio(Image imageControl, Duration naturalDuration)
+ {
+ var speedRatio = GetAnimationSpeedRatio(imageControl);
+ var duration = GetAnimationDuration(imageControl);
+
+ if (speedRatio.HasValue && duration.HasValue)
+ throw new InvalidOperationException("Cannot set both AnimationSpeedRatio and AnimationDuration");
+
+ if (speedRatio.HasValue)
+ return speedRatio.Value;
+
+ if (duration.HasValue)
+ {
+ if (!duration.Value.HasTimeSpan)
+ throw new InvalidOperationException("AnimationDuration cannot be Automatic or Forever");
+ if (duration.Value.TimeSpan.Ticks <= 0)
+ throw new InvalidOperationException("AnimationDuration must be strictly positive");
+ return naturalDuration.TimeSpan.Ticks / (double)duration.Value.TimeSpan.Ticks;
+ }
+
+ return 1.0;
+ }
+
private static BitmapSource ClearArea(BitmapSource frame, FrameMetadata metadata)
{
DrawingVisual visual = new DrawingVisual();
@@ -712,37 +811,35 @@ private static BitmapSource MakeFrame(
return result;
}
- private static RepeatBehavior GetActualRepeatBehavior(Image imageControl, BitmapDecoder decoder, GifFile gifMetadata)
+ private static RepeatBehavior GetActualRepeatBehavior(Image imageControl, int repeatCountFromMetadata)
{
// If specified explicitly, use this value
var repeatBehavior = GetRepeatBehavior(imageControl);
if (repeatBehavior != default(RepeatBehavior))
return repeatBehavior;
- int repeatCount;
- if (gifMetadata != null)
- {
- repeatCount = gifMetadata.RepeatCount;
- }
- else
- {
- repeatCount = GetRepeatCount(decoder);
- }
- if (repeatCount == 0)
+ if (repeatCountFromMetadata == 0)
return RepeatBehavior.Forever;
- return new RepeatBehavior(repeatCount);
+ return new RepeatBehavior(repeatCountFromMetadata);
}
- private static int GetRepeatCount(BitmapDecoder decoder)
+ private static int GetRepeatCountFromMetadata(BitmapDecoder decoder, GifFile gifMetadata)
{
- var ext = GetApplicationExtension(decoder, "NETSCAPE2.0");
- if (ext != null)
+ if (gifMetadata != null)
+ {
+ return gifMetadata.RepeatCount;
+ }
+ else
{
- byte[] bytes = ext.GetQueryOrNull("/Data");
- if (bytes != null && bytes.Length >= 4)
- return BitConverter.ToUInt16(bytes, 2);
+ var ext = GetApplicationExtension(decoder, "NETSCAPE2.0");
+ if (ext != null)
+ {
+ byte[] bytes = ext.GetQueryOrNull("/Data");
+ if (bytes != null && bytes.Length >= 4)
+ return BitConverter.ToUInt16(bytes, 2);
+ }
+ return 1;
}
- return 1;
}
private static BitmapMetadata GetApplicationExtension(BitmapDecoder decoder, string application)