Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 39 additions & 27 deletions src/Controls/src/Core/AnimationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
// THE SOFTWARE.

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using Microsoft.Maui.Animations;
using Microsoft.Maui.Controls.Internals;
Expand All @@ -36,7 +37,14 @@ namespace Microsoft.Maui.Controls
/// <include file="../../docs/Microsoft.Maui.Controls/AnimationExtensions.xml" path="Type[@FullName='Microsoft.Maui.Controls.AnimationExtensions']/Docs/*" />
public static class AnimationExtensions
{
static readonly Dictionary<int, Animation> s_tweeners;
// We use a ConcurrentDictionary because Tweener relies on being able to remove
// animations from the AnimationManager within its finalizer (via the Remove extension
// method below). Since finalization occurs on a different thread, it risks crashes when
// the finalizer is running at the same time another animation is finsihing and removing
// itself from this dictionary. So until we can change that design, this dictionary must
// be thread-safe.
static readonly ConcurrentDictionary<int, Animation> s_tweeners;

static readonly Dictionary<AnimatableKey, Info> s_animations;
static readonly Dictionary<AnimatableKey, int> s_kinetics;
Comment thread
hartez marked this conversation as resolved.
static int s_currentTweener = 1;
Expand All @@ -45,7 +53,7 @@ static AnimationExtensions()
{
s_animations = new Dictionary<AnimatableKey, Info>();
s_kinetics = new Dictionary<AnimatableKey, int>();
s_tweeners = new Dictionary<int, Animation>();
s_tweeners = new ConcurrentDictionary<int, Animation>();
}

public static int Add(this IAnimationManager animationManager, Action<double> step)
Expand All @@ -61,6 +69,7 @@ public static int Add(this IAnimationManager animationManager, Action<double> st
animation.Commit(animationManager);
return id;
}

public static int Insert(this IAnimationManager animationManager, Func<long, bool> step)
{
var id = s_currentTweener++;
Expand All @@ -74,11 +83,13 @@ public static int Insert(this IAnimationManager animationManager, Func<long, boo
animation.Commit(animationManager);
return id;
}

public static void Remove(this IAnimationManager animationManager, int tickerId)
{
var animation = s_tweeners[tickerId];
s_tweeners.Remove(tickerId);
animationManager.Remove(animation);
if (s_tweeners.TryRemove(tickerId, out Animation animation))
{
animationManager.Remove(animation);
}
}

/// <include file="../../docs/Microsoft.Maui.Controls/AnimationExtensions.xml" path="//Member[@MemberName='AbortAnimation']/Docs/*" />
Expand Down Expand Up @@ -213,14 +224,13 @@ static void AbortKinetic(AnimatableKey key)
{
if (s_kinetics.TryGetValue(key, out var ticker))
{
var animation = s_tweeners[ticker];
animation.AnimationManager?.Remove(ticker);
s_kinetics.Remove(key);
}
if (!s_kinetics.ContainsKey(key))
{
return;
if (s_tweeners.TryGetValue(ticker, out Animation animation))
{
animation.AnimationManager?.Remove(ticker);
}
}

s_kinetics.Remove(key);
}

static void AnimateInternal<T>(IAnimatable self, IAnimationManager animationManager, string name, Func<double, T> transform, Action<T> callback,
Expand Down Expand Up @@ -279,8 +289,11 @@ static void AnimateKineticInternal(IAnimatable self, IAnimationManager animation
if (!result)
{
finished?.Invoke();
if (s_kinetics.TryGetValue(key, out var ticker))
{
animationManager.Remove(ticker);
}
s_kinetics.Remove(key);
animationManager.Remove(tick);
}
return result;
});
Expand All @@ -292,21 +305,25 @@ static void AnimateKineticInternal(IAnimatable self, IAnimationManager animation
static void HandleTweenerFinished(object o, EventArgs args)
{
var tweener = o as Tweener;
Info info;
if (tweener != null && s_animations.TryGetValue(tweener.Handle, out info))

if (tweener != null && s_animations.TryGetValue(tweener.Handle, out Info info))
{
IAnimatable owner;
if (info.Owner.TryGetTarget(out owner))
owner.BatchBegin();
info.Callback(tweener.Value);
var tweenerValue = tweener.Value;
info.Owner.TryGetTarget(out IAnimatable owner);

owner?.BatchBegin();

info.Callback(tweenerValue);

var repeat = false;

// If the Ticker has been disabled (e.g., by power save mode), then don't repeat the animation
var animationsEnabled = info.AnimationManager.Ticker.SystemEnabled;

if (info.Repeat != null && animationsEnabled)
{
repeat = info.Repeat();
}

if (!repeat)
{
Expand All @@ -316,10 +333,9 @@ static void HandleTweenerFinished(object o, EventArgs args)
tweener.Stop();
}

info.Finished?.Invoke(tweener.Value, !animationsEnabled);
info.Finished?.Invoke(tweenerValue, !animationsEnabled);

if (info.Owner.TryGetTarget(out owner))
owner.BatchCommit();
owner?.BatchCommit();

if (repeat)
{
Expand All @@ -330,11 +346,7 @@ static void HandleTweenerFinished(object o, EventArgs args)

static void HandleTweenerUpdated(object o, EventArgs args)
{
var tweener = o as Tweener;
Info info;
IAnimatable owner;

if (tweener != null && s_animations.TryGetValue(tweener.Handle, out info) && info.Owner.TryGetTarget(out owner))
if (o is Tweener tweener && s_animations.TryGetValue(tweener.Handle, out Info info) && info.Owner.TryGetTarget(out IAnimatable owner))
{
owner.BatchBegin();
info.Callback(info.Easing.Ease(tweener.Value));
Expand Down
139 changes: 79 additions & 60 deletions src/Controls/src/Core/Tweener.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@

using System;
using Microsoft.Maui.Animations;
using Microsoft.Maui.Controls.Internals;

namespace Microsoft.Maui.Controls
{
Expand All @@ -39,28 +38,40 @@ public TweenerAnimation(Func<long, bool> step)
{
_step = step;
}

protected override void OnTick(double millisecondsSinceLastUpdate)
{
var running = _step.Invoke((long)millisecondsSinceLastUpdate);
HasFinished = !running;
}

internal override void ForceFinish()
{
if (HasFinished)
{
return;
}

HasFinished = true;

// The tweeners use long.MaxValue for in-band signaling that they should
// jump to the end of the animation
_ = _step.Invoke(long.MaxValue);
Comment thread
mattleibow marked this conversation as resolved.
}
}

internal class Tweener
{
IAnimationManager animationManager;
readonly IAnimationManager _animationManager;
long _lastMilliseconds;

int _timer;
int _animationManagerKey;
long _frames;

public Tweener(uint length, IAnimationManager animationManager)
{
Value = 0.0f;
Length = length;
this.animationManager = animationManager;
Rate = 1;
_animationManager = animationManager;
Loop = false;
}

Expand All @@ -69,119 +80,127 @@ public Tweener(uint length, uint rate, IAnimationManager animationManager)
Value = 0.0f;
Length = length;
Rate = rate;
this.animationManager = animationManager;
_animationManager = animationManager;
Loop = false;
}

public AnimatableKey Handle { get; set; }

public uint Length { get; }

public uint Rate { get; }
public uint Rate { get; } = 1;
Comment thread
hartez marked this conversation as resolved.

public bool Loop { get; set; }

public double Value { get; private set; }
public double Value { get; set; }

public event EventHandler Finished;
public event EventHandler ValueUpdated;

public void Pause()
{
if (_timer != 0)
if (_animationManagerKey != 0)
{
animationManager.Remove(_timer);
_timer = 0;
_animationManager.Remove(_animationManagerKey);
_animationManagerKey = 0;
}
}

bool Step(long step)
{
if (step == long.MaxValue)
{
// Signal that the Tweener is being force to move to the finished state,
// usually because the underlying Ticker has been disabled by the system
FinishImmediately();
return false;
}
else
{
long ms = step + _lastMilliseconds;
Value = Math.Min(1.0f, ms / (double)Length);
_lastMilliseconds = ms;
}

long wantedFrames = (_lastMilliseconds / Rate) + 1;
if (wantedFrames > _frames || Value >= 1.0f)
{
ValueUpdated?.Invoke(this, EventArgs.Empty);
}

_frames = wantedFrames;

if (Value >= 1.0f)
{
if (Loop)
{
_lastMilliseconds = 0;
Value = 0.0f;
return true;
}

Finished?.Invoke(this, EventArgs.Empty);

Value = 0.0f;
_animationManagerKey = 0;
return false;
}

return true;
}

public void Start()
{
Pause();

_lastMilliseconds = 0;
_frames = 0;

if (!animationManager.Ticker.SystemEnabled)
if (!_animationManager.Ticker.SystemEnabled)
{
// The Ticker's disabled, probably because the system has animations disabled
// The Tweener should move immediately to the finished state and shut down
FinishImmediately();
return;
}

_timer = animationManager.Insert(step =>
{
if (step == long.MaxValue)
{
// We're being forced to finish
Value = 1.0;
}
else
{
long ms = step + _lastMilliseconds;

Value = Math.Min(1.0f, ms / (double)Length);

_lastMilliseconds = ms;
}
_animationManagerKey = _animationManager.Insert(Step);

long wantedFrames = (_lastMilliseconds / Rate) + 1;
if (wantedFrames > _frames || Value >= 1.0f)
{
ValueUpdated?.Invoke(this, EventArgs.Empty);
}
_frames = wantedFrames;

if (Value >= 1.0f)
{
if (Loop)
{
_lastMilliseconds = 0;
Value = 0.0f;
return true;
}

Finished?.Invoke(this, EventArgs.Empty);
Value = 0.0f;
_timer = 0;
return false;
}
return true;
});
if (!animationManager.Ticker.IsRunning)
animationManager.Ticker.Start();
if (!_animationManager.Ticker.IsRunning)
_animationManager.Ticker.Start();
}

void FinishImmediately()
{
Value = 1.0f;

ValueUpdated?.Invoke(this, EventArgs.Empty);
Finished?.Invoke(this, EventArgs.Empty);

Value = 0.0f;
_timer = 0;
_animationManagerKey = 0;
}

public void Stop()
{
Pause();
Value = 1.0f;
Finished?.Invoke(this, EventArgs.Empty);
Value = 0.0f;
}

public event EventHandler ValueUpdated;

~Tweener()
{
if (_timer != 0)
if (_animationManagerKey != 0)
{
try
{
animationManager.Remove(_timer);
_animationManager.Remove(_animationManagerKey);
}
catch (InvalidOperationException)
{
}
}
_timer = 0;
_animationManagerKey = 0;
}
}
}
Loading