Skip to content

Commit

Permalink
Re-enable disabled animation tests;
Browse files Browse the repository at this point in the history
Make Android respect power saving/disabled animations;
Watch animation duration if API >= 33
  • Loading branch information
hartez committed Oct 4, 2023
1 parent a05a5a7 commit 92f5b51
Show file tree
Hide file tree
Showing 22 changed files with 447 additions and 196 deletions.
66 changes: 39 additions & 27 deletions src/Controls/src/Core/AnimationExtensions.cs
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;
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
138 changes: 79 additions & 59 deletions src/Controls/src/Core/Tweener.cs
Expand Up @@ -27,7 +27,6 @@

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

namespace Microsoft.Maui.Controls
{
Expand All @@ -39,27 +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);
}
}

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;
_animationManager = animationManager;
Rate = 1;
Loop = false;
}
Expand All @@ -69,119 +81,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;

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;
}
}
}

0 comments on commit 92f5b51

Please sign in to comment.