From a3321f53c8abd8949cc7c470845274063c13f8a1 Mon Sep 17 00:00:00 2001 From: charlenni Date: Thu, 15 Aug 2019 00:32:36 +0200 Subject: [PATCH] Added an animation to Navigator and a FlyTo() function (#627) * Added added an animation to Navigator and a FlyTo() function for testing this animation * Added animation to the most methods of Navigator * Added an animator class with entries, so that each value could be animated * Replaced changes of Navigator with original code * Reverted Navigator setting from AnimatedNavigator to normal Navigator --- Mapsui/AnimatedNavigator.cs | 485 ++++++++++++++++++ Mapsui/Navigator.cs | 2 +- Mapsui/Utilities/Animation.cs | 116 +++++ Mapsui/Utilities/AnimationEntry.cs | 82 +++ Mapsui/Utilities/AnimationEventArgs.cs | 12 + Mapsui/Utilities/Easing.cs | 98 ++++ .../AnimationSample.cs | 33 ++ .../Mapsui.Samples.Forms.Shared.projitems | 1 + 8 files changed, 828 insertions(+), 1 deletion(-) create mode 100644 Mapsui/AnimatedNavigator.cs create mode 100644 Mapsui/Utilities/Animation.cs create mode 100644 Mapsui/Utilities/AnimationEntry.cs create mode 100644 Mapsui/Utilities/AnimationEventArgs.cs create mode 100644 Mapsui/Utilities/Easing.cs create mode 100644 Samples/Mapsui.Samples.Forms/Mapsui.Samples.Forms.Shared/AnimationSample.cs diff --git a/Mapsui/AnimatedNavigator.cs b/Mapsui/AnimatedNavigator.cs new file mode 100644 index 0000000000..b5487bb67d --- /dev/null +++ b/Mapsui/AnimatedNavigator.cs @@ -0,0 +1,485 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Timers; +using Mapsui.Geometries; +using Mapsui.Utilities; + +namespace Mapsui +{ + public class AnimatedNavigator : INavigator + { + private readonly Map _map; + private readonly IViewport _viewport; + private Animation _animation; + + public EventHandler Navigated { get; set; } + + public AnimatedNavigator(Map map, IViewport viewport) + { + _map = map; + _viewport = viewport; + } + + + /// + public void NavigateTo(BoundingBox extent, ScaleMethod scaleMethod = ScaleMethod.Fit) + { + NavigateTo(extent, scaleMethod, 300); + } + + /// + /// Navigate center of viewport to center of extent and change resolution + /// + /// New extent for viewport to show + /// Scale method to use to determine resolution + /// Duration of animation in millisecondsScale method to use to determine resolution + public void NavigateTo(BoundingBox extent, ScaleMethod scaleMethod = ScaleMethod.Fit, long duration = 300) + { + if (extent == null) return; + + var resolution = ZoomHelper.DetermineResolution( + extent.Width, extent.Height, _viewport.Width, _viewport.Height, scaleMethod); + + NavigateTo(extent.Centroid, resolution, duration); + } + + /// + public void NavigateToFullEnvelope(ScaleMethod scaleMethod = ScaleMethod.Fill) + { + NavigateTo(_map.Envelope, scaleMethod, 300); + } + + /// + /// Navigate to a resolution, so such the map uses the fill method + /// + /// + /// Duration of animation in millisecondsScale method to use to determine resolution + public void NavigateToFullEnvelope(ScaleMethod scaleMethod = ScaleMethod.Fill, long duration = 300) + { + NavigateTo(_map.Envelope, scaleMethod, duration); + } + + /// + public void NavigateTo(Point center, double resolution) + { + NavigateTo(center, resolution, 300); + } + + /// + /// Navigate to center and change resolution with animation + /// + /// New center to move to + /// New resolution to use + /// Duration of animation in milliseconds + public void NavigateTo(Point center, double resolution, long duration = 300) + { + // Stop any old animation if there is one + if (_animation != null) + { + _animation.Stop(false); + _animation = null; + } + + if (duration == 0) + { + _viewport.SetCenter(center); + _viewport.SetResolution(resolution); + + Navigated?.Invoke(this, EventArgs.Empty); + } + else + { + var animations = new List(); + + if (!_viewport.Center.Equals(center)) + { + var entry = new AnimationEntry( + start: _viewport.Center, + end: (ReadOnlyPoint)center, + animationStart: 0, + animationEnd: 1, + easing: Easing.Linear, + tick: CenterTick, + final: CenterFinal + ); + animations.Add(entry); + } + + if (_viewport.Resolution != resolution) + { + var entry = new AnimationEntry( + start: _viewport.Resolution, + end: resolution, + animationStart: 0, + animationEnd: 1, + easing: Easing.Linear, + tick: ResolutionTick, + final: ResolutionFinal + ); + animations.Add(entry); + } + + if (animations.Count == 0) + return; + + _animation = new Animation(duration); + _animation.Entries.AddRange(animations); + _animation.Start(); + } + } + + /// + public void ZoomTo(double resolution) + { + ZoomTo(resolution, 300); + } + + /// + /// Change resolution of viewport + /// + /// New resolution to use + /// Duration of animation in milliseconds + public void ZoomTo(double resolution, long duration = 300) + { + // Stop any old animation if there is one + if (_animation != null) + { + _animation.Stop(false); + _animation = null; + } + + if (duration == 0) + { + _viewport.SetResolution(resolution); + + Navigated?.Invoke(this, EventArgs.Empty); + } + else + { + var animations = new List(); + + if (_viewport.Resolution == resolution) + return; + + var entry = new AnimationEntry( + start: _viewport.Resolution, + end: resolution, + animationStart: 0, + animationEnd: 1, + easing: Easing.Linear, + tick: ResolutionTick, + final: ResolutionFinal + ); + animations.Add(entry); + + _animation = new Animation(duration); + _animation.Entries.AddRange(animations); + _animation.Start(); + } + } + + /// + /// Zoom in to the next resolution + /// + public void ZoomIn() + { + var resolution = ZoomHelper.ZoomIn(_map.Resolutions, _viewport.Resolution); + + ZoomTo(resolution); + } + + /// + /// Zoom out to the next resolution + /// + public void ZoomOut() + { + var resolution = ZoomHelper.ZoomOut(_map.Resolutions, _viewport.Resolution); + + ZoomTo(resolution); + } + + /// + /// Zoom in to a given point + /// + /// Center to use for zoom in + public void ZoomIn(Point centerOfZoom) + { + var resolution = ZoomHelper.ZoomIn(_map.Resolutions, _viewport.Resolution); + ZoomTo(resolution, centerOfZoom); + } + + /// + /// Zoom out to a given point + /// + /// Center to use for zoom out + public void ZoomOut(Point centerOfZoom) + { + var resolution = ZoomHelper.ZoomOut(_map.Resolutions, _viewport.Resolution); + ZoomTo(resolution, centerOfZoom); + } + + /// + /// Zoom to a given resolution with a given point as center + /// + /// Resolution to zoom + /// Center to use for zoom + public void ZoomTo(double resolution, Point centerOfZoom) + { + // 1) Temporarily center on the center of zoom + _viewport.SetCenter(_viewport.ScreenToWorld(centerOfZoom)); + + // 2) Then zoom + _viewport.SetResolution(resolution); + + // 3) Then move the temporary center of the map back to the mouse position + _viewport.SetCenter(_viewport.ScreenToWorld( + _viewport.Width - centerOfZoom.X, + _viewport.Height - centerOfZoom.Y)); + + Navigated?.Invoke(this, EventArgs.Empty); + } + + /// + public void CenterOn(double x, double y) + { + CenterOn(x, y, 300); + } + + /// + public void CenterOn(Point center) + { + CenterOn(center.X, center.Y, 300); + } + + /// + /// Change center of viewport to X/Y coordinates + /// + /// X value of the new center + /// Y value of the new center + /// Duration of animation in milliseconds + /// Function for easing + public void CenterOn(double x, double y, long duration = 300) + { + CenterOn(new Point(x, y), duration); + } + + /// + /// Change center of viewport + /// + /// New center point of viewport + /// Duration of animation in milliseconds + /// Function for easing + public void CenterOn(Point center, long duration = 300) + { + // Stop any old animation if there is one + if (_animation != null) + { + _animation.Stop(false); + _animation = null; + } + + if (duration == 0) + { + _viewport.SetCenter(center); + + Navigated?.Invoke(this, EventArgs.Empty); + } + else + { + var animations = new List(); + + if (_viewport.Center.Equals(center)) + return; + + var entry = new AnimationEntry( + start: _viewport.Center, + end: (ReadOnlyPoint)center, + animationStart: 0, + animationEnd: 1, + easing: Easing.Linear, + tick: CenterTick, + final: CenterFinal + ); + animations.Add(entry); + + _animation = new Animation(duration); + _animation.Entries.AddRange(animations); + _animation.Start(); + } + } + + /// + /// Fly to the given center with zooming out to given resolution and in again + /// + /// Point to fly to + /// Maximum resolution to zoom out + /// Duration for animation in milliseconds + public void FlyTo(Point center, double maxResolution, long duration = 2000) + { + // Stop any old animation if there is one + if (_animation != null) + { + _animation.Stop(false); + _animation = null; + } + + var halfCenter = new Point(_viewport.Center.X + (center.X - _viewport.Center.X) / 2.0, _viewport.Center.Y + (center.Y - _viewport.Center.Y) / 2.0); + var resolution = _viewport.Resolution; + + if (duration == 0) + { + _viewport.SetCenter(center); + + Navigated?.Invoke(this, EventArgs.Empty); + } + else + { + var animations = new List(); + AnimationEntry entry; + + if (!_viewport.Center.Equals(center)) + { + entry = new AnimationEntry( + start: _viewport.Center, + end: (ReadOnlyPoint)center, + animationStart: 0, + animationEnd: 1, + easing: Easing.Linear, + tick: CenterTick, + final: CenterFinal + ); + animations.Add(entry); + } + + entry = new AnimationEntry( + start: _viewport.Resolution, + end: maxResolution, + animationStart: 0, + animationEnd: 0.5, + easing: Easing.SinOut, + tick: ResolutionTick, + final: ResolutionFinal + ); + animations.Add(entry); + + entry = new AnimationEntry( + start: maxResolution, + end: _viewport.Resolution, + animationStart: 0.5, + animationEnd: 1, + easing: Easing.SinIn, + tick: ResolutionTick, + final: ResolutionFinal + ); + animations.Add(entry); + + _animation = new Animation(duration); + _animation.Entries.AddRange(animations); + _animation.Start(); + } + } + + /// + public void RotateTo(double rotation) + { + RotateTo(rotation, 300); + } + + /// + /// Change rotation of viewport + /// + /// New rotation in degrees of viewport> + public void RotateTo(double rotation, long duration) + { + // Stop any old animation if there is one + if (_animation != null) + { + _animation.Stop(false); + _animation = null; + } + + if (duration == 0) + { + _viewport.SetRotation(rotation); + + Navigated?.Invoke(this, EventArgs.Empty); + } + else + { + var animations = new List(); + AnimationEntry entry; + + if (_viewport.Rotation == rotation) + return; + + entry = new AnimationEntry( + start: _viewport.Rotation, + end: rotation, + animationStart: 0, + animationEnd: 1, + easing: Easing.Linear, + tick: RotationTick, + final: RotationFinal + ); + animations.Add(entry); + + _animation = new Animation(duration); + _animation.Entries.AddRange(animations); + _animation.Start(); + } + } + + private void CenterTick(AnimationEntry entry, double value) + { + var x = ((ReadOnlyPoint)entry.Start).X + (((ReadOnlyPoint)entry.End).X - ((ReadOnlyPoint)entry.Start).X) * entry.Easing.Ease(value); + var y = ((ReadOnlyPoint)entry.Start).Y + (((ReadOnlyPoint)entry.End).Y - ((ReadOnlyPoint)entry.Start).Y) * entry.Easing.Ease(value); + + // Set new values + _viewport.SetCenter(x, y); + + Navigated?.Invoke(this, EventArgs.Empty); + } + + private void CenterFinal(AnimationEntry entry) + { + _viewport.SetCenter((ReadOnlyPoint)entry.End); + + Navigated?.Invoke(this, EventArgs.Empty); + } + + private void ResolutionTick(AnimationEntry entry, double value) + { + var r = (double)entry.Start + ((double)entry.End - (double)entry.Start) * entry.Easing.Ease(value); + + // Set new values + _viewport.SetResolution(r); + + Navigated?.Invoke(this, EventArgs.Empty); + } + + private void ResolutionFinal(AnimationEntry entry) + { + _viewport.SetResolution((double)entry.End); + + Navigated?.Invoke(this, EventArgs.Empty); + } + + private void RotationTick(AnimationEntry entry, double value) + { + var r = (double)entry.Start + ((double)entry.End - (double)entry.Start) * entry.Easing.Ease(value); + + // Set new values + _viewport.SetRotation(r); + + Navigated?.Invoke(this, EventArgs.Empty); + } + + private void RotationFinal(AnimationEntry entry) + { + _viewport.SetRotation((double)entry.End); + + Navigated?.Invoke(this, EventArgs.Empty); + } + } +} diff --git a/Mapsui/Navigator.cs b/Mapsui/Navigator.cs index 69cd0675ff..f792f0a7f1 100644 --- a/Mapsui/Navigator.cs +++ b/Mapsui/Navigator.cs @@ -9,7 +9,7 @@ public class Navigator : INavigator private readonly Map _map; private readonly IViewport _viewport; - public EventHandler Navigated { get; set; } + public EventHandler Navigated { get; set; } public Navigator(Map map, IViewport viewport) { diff --git a/Mapsui/Utilities/Animation.cs b/Mapsui/Utilities/Animation.cs new file mode 100644 index 0000000000..845ecdec03 --- /dev/null +++ b/Mapsui/Utilities/Animation.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Timers; + +namespace Mapsui.Utilities +{ + public class Animation + { + private Timer _timer; + private Stopwatch _stopwatch; + private long _stopwatchStart; + private long _durationTicks; + + public Animation(long duration) + { + Duration = duration; + + // Create timer for animation + _timer = new Timer + { + Interval = 16, + AutoReset = true + }; + _timer.Elapsed += HandleTimerElapse; + } + + public EventHandler Started { get; set; } + public EventHandler Stopped { get; set; } + public EventHandler Ticked { get; set; } + + /// + /// Duration of the whole animation cycle in milliseconds + /// + public long Duration { get; } = 300; + + /// + /// Animations, that should be made + /// + public List Entries { get; } = new List(); + + /// + /// True, if animation is running + /// + public bool IsRunning { get => _timer != null && _timer.Enabled; } + + public void Start() + { + if (IsRunning) + { + Stop(false); + } + + // Animation in ticks; + _durationTicks = Duration * Stopwatch.Frequency / 1000; + + _stopwatch = Stopwatch.StartNew(); + _stopwatchStart = _stopwatch.ElapsedTicks; + _timer.Start(); + + Started?.Invoke(this, new AnimationEventArgs(0)); + } + + /// + /// Stop a running animation if there is one + /// + /// Should final of each list entry be called + public void Stop(bool gotoEnd = true) + { + if (!_timer.Enabled) + return; + + _timer.Stop(); + _stopwatch.Stop(); + + double ticks = _stopwatch.ElapsedTicks - _stopwatchStart; + var value = ticks / _durationTicks; + + if (gotoEnd) + { + foreach(var entry in Entries) + { + entry.Final(); + } + } + + Stopped?.Invoke(this, new AnimationEventArgs(value)); + } + + /// + /// Timer tick for animation + /// + /// Sender of this tick + /// Timer tick arguments + private void HandleTimerElapse(object sender, ElapsedEventArgs e) + { + double ticks = _stopwatch.ElapsedTicks - _stopwatchStart; + var value = ticks / _durationTicks; + + if (value >= 1.0) + { + Stop(true); + return; + } + + // Calc new values + foreach(var entry in Entries) + { + if (value >= entry.AnimationStart && value <= entry.AnimationEnd) + entry.Tick(value); + } + + Ticked?.Invoke(this, new AnimationEventArgs(value)); + } + } +} diff --git a/Mapsui/Utilities/AnimationEntry.cs b/Mapsui/Utilities/AnimationEntry.cs new file mode 100644 index 0000000000..774e147a0d --- /dev/null +++ b/Mapsui/Utilities/AnimationEntry.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Mapsui.Utilities +{ + public class AnimationEntry + { + private double _animationDelta; + private Action _tick; + private Action _final; + + public AnimationEntry(object start, object end, + double animationStart = 0, double animationEnd = 1, + Easing easing = null, + Action tick = null, + Action final = null) + { + AnimationStart = animationStart; + AnimationEnd = animationEnd; + + Start = start; + End = end; + + Easing = easing ?? Easing.Linear; + + _animationDelta = AnimationEnd - AnimationStart; + + _tick = tick; + _final = final; + } + + /// + /// When this animation starts in animation cycle. Value between 0 and 1. + /// + public double AnimationStart { get; } + + /// + /// When this animation ends in animation cycle. Value between 0 and 1. + /// + public double AnimationEnd { get; } + + /// + /// Object holding the starting value + /// + public object Start { get; } + + /// + /// Object holding the end value + /// + public object End { get; } + + /// + /// Easing to use for this animation + /// + public Easing Easing { get; } + + /// + /// Called when a value should changed + /// + /// Position in animation cycle between 0 and 1 + public void Tick(double value) + { + if (value < AnimationStart || value > AnimationEnd) + return; + + // Each tick gets a value between 0 and 1 for its own cycle + // Its independent from the global animation cycle + var v = (value - AnimationStart) / _animationDelta; + + _tick(this, v); + } + + /// + /// Called when the animation cycle is at the end + /// + public void Final() + { + _final(this); + } + } +} diff --git a/Mapsui/Utilities/AnimationEventArgs.cs b/Mapsui/Utilities/AnimationEventArgs.cs new file mode 100644 index 0000000000..90d45bf493 --- /dev/null +++ b/Mapsui/Utilities/AnimationEventArgs.cs @@ -0,0 +1,12 @@ +namespace Mapsui.Utilities +{ + public class AnimationEventArgs + { + public AnimationEventArgs(double value) + { + Value = value; + } + + public double Value { get; } + } +} diff --git a/Mapsui/Utilities/Easing.cs b/Mapsui/Utilities/Easing.cs new file mode 100644 index 0000000000..9b0dc2cba8 --- /dev/null +++ b/Mapsui/Utilities/Easing.cs @@ -0,0 +1,98 @@ +// +// Tweener.cs +// +// Author: +// Jason Smith +// +// Copyright (c) 2012 Xamarin Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; + +namespace Mapsui.Utilities +{ + public class Easing + { + public static readonly Easing Linear = new Easing(x => x); + + public static readonly Easing SinOut = new Easing(x => Math.Sin(x * Math.PI * 0.5f)); + public static readonly Easing SinIn = new Easing(x => 1.0f - Math.Cos(x * Math.PI * 0.5f)); + public static readonly Easing SinInOut = new Easing(x => -Math.Cos(Math.PI * x) / 2.0f + 0.5f); + + public static readonly Easing CubicIn = new Easing(x => x * x * x); + public static readonly Easing CubicOut = new Easing(x => Math.Pow(x - 1.0f, 3.0f) + 1.0f); + + public static readonly Easing CubicInOut = new Easing(x => x < 0.5f ? Math.Pow(x * 2.0f, 3.0f) / 2.0f : (Math.Pow((x - 1) * 2.0f, 3.0f) + 2.0f) / 2.0f); + + public static readonly Easing BounceOut; + public static readonly Easing BounceIn; + + public static readonly Easing SpringIn = new Easing(x => x * x * ((1.70158f + 1) * x - 1.70158f)); + public static readonly Easing SpringOut = new Easing(x => (x - 1) * (x - 1) * ((1.70158f + 1) * (x - 1) + 1.70158f) + 1); + + readonly Func _easingFunc; + + static Easing() + { + BounceOut = new Easing(p => + { + if (p < 1 / 2.75f) + { + return 7.5625f * p * p; + } + if (p < 2 / 2.75f) + { + p -= 1.5f / 2.75f; + + return 7.5625f * p * p + .75f; + } + if (p < 2.5f / 2.75f) + { + p -= 2.25f / 2.75f; + + return 7.5625f * p * p + .9375f; + } + p -= 2.625f / 2.75f; + + return 7.5625f * p * p + .984375f; + }); + + BounceIn = new Easing(p => 1.0f - BounceOut.Ease(1 - p)); + } + + public Easing(Func easingFunc) + { + if (easingFunc == null) + throw new ArgumentNullException("easingFunc"); + + _easingFunc = easingFunc; + } + + public double Ease(double v) + { + return _easingFunc(v); + } + + public static implicit operator Easing(Func func) + { + return new Easing(func); + } + } +} diff --git a/Samples/Mapsui.Samples.Forms/Mapsui.Samples.Forms.Shared/AnimationSample.cs b/Samples/Mapsui.Samples.Forms/Mapsui.Samples.Forms.Shared/AnimationSample.cs new file mode 100644 index 0000000000..b0d063390b --- /dev/null +++ b/Samples/Mapsui.Samples.Forms/Mapsui.Samples.Forms.Shared/AnimationSample.cs @@ -0,0 +1,33 @@ +using System; +using Mapsui.Samples.Common.Maps; +using Mapsui.UI; +using Mapsui.UI.Forms; + +namespace Mapsui.Samples.Forms +{ + public class AnimationSample : IFormsSample + { + static int markerNum = 1; + static Random rnd = new Random(); + + public string Name => "Animation Sample"; + + public string Category => "Forms"; + + public bool OnClick(object sender, EventArgs args) + { + var mapView = sender as MapView; + var e = args as MapClickedEventArgs; + + var navigator = (AnimatedNavigator)mapView.Navigator; + navigator.FlyTo(e.Point.ToMapsui(), mapView.Viewport.Resolution * 2); + + return true; + } + + public void Setup(IMapControl mapControl) + { + mapControl.Map = OsmSample.CreateMap(); + } + } +} diff --git a/Samples/Mapsui.Samples.Forms/Mapsui.Samples.Forms.Shared/Mapsui.Samples.Forms.Shared.projitems b/Samples/Mapsui.Samples.Forms/Mapsui.Samples.Forms.Shared/Mapsui.Samples.Forms.Shared.projitems index a4746def5a..1ef9255657 100644 --- a/Samples/Mapsui.Samples.Forms/Mapsui.Samples.Forms.Shared/Mapsui.Samples.Forms.Shared.projitems +++ b/Samples/Mapsui.Samples.Forms/Mapsui.Samples.Forms.Shared/Mapsui.Samples.Forms.Shared.projitems @@ -26,6 +26,7 @@ MapPage.xaml Code +