File: Animations\AnimationManager.cs
Web Access
Project: src\src\Core\src\Core.csproj (Microsoft.Maui)
using System;
using System.Collections.Generic;
 
namespace Microsoft.Maui.Animations
{
	/// <inheritdoc/>
	public class AnimationManager : IAnimationManager, IDisposable
	{
		readonly List<Animation> _animations = new();
		long _lastUpdate;
		bool _disposedValue;
 
		/// <summary>
		/// Instantiate a new <see cref="AnimationManager"/> object.
		/// </summary>
		/// <param name="ticker">An instance of <see cref="ITicker"/> that will be used to time the animations.</param>
		public AnimationManager(ITicker ticker)
		{
			_lastUpdate = GetCurrentTick();
 
			Ticker = ticker;
			Ticker.Fire = OnFire;
		}
 
		/// <inheritdoc/>
		public ITicker Ticker { get; }
 
		/// <inheritdoc/>
		public double SpeedModifier { get; set; } = 1;
 
		/// <inheritdoc/>
		public bool AutoStartTicker { get; set; } = true;
 
		/// <inheritdoc/>
		public void Add(Animation animation)
		{
			// If animations are disabled, don't do anything
			if (!Ticker.SystemEnabled)
			{
				return;
			}
 
			if (!_animations.Contains(animation))
				_animations.Add(animation);
			if (!Ticker.IsRunning && AutoStartTicker)
				Start();
		}
 
		/// <inheritdoc/>
		public void Remove(Animation animation)
		{
			_animations.TryRemove(animation);
 
			if (_animations.Count == 0)
				End();
		}
 
		void Start()
		{
			_lastUpdate = GetCurrentTick();
			Ticker.Start();
		}
 
		void End() =>
			Ticker?.Stop();
 
		static long GetCurrentTick() =>
			Environment.TickCount & int.MaxValue;
 
		void OnFire()
		{
			if (!Ticker.SystemEnabled)
			{
				// This is a hack - if we're here, the ticker has detected that animations are no longer enabled,
				// and it's invoked the Fire event one last time because that's the only communication mechanism
				// it currently has available with the AnimationManager. We need to force all the running animations
				// to move to their finished state and stop running.
 
				ForceFinishAnimations();
				return;
			}
 
			var now = GetCurrentTick();
			var milliseconds = TimeSpan.FromMilliseconds(now - _lastUpdate).TotalMilliseconds;
			_lastUpdate = now;
 
			var animations = new List<Animation>(_animations);
			animations.ForEach(OnAnimationTick);
 
			if (_animations.Count == 0)
				End();
 
			void OnAnimationTick(Animation animation)
			{
				if (animation.HasFinished)
				{
					_animations.TryRemove(animation);
					animation.RemoveFromParent();
					return;
				}
 
				animation.Tick(AdjustSpeed(milliseconds));
 
				if (animation.HasFinished)
				{
					_animations.TryRemove(animation);
					animation.RemoveFromParent();
				}
			}
		}
 
		protected virtual void Dispose(bool disposing)
		{
			if (!_disposedValue)
			{
				if (disposing && Ticker is IDisposable disposable)
					disposable.Dispose();
 
				_disposedValue = true;
			}
		}
 
		/// <inheritdoc/>
		public void Dispose()
		{
			Dispose(disposing: true);
			GC.SuppressFinalize(this);
		}
 
		void ForceFinishAnimations()
		{
			var animations = new List<Animation>(_animations);
			animations.ForEach(ForceFinish);
			End();
 
			void ForceFinish(Animation animation)
			{
				animation.ForceFinish();
				_animations.TryRemove(animation);
				animation.RemoveFromParent();
			}
		}
 
		internal virtual double AdjustSpeed(double elapsedMilliseconds)
		{
			return elapsedMilliseconds * SpeedModifier;
		}
	}
}