From b8ec3081f866eec0986358f7da04fbea82281924 Mon Sep 17 00:00:00 2001 From: Omid Mafakher Date: Sun, 7 Jul 2024 17:58:17 +0330 Subject: [PATCH 1/4] #27 Implementation of alert transition --- .../Platform/Ios/AlertTransition.cs | 143 ++++++++++++++++++ .../Directory.Build.props | 6 - .../MainActivity.cs | 4 +- .../Program.cs | 4 +- .../AppDelegate.cs | 8 +- .../ShellBottomCustomNavigator.csproj | 10 +- .../ShellBottomCustomNavigator/Styles.axaml | 10 +- .../ShellExample.Android.csproj | 6 +- .../ShellExample.Desktop.csproj | 2 +- .../ShellExample.Web/ShellExample.Web.csproj | 2 +- .../ShellExample/ShellExample.iOS/Info.plist | 2 +- .../ShellExample.iOS/ShellExample.iOS.csproj | 2 +- .../ShellExample/ShellExample.csproj | 2 +- .../ShellExample/ShellExample/Styles.axaml | 6 +- .../ShellExample/Views/MainView.axaml | 4 +- 15 files changed, 177 insertions(+), 34 deletions(-) create mode 100644 src/AvaloniaInside.Shell/Platform/Ios/AlertTransition.cs delete mode 100644 src/Example/ShellBottomCustomNavigator/Directory.Build.props diff --git a/src/AvaloniaInside.Shell/Platform/Ios/AlertTransition.cs b/src/AvaloniaInside.Shell/Platform/Ios/AlertTransition.cs new file mode 100644 index 0000000..1e7d122 --- /dev/null +++ b/src/AvaloniaInside.Shell/Platform/Ios/AlertTransition.cs @@ -0,0 +1,143 @@ +using Avalonia.Animation.Easings; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.Rendering.Composition; +using Avalonia; +using System; +using System.Threading; +using System.Threading.Tasks; +using AvaloniaInside.Shell.Platform.Ios; + +namespace AvaloniaInside.Shell.Platform.Windows; +public class AlertTransition : PlatformBasePageTransition +{ + public static readonly AlertTransition Instance = new(); + + private const float EndingCue = 0.7f; + private const float StartingCue = 0.3f; + + /// + /// Gets the duration of the animation. + /// + public override TimeSpan Duration { get; set; } = TimeSpan.FromSeconds(0.25); + + /// + /// Scale factor to animate in + /// + public float ZoomInFactor { get; set; } = 1.25f; + + /// + /// Scale factor to animate out + /// + public float ZoomOutFactor { get; set; } = 0.75f; + + /// + /// Gets or sets element entrance easing. + /// + public override Easing Easing { get; set; } = Easing.Parse("0.85, 0.0, 0.0, 1.0"); + + protected override CompositionAnimationGroup GetOrCreateEnteranceAnimation(CompositionVisual element, double widthDistance, double heightDistance) + { + var compositor = element.Compositor; + + var scaleAnimation = compositor.CreateVector3DKeyFrameAnimation(); + scaleAnimation.Duration = Duration; + scaleAnimation.Target = nameof(element.Scale); + scaleAnimation.InsertKeyFrame(StartingCue, new Vector3D(ZoomOutFactor, ZoomOutFactor, 1), Easing); + scaleAnimation.InsertKeyFrame(1.0f, new Vector3D(1, 1, 1), Easing); + + var fadeAnimation = compositor.CreateScalarKeyFrameAnimation(); + fadeAnimation.Duration = Duration; + fadeAnimation.Target = nameof(element.Opacity); + fadeAnimation.InsertKeyFrame(StartingCue, 0f, Easing); + fadeAnimation.InsertKeyFrame(1.0f, 1f, Easing); + + var enteranceAnimation = compositor.CreateAnimationGroup(); + enteranceAnimation.Add(scaleAnimation); + enteranceAnimation.Add(fadeAnimation); + return enteranceAnimation; + } + + protected override CompositionAnimationGroup GetOrCreateExitAnimation(CompositionVisual element, double widthDistance, double heightDistance) + { + var compositor = element.Compositor; + + var scaleAnimation = compositor.CreateVector3DKeyFrameAnimation(); + scaleAnimation.Duration = Duration; + scaleAnimation.Target = nameof(element.Scale); + scaleAnimation.InsertKeyFrame(0f, new Vector3D(1, 1, 1), Easing); + scaleAnimation.InsertKeyFrame(EndingCue, new Vector3D(ZoomOutFactor, ZoomOutFactor, 1), Easing); + + var fadeAnimation = compositor.CreateScalarKeyFrameAnimation(); + fadeAnimation.Duration = Duration; + fadeAnimation.Target = nameof(element.Opacity); + fadeAnimation.InsertKeyFrame(0f, 1f, Easing); + fadeAnimation.InsertKeyFrame(EndingCue, 0f, Easing); + + var exitAnimation = compositor.CreateAnimationGroup(); + exitAnimation.Add(scaleAnimation); + exitAnimation.Add(fadeAnimation); + return exitAnimation; + } + + protected override CompositionAnimationGroup GetOrCreateSendBackAnimation(CompositionVisual element, double widthDistance, double heightDistance) + { + var compositor = element.Compositor; + + var scaleAnimation = compositor.CreateVector3DKeyFrameAnimation(); + scaleAnimation.Duration = Duration; + scaleAnimation.Target = nameof(element.Scale); + scaleAnimation.InsertKeyFrame(0f, new Vector3D(1, 1, 1), Easing); + scaleAnimation.InsertKeyFrame(EndingCue, new Vector3D(ZoomInFactor, ZoomInFactor, 1), Easing); + + var fadeAnimation = compositor.CreateScalarKeyFrameAnimation(); + fadeAnimation.Duration = Duration; + fadeAnimation.Target = nameof(element.Opacity); + fadeAnimation.InsertKeyFrame(0f, 1f, Easing); + fadeAnimation.InsertKeyFrame(EndingCue, 0f, Easing); + + var sendBackAnimation = compositor.CreateAnimationGroup(); + sendBackAnimation.Add(scaleAnimation); + sendBackAnimation.Add(fadeAnimation); + return sendBackAnimation; + } + + protected override CompositionAnimationGroup GetOrCreateBringBackAnimation(CompositionVisual element, double widthDistance, double heightDistance) + { + var compositor = element.Compositor; + + var scaleAnimation = compositor.CreateVector3DKeyFrameAnimation(); + scaleAnimation.Duration = Duration; + scaleAnimation.Target = nameof(element.Scale); + scaleAnimation.InsertKeyFrame(StartingCue, new Vector3D(ZoomInFactor, ZoomInFactor, 1), Easing); + scaleAnimation.InsertKeyFrame(1f, new Vector3D(1, 1, 1), Easing); + + var fadeAnimation = compositor.CreateScalarKeyFrameAnimation(); + fadeAnimation.Duration = Duration; + fadeAnimation.Target = nameof(element.Opacity); + fadeAnimation.InsertKeyFrame(StartingCue, 0f, Easing); + fadeAnimation.InsertKeyFrame(1f, 1f, Easing); + + var bringBackAnimation = compositor.CreateAnimationGroup(); + bringBackAnimation.Add(scaleAnimation); + bringBackAnimation.Add(fadeAnimation); + return bringBackAnimation; + } + + protected override Task RunAnimationAsync( + CompositionVisual parentComposition, + CompositionVisual? fromElement, + CompositionVisual? toElement, + bool forward, + double distance, + double heightDistance, + CancellationToken cancellationToken) + { + if (toElement != null) + toElement.CenterPoint = new Vector3D(parentComposition.Size.X * 0.5, parentComposition.Size.Y * 0.5, 0); + + if (fromElement != null) + fromElement.CenterPoint = new Vector3D(parentComposition.Size.X * 0.5, parentComposition.Size.Y * 0.5, 0); + + return base.RunAnimationAsync(parentComposition, fromElement, toElement, forward, distance, heightDistance, cancellationToken); + } +} diff --git a/src/Example/ShellBottomCustomNavigator/Directory.Build.props b/src/Example/ShellBottomCustomNavigator/Directory.Build.props deleted file mode 100644 index 74d63f7..0000000 --- a/src/Example/ShellBottomCustomNavigator/Directory.Build.props +++ /dev/null @@ -1,6 +0,0 @@ - - - enable - 11.0.7 - - diff --git a/src/Example/ShellBottomCustomNavigator/ShellBottomCustomNavigator.Android/MainActivity.cs b/src/Example/ShellBottomCustomNavigator/ShellBottomCustomNavigator.Android/MainActivity.cs index d235bbd..4f9e311 100644 --- a/src/Example/ShellBottomCustomNavigator/ShellBottomCustomNavigator.Android/MainActivity.cs +++ b/src/Example/ShellBottomCustomNavigator/ShellBottomCustomNavigator.Android/MainActivity.cs @@ -3,6 +3,7 @@ using Avalonia; using Avalonia.Android; using Avalonia.ReactiveUI; +using AvaloniaInside.Shell; namespace ShellBottomCustomNavigator.Android; @@ -18,6 +19,7 @@ protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) { return base.CustomizeAppBuilder(builder) .WithInterFont() + .UseShell() .UseReactiveUI(); } -} \ No newline at end of file +} diff --git a/src/Example/ShellBottomCustomNavigator/ShellBottomCustomNavigator.Browser/Program.cs b/src/Example/ShellBottomCustomNavigator/ShellBottomCustomNavigator.Browser/Program.cs index 83b77a7..dd4bcb1 100644 --- a/src/Example/ShellBottomCustomNavigator/ShellBottomCustomNavigator.Browser/Program.cs +++ b/src/Example/ShellBottomCustomNavigator/ShellBottomCustomNavigator.Browser/Program.cs @@ -3,6 +3,7 @@ using Avalonia; using Avalonia.Browser; using Avalonia.ReactiveUI; +using AvaloniaInside.Shell; using ShellBottomCustomNavigator; [assembly: SupportedOSPlatform("browser")] @@ -12,8 +13,9 @@ internal sealed partial class Program private static Task Main(string[] args) => BuildAvaloniaApp() .WithInterFont() .UseReactiveUI() + .UseShell() .StartBrowserAppAsync("out"); public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure(); -} \ No newline at end of file +} diff --git a/src/Example/ShellBottomCustomNavigator/ShellBottomCustomNavigator.iOS/AppDelegate.cs b/src/Example/ShellBottomCustomNavigator/ShellBottomCustomNavigator.iOS/AppDelegate.cs index 759995f..c917df7 100644 --- a/src/Example/ShellBottomCustomNavigator/ShellBottomCustomNavigator.iOS/AppDelegate.cs +++ b/src/Example/ShellBottomCustomNavigator/ShellBottomCustomNavigator.iOS/AppDelegate.cs @@ -5,11 +5,12 @@ using Avalonia.iOS; using Avalonia.Media; using Avalonia.ReactiveUI; +using AvaloniaInside.Shell; namespace ShellBottomCustomNavigator.iOS; -// The UIApplicationDelegate for the application. This class is responsible for launching the -// User Interface of the application, as well as listening (and optionally responding) to +// The UIApplicationDelegate for the application. This class is responsible for launching the +// User Interface of the application, as well as listening (and optionally responding) to // application events from iOS. [Register("AppDelegate")] #pragma warning disable CA1711 // Identifiers should not have incorrect suffix @@ -20,6 +21,7 @@ protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) { return base.CustomizeAppBuilder(builder) .WithInterFont() + .UseShell() .UseReactiveUI(); } -} \ No newline at end of file +} diff --git a/src/Example/ShellBottomCustomNavigator/ShellBottomCustomNavigator/ShellBottomCustomNavigator.csproj b/src/Example/ShellBottomCustomNavigator/ShellBottomCustomNavigator/ShellBottomCustomNavigator.csproj index 765b923..680219e 100644 --- a/src/Example/ShellBottomCustomNavigator/ShellBottomCustomNavigator/ShellBottomCustomNavigator.csproj +++ b/src/Example/ShellBottomCustomNavigator/ShellBottomCustomNavigator/ShellBottomCustomNavigator.csproj @@ -12,12 +12,12 @@ - - - - + + + + - + diff --git a/src/Example/ShellBottomCustomNavigator/ShellBottomCustomNavigator/Styles.axaml b/src/Example/ShellBottomCustomNavigator/ShellBottomCustomNavigator/Styles.axaml index 526dd69..e723cff 100644 --- a/src/Example/ShellBottomCustomNavigator/ShellBottomCustomNavigator/Styles.axaml +++ b/src/Example/ShellBottomCustomNavigator/ShellBottomCustomNavigator/Styles.axaml @@ -36,7 +36,7 @@ - + @@ -75,7 +75,7 @@ - - - + - + + + + + Black + White + + + White + Black + + + + + + + + diff --git a/src/AvaloniaInside.Shell/Page.cs b/src/AvaloniaInside.Shell/Page.cs index b43807c..d0408e8 100644 --- a/src/AvaloniaInside.Shell/Page.cs +++ b/src/AvaloniaInside.Shell/Page.cs @@ -1,4 +1,5 @@ -using Avalonia.Controls; +using System; +using Avalonia.Controls; using System.Threading; using System.Threading.Tasks; @@ -8,6 +9,8 @@ public class Page : UserControl, INavigationLifecycle, INavigatorLifecycle public ShellView? Shell { get; internal set; } public INavigator? Navigator => Shell?.Navigator; + protected override Type StyleKeyOverride => typeof(Page); + public virtual Task AppearAsync(CancellationToken cancellationToken) => Task.CompletedTask; public virtual Task ArgumentAsync(object args, CancellationToken cancellationToken) => Task.CompletedTask; public virtual Task DisappearAsync(CancellationToken cancellationToken) => Task.CompletedTask; diff --git a/src/AvaloniaInside.Shell/Platform/Android/AndroidDefaultPageSlide.cs b/src/AvaloniaInside.Shell/Platform/Android/AndroidDefaultPageSlide.cs index bbbda92..0aa805f 100644 --- a/src/AvaloniaInside.Shell/Platform/Android/AndroidDefaultPageSlide.cs +++ b/src/AvaloniaInside.Shell/Platform/Android/AndroidDefaultPageSlide.cs @@ -35,7 +35,7 @@ public class AndroidDefaultPageSlide : PlatformBasePageTransition /// public override Easing Easing { get; set; } = new FastOutExtraSlowInEasing(); - protected override CompositionAnimationGroup GetOrCreateEnteranceAnimation(CompositionVisual element, double widthDistance, double heightDistance) + protected override CompositionAnimationGroup GetOrCreateEntranceAnimation(CompositionVisual element, double widthDistance, double heightDistance) { var compositor = element.Compositor; diff --git a/src/AvaloniaInside.Shell/Platform/Android/MaterialListPageSlide.cs b/src/AvaloniaInside.Shell/Platform/Android/MaterialListPageSlide.cs index 6618499..3f048d8 100644 --- a/src/AvaloniaInside.Shell/Platform/Android/MaterialListPageSlide.cs +++ b/src/AvaloniaInside.Shell/Platform/Android/MaterialListPageSlide.cs @@ -23,7 +23,7 @@ public class MaterialListPageSlide : PlatformBasePageTransition /// public override Easing Easing { get; set; } = new FastOutExtraSlowInEasing(); - protected override CompositionAnimationGroup GetOrCreateEnteranceAnimation(CompositionVisual element, double widthDistance, double heightDistance) + protected override CompositionAnimationGroup GetOrCreateEntranceAnimation(CompositionVisual element, double widthDistance, double heightDistance) { var compositor = element.Compositor; diff --git a/src/AvaloniaInside.Shell/Platform/Ios/AlertTransition.cs b/src/AvaloniaInside.Shell/Platform/Ios/AlertTransition.cs index 1e7d122..e1235fe 100644 --- a/src/AvaloniaInside.Shell/Platform/Ios/AlertTransition.cs +++ b/src/AvaloniaInside.Shell/Platform/Ios/AlertTransition.cs @@ -1,41 +1,41 @@ -using Avalonia.Animation.Easings; -using Avalonia.Rendering.Composition.Animations; -using Avalonia.Rendering.Composition; -using Avalonia; -using System; +using System; using System.Threading; using System.Threading.Tasks; -using AvaloniaInside.Shell.Platform.Ios; +using Avalonia; +using Avalonia.Animation.Easings; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Animations; + +namespace AvaloniaInside.Shell.Platform.Ios; -namespace AvaloniaInside.Shell.Platform.Windows; public class AlertTransition : PlatformBasePageTransition { public static readonly AlertTransition Instance = new(); - private const float EndingCue = 0.7f; - private const float StartingCue = 0.3f; + private const float EndingCue = 0.9f; + private const float StartingCue = 0.1f; /// /// Gets the duration of the animation. /// - public override TimeSpan Duration { get; set; } = TimeSpan.FromSeconds(0.25); + public override TimeSpan Duration { get; set; } = TimeSpan.FromSeconds(.25); /// /// Scale factor to animate in /// - public float ZoomInFactor { get; set; } = 1.25f; + public float ZoomInFactor { get; set; } = 1f; /// /// Scale factor to animate out /// - public float ZoomOutFactor { get; set; } = 0.75f; + public float ZoomOutFactor { get; set; } = 1.1f; /// /// Gets or sets element entrance easing. /// - public override Easing Easing { get; set; } = Easing.Parse("0.85, 0.0, 0.0, 1.0"); + public override Easing Easing { get; set; } = Easing.Parse("1.0, 0.0, 0.0, 0.85"); - protected override CompositionAnimationGroup GetOrCreateEnteranceAnimation(CompositionVisual element, double widthDistance, double heightDistance) + protected override CompositionAnimationGroup GetOrCreateEntranceAnimation(CompositionVisual element, double widthDistance, double heightDistance) { var compositor = element.Compositor; diff --git a/src/AvaloniaInside.Shell/Platform/Ios/IosModalSlide.cs b/src/AvaloniaInside.Shell/Platform/Ios/IosModalSlide.cs new file mode 100644 index 0000000..2103524 --- /dev/null +++ b/src/AvaloniaInside.Shell/Platform/Ios/IosModalSlide.cs @@ -0,0 +1,136 @@ +using System.Threading; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Animations; + +namespace AvaloniaInside.Shell.Platform.Ios; + +public class IosModalSlide : PlatformBasePageTransition +{ + public static readonly IosModalSlide Instance = new(); + + protected override CompositionAnimationGroup GetOrCreateEntranceAnimation(CompositionVisual element, double widthDistance, double heightDistance) + { + var compositor = element.Compositor; + + var offsetAnimation = compositor.CreateVector3DKeyFrameAnimation(); + offsetAnimation.Duration = Duration; + offsetAnimation.Target = nameof(element.Offset); + offsetAnimation.InsertKeyFrame(0f, new Vector3D(0, heightDistance, 0), Easing); + offsetAnimation.InsertKeyFrame(1.0f, new Vector3D(0, 40, 0), Easing); + + var fadeAnimation = compositor.CreateScalarKeyFrameAnimation(); + fadeAnimation.Duration = Duration; + fadeAnimation.Target = nameof(element.Opacity); + fadeAnimation.InsertKeyFrame(0f, 0.9f); + fadeAnimation.InsertKeyFrame(0.5f, 1f); + + var entranceAnimation = compositor.CreateAnimationGroup(); + entranceAnimation.Add(offsetAnimation); + entranceAnimation.Add(fadeAnimation); + return entranceAnimation; + } + + protected override CompositionAnimationGroup GetOrCreateExitAnimation(CompositionVisual element, double widthDistance, double heightDistance) + { + ShouldHideAfterExit = false; + + var compositor = element.Compositor; + + var offsetAnimation = compositor.CreateVector3DKeyFrameAnimation(); + offsetAnimation.Duration = Duration; + offsetAnimation.Target = nameof(element.Offset); + offsetAnimation.InsertKeyFrame(0f, new Vector3D(0, 40, 0), Easing); + offsetAnimation.InsertKeyFrame(1.0f, new Vector3D(0, heightDistance, 0), Easing); + + var fadeAnimation = compositor.CreateScalarKeyFrameAnimation(); + fadeAnimation.Duration = Duration; + fadeAnimation.Target = nameof(element.Opacity); + fadeAnimation.InsertKeyFrame(.5f, 1f); + fadeAnimation.InsertKeyFrame(1f, 0.9f); + + var exitAnimation = compositor.CreateAnimationGroup(); + exitAnimation.Add(offsetAnimation); + exitAnimation.Add(fadeAnimation); + return exitAnimation; + } + + protected override CompositionAnimationGroup GetOrCreateSendBackAnimation(CompositionVisual element, double widthDistance, double heightDistance) + { + ShouldHideAfterExit = false; + + var compositor = element.Compositor; + + var offsetAnimation = compositor.CreateVector3DKeyFrameAnimation(); + offsetAnimation.Duration = Duration; + offsetAnimation.Target = nameof(element.Offset); + offsetAnimation.InsertKeyFrame(0f, new Vector3D(0, 0, 0), Easing); + offsetAnimation.InsertKeyFrame(1.0f, new Vector3D(0, 10, 0), Easing); + + var scaleAnimation = compositor.CreateVector3DKeyFrameAnimation(); + scaleAnimation.Duration = Duration; + scaleAnimation.Target = nameof(element.Scale); + scaleAnimation.InsertKeyFrame(0f, new Vector3D(1f, 1f, 0), Easing); + scaleAnimation.InsertKeyFrame(1f, new Vector3D(.9f, .97f, 0), Easing); + + var fadeAnimation = compositor.CreateScalarKeyFrameAnimation(); + fadeAnimation.Duration = Duration; + fadeAnimation.Target = nameof(element.Opacity); + fadeAnimation.InsertKeyFrame(.5f, 1f); + fadeAnimation.InsertKeyFrame(1f, 0.8f); + + var sendBackAnimation = compositor.CreateAnimationGroup(); + sendBackAnimation.Add(offsetAnimation); + sendBackAnimation.Add(scaleAnimation); + sendBackAnimation.Add(fadeAnimation); + return sendBackAnimation; + } + + protected override CompositionAnimationGroup GetOrCreateBringBackAnimation(CompositionVisual element, double widthDistance, double heightDistance) + { + var compositor = element.Compositor; + + var offsetAnimation = compositor.CreateVector3DKeyFrameAnimation(); + offsetAnimation.Duration = Duration; + offsetAnimation.Target = nameof(element.Offset); + offsetAnimation.InsertKeyFrame(0f, new Vector3D(0, 10, 0), Easing); + offsetAnimation.InsertKeyFrame(1.0f, new Vector3D(0, 0, 0), Easing); + + var scaleAnimation = compositor.CreateVector3DKeyFrameAnimation(); + scaleAnimation.Duration = Duration; + scaleAnimation.Target = nameof(element.Scale); + scaleAnimation.InsertKeyFrame(0f, new Vector3D(.9f, .97f, 0), Easing); + scaleAnimation.InsertKeyFrame(1.0f, new Vector3D(1f, 1f, 0), Easing); + + var fadeAnimation = compositor.CreateScalarKeyFrameAnimation(); + fadeAnimation.Duration = Duration; + fadeAnimation.Target = nameof(element.Opacity); + fadeAnimation.InsertKeyFrame(0f, 0.9f); + fadeAnimation.InsertKeyFrame(0.5f, 1f); + + var bringBackAnimation = compositor.CreateAnimationGroup(); + bringBackAnimation.Add(offsetAnimation); + bringBackAnimation.Add(scaleAnimation); + bringBackAnimation.Add(fadeAnimation); + return bringBackAnimation; + } + + protected override Task RunAnimationAsync( + CompositionVisual parentComposition, + CompositionVisual? fromElement, + CompositionVisual? toElement, + bool forward, + double distance, + double heightDistance, + CancellationToken cancellationToken) + { + if (toElement != null) + toElement.CenterPoint = new Vector3D(parentComposition.Size.X * 0.5, parentComposition.Size.Y * 0.5, 0); + + if (fromElement != null) + fromElement.CenterPoint = new Vector3D(parentComposition.Size.X * 0.5, parentComposition.Size.Y * 0.5, 0); + + return base.RunAnimationAsync(parentComposition, fromElement, toElement, forward, distance, heightDistance, cancellationToken); + } +} diff --git a/src/AvaloniaInside.Shell/Platform/Ios/IosPageSlide.cs b/src/AvaloniaInside.Shell/Platform/Ios/IosPageSlide.cs index 2a22fe6..1c5502f 100644 --- a/src/AvaloniaInside.Shell/Platform/Ios/IosPageSlide.cs +++ b/src/AvaloniaInside.Shell/Platform/Ios/IosPageSlide.cs @@ -1,4 +1,5 @@ -using Avalonia; +using System; +using Avalonia; using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition.Animations; @@ -6,9 +7,12 @@ namespace AvaloniaInside.Shell.Platform.Ios; public class DefaultIosPageSlide : PlatformBasePageTransition { + private const float ScaleOutDuration = .8f; + public static readonly DefaultIosPageSlide Instance = new(); + public override TimeSpan Duration { get; set; } = TimeSpan.FromSeconds(.25); - protected override CompositionAnimationGroup GetOrCreateEnteranceAnimation(CompositionVisual element, double widthDistance, double heightDistance) + protected override CompositionAnimationGroup GetOrCreateEntranceAnimation(CompositionVisual element, double widthDistance, double heightDistance) { var compositor = element.Compositor; @@ -35,13 +39,13 @@ protected override CompositionAnimationGroup GetOrCreateExitAnimation(Compositio var compositor = element.Compositor; var offsetAnimation = compositor.CreateVector3DKeyFrameAnimation(); - offsetAnimation.Duration = Duration; + offsetAnimation.Duration = Duration * ScaleOutDuration; offsetAnimation.Target = nameof(element.Offset); offsetAnimation.InsertKeyFrame(0f, new Vector3D(0, 0, 0), Easing); offsetAnimation.InsertKeyFrame(1.0f, new Vector3D(widthDistance, 0, 0), Easing); var fadeAnimation = compositor.CreateScalarKeyFrameAnimation(); - fadeAnimation.Duration = Duration; + fadeAnimation.Duration = Duration * ScaleOutDuration; fadeAnimation.Target = nameof(element.Opacity); fadeAnimation.InsertKeyFrame(.5f, 1f); fadeAnimation.InsertKeyFrame(1f, 0.9f); @@ -79,13 +83,13 @@ protected override CompositionAnimationGroup GetOrCreateBringBackAnimation(Compo var compositor = element.Compositor; var offsetAnimation = compositor.CreateVector3DKeyFrameAnimation(); - offsetAnimation.Duration = Duration; + offsetAnimation.Duration = Duration * ScaleOutDuration; offsetAnimation.Target = nameof(element.Offset); offsetAnimation.InsertKeyFrame(0f, new Vector3D(widthDistance / -4d, 0, 0), Easing); offsetAnimation.InsertKeyFrame(1.0f, new Vector3D(0, 0, 0), Easing); var fadeAnimation = compositor.CreateScalarKeyFrameAnimation(); - fadeAnimation.Duration = Duration; + fadeAnimation.Duration = Duration * ScaleOutDuration; fadeAnimation.Target = nameof(element.Opacity); fadeAnimation.InsertKeyFrame(0f, .9f); fadeAnimation.InsertKeyFrame(1f, 1f); diff --git a/src/AvaloniaInside.Shell/Platform/PlatformBasePageTransition.cs b/src/AvaloniaInside.Shell/Platform/PlatformBasePageTransition.cs index 959093b..379f682 100644 --- a/src/AvaloniaInside.Shell/Platform/PlatformBasePageTransition.cs +++ b/src/AvaloniaInside.Shell/Platform/PlatformBasePageTransition.cs @@ -11,12 +11,12 @@ namespace AvaloniaInside.Shell.Platform; public abstract class PlatformBasePageTransition : IPageTransition { - private CompositionAnimationGroup? _enteranceAnimation; + private CompositionAnimationGroup? _entranceAnimation; private CompositionAnimationGroup? _exitAnimation; private CompositionAnimationGroup? _sendBackAnimation; private CompositionAnimationGroup? _bringBackAnimation; - private double _lastDistance = 0; + private readonly double _lastDistance = 0; /// /// Gets the duration of the animation. @@ -28,7 +28,9 @@ public abstract class PlatformBasePageTransition : IPageTransition /// public virtual Easing Easing { get; set; } = Easing.Parse("0.42, 0.0, 0.58, 1.0"); - protected abstract CompositionAnimationGroup GetOrCreateEnteranceAnimation(CompositionVisual element, double distance, double heightDistance); + protected virtual bool ShouldHideAfterExit { get; set; } = true; + + protected abstract CompositionAnimationGroup GetOrCreateEntranceAnimation(CompositionVisual element, double distance, double heightDistance); protected abstract CompositionAnimationGroup GetOrCreateExitAnimation(CompositionVisual element, double distance, double heightDistance); @@ -52,7 +54,7 @@ public async Task Start(Visual? from, Visual? to, bool forward, CancellationToke if (distance != _lastDistance) { - _enteranceAnimation = null; + _entranceAnimation = null; _exitAnimation = null; _sendBackAnimation = null; _bringBackAnimation = null; @@ -60,7 +62,7 @@ public async Task Start(Visual? from, Visual? to, bool forward, CancellationToke if (to != null) to.IsVisible = true; await RunAnimationAsync(parentComposition, fromElement, toElement, forward, parent.Bounds.Width, parent.Bounds.Height, cancellationToken); - if (from != null) from.IsVisible = false; + if (from != null && ShouldHideAfterExit) from.IsVisible = false; } protected virtual Task RunAnimationAsync( @@ -75,7 +77,7 @@ protected virtual Task RunAnimationAsync( if (toElement != null) { var animation = forward - ? _enteranceAnimation ??= GetOrCreateEnteranceAnimation(parentComposition, widthDistance, heightDistance) + ? _entranceAnimation ??= GetOrCreateEntranceAnimation(parentComposition, widthDistance, heightDistance) : _bringBackAnimation ??= GetOrCreateBringBackAnimation(parentComposition, widthDistance, heightDistance); toElement.StartAnimationGroup(animation); diff --git a/src/AvaloniaInside.Shell/Platform/PlatformSetup.cs b/src/AvaloniaInside.Shell/Platform/PlatformSetup.cs index e328dc7..228229a 100644 --- a/src/AvaloniaInside.Shell/Platform/PlatformSetup.cs +++ b/src/AvaloniaInside.Shell/Platform/PlatformSetup.cs @@ -11,6 +11,8 @@ public static IPageTransition TransitionForPage { get { + return DefaultIosPageSlide.Instance; + if (OperatingSystem.IsAndroid()) return AndroidDefaultPageSlide.Instance; if (OperatingSystem.IsIOS()) @@ -27,6 +29,8 @@ public static IPageTransition TransitionForList { get { + return DefaultIosPageSlide.Instance; + if (OperatingSystem.IsAndroid()) return MaterialListPageSlide.Instance; if (OperatingSystem.IsIOS()) @@ -49,4 +53,22 @@ public static IPageTransition? TransitionForTab return MaterialListPageSlide.Instance; } } + + public static IPageTransition TransitionForModal + { + get + { + return AlertTransition.Instance; + + if (OperatingSystem.IsAndroid()) + return AndroidDefaultPageSlide.Instance; + if (OperatingSystem.IsIOS()) + return AlertTransition.Instance; + if (OperatingSystem.IsWindows()) + return DrillInNavigationTransition.Instance;; + + //Default for the moment + return DrillInNavigationTransition.Instance; + } + } } diff --git a/src/AvaloniaInside.Shell/Platform/Windows/DrillInNavigationTransition.cs b/src/AvaloniaInside.Shell/Platform/Windows/DrillInNavigationTransition.cs index 9694455..fb3122e 100644 --- a/src/AvaloniaInside.Shell/Platform/Windows/DrillInNavigationTransition.cs +++ b/src/AvaloniaInside.Shell/Platform/Windows/DrillInNavigationTransition.cs @@ -35,7 +35,7 @@ public class DrillInNavigationTransition : PlatformBasePageTransition /// public override Easing Easing { get; set; } = Easing.Parse("0.85, 0.0, 0.0, 1.0"); - protected override CompositionAnimationGroup GetOrCreateEnteranceAnimation(CompositionVisual element, double widthDistance, double heightDistance) + protected override CompositionAnimationGroup GetOrCreateEntranceAnimation(CompositionVisual element, double widthDistance, double heightDistance) { var compositor = element.Compositor; diff --git a/src/AvaloniaInside.Shell/Platform/Windows/EntranceNavigationTransition.cs b/src/AvaloniaInside.Shell/Platform/Windows/EntranceNavigationTransition.cs index 17423d4..425794d 100644 --- a/src/AvaloniaInside.Shell/Platform/Windows/EntranceNavigationTransition.cs +++ b/src/AvaloniaInside.Shell/Platform/Windows/EntranceNavigationTransition.cs @@ -31,7 +31,7 @@ public class EntranceNavigationTransition : PlatformBasePageTransition public Easing Easing2 { get; set; } = Easing.Parse("0.85, 0.0, 0.75, 1.0"); public Easing Easing3 { get; set; } = Easing.Parse("0.85, 0.0, 0.0, 1.0"); - protected override CompositionAnimationGroup GetOrCreateEnteranceAnimation(CompositionVisual element, double widthDistance, double heightDistance) + protected override CompositionAnimationGroup GetOrCreateEntranceAnimation(CompositionVisual element, double widthDistance, double heightDistance) { var compositor = element.Compositor; diff --git a/src/AvaloniaInside.Shell/Platform/Windows/ListSlideNavigationTransition.cs b/src/AvaloniaInside.Shell/Platform/Windows/ListSlideNavigationTransition.cs index 73a9bb3..d93a466 100644 --- a/src/AvaloniaInside.Shell/Platform/Windows/ListSlideNavigationTransition.cs +++ b/src/AvaloniaInside.Shell/Platform/Windows/ListSlideNavigationTransition.cs @@ -18,7 +18,7 @@ public class ListSlideNavigationTransition : PlatformBasePageTransition public float FadeFactor { get; set; } = 0.7f; - protected override CompositionAnimationGroup GetOrCreateEnteranceAnimation(CompositionVisual element, double widthDistance, double heightDistance) + protected override CompositionAnimationGroup GetOrCreateEntranceAnimation(CompositionVisual element, double widthDistance, double heightDistance) { var compositor = element.Compositor; diff --git a/src/AvaloniaInside.Shell/ShellView.cs b/src/AvaloniaInside.Shell/ShellView.cs index 750124e..90e0ce0 100644 --- a/src/AvaloniaInside.Shell/ShellView.cs +++ b/src/AvaloniaInside.Shell/ShellView.cs @@ -8,6 +8,7 @@ using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Threading; +using AvaloniaInside.Shell.Platform; using AvaloniaInside.Shell.Platform.Windows; using ReactiveUI; using Splat; @@ -180,7 +181,7 @@ public bool ApplyBottomSafePadding #endregion - #region ApplyBottomSafePadding + #region DefaultPageTransition /// /// Defines the property. @@ -188,7 +189,7 @@ public bool ApplyBottomSafePadding public static readonly StyledProperty DefaultPageTransitionProperty = AvaloniaProperty.Register( nameof(DefaultPageTransition), - defaultValue: new DrillInNavigationTransition()); + defaultValue: PlatformSetup.TransitionForPage); /// /// Gets or sets the animation played when content appears and disappears. @@ -201,6 +202,27 @@ public IPageTransition? DefaultPageTransition #endregion + #region ModalPageTransition + + /// + /// Defines the property. + /// + public static readonly StyledProperty ModalPageTransitionProperty = + AvaloniaProperty.Register( + nameof(ModalPageTransition), + defaultValue: PlatformSetup.TransitionForModal); + + /// + /// Gets or sets the animation played when content appears and disappears. + /// + public IPageTransition? ModalPageTransition + { + get => GetValue(ModalPageTransitionProperty); + set => SetValue(ModalPageTransitionProperty, value); + } + + #endregion + #endregion #region Attached properties diff --git a/src/AvaloniaInside.Shell/StackContentView.cs b/src/AvaloniaInside.Shell/StackContentView.cs index a4e0601..192fb50 100644 --- a/src/AvaloniaInside.Shell/StackContentView.cs +++ b/src/AvaloniaInside.Shell/StackContentView.cs @@ -1,4 +1,3 @@ -using System.Collections.Specialized; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -6,199 +5,117 @@ using Avalonia.Animation; using Avalonia.Controls; using AvaloniaInside.Shell.Platform; -using AvaloniaInside.Shell.Platform.Windows; namespace AvaloniaInside.Shell; -public class StackContentViewPanel : Panel +public class StackContentView : Panel { - public Task? AnimationToBeDone { get; set; } - - protected override Size ArrangeOverride(Size finalSize) - { - var rcChild = new Rect(finalSize); - var zindex = 0; - foreach (var control in Children) - { - control.ZIndex = zindex++; - control.Arrange(rcChild); - } - - return finalSize; - } - - protected override async void ChildrenChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - if (e.Action == NotifyCollectionChangedAction.Remove && AnimationToBeDone != null) - { - await AnimationToBeDone; - AnimationToBeDone = null; - } - base.ChildrenChanged(sender, e); - } -} - -public class StackContentViewItem : ContentControl -{ - -} - -public class StackContentView : ItemsControl -{ - private readonly SemaphoreSlim _semaphoreSlim = new(1, 1); - private NavigateType? _pendingNavigateType; - private Control? _lastContainer; - public static readonly StyledProperty HasContentProperty = - AvaloniaProperty.Register(nameof(HasContent)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty PageTransitionProperty = - AvaloniaProperty.Register( - nameof(PageTransition), - defaultValue: PlatformSetup.TransitionForPage); - - /// - /// Gets or sets the animation played when content appears and disappears. - /// - public IPageTransition? PageTransition - { - get => GetValue(PageTransitionProperty); - set => SetValue(PageTransitionProperty, value); - } - - public StackContentView() - { - //ItemsPanel = new ItemsPanelTemplate() { Content = new StackContentViewPanel() }; - } - - protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey) => - new StackContentViewItem(); - - protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey) => - NeedsContainer(item, out recycleKey); - - protected override void PrepareContainerForItemOverride(Control element, object? item, int index) - { - base.PrepareContainerForItemOverride(element, item, index); - } - - protected override void ContainerForItemPreparedOverride(Control container, object? item, int index) - { - base.ContainerForItemPreparedOverride(container, item, index); - - if (index == Items.Count - 1) - { - _lastContainer = container; - _ = PageTransition?.Start( - Items.Count > 1 ? ContainerFromIndex(index - 1) : null, - container, - true, - CancellationToken.None); - } - } - - protected override async void ClearContainerForItemOverride(Control container) - { - var currentContainer = ContainerFromIndex(Items.Count - 1); - - if (_lastContainer != container) - { - base.ClearContainerForItemOverride(container); - return; - } - - var task = (PageTransition?.Start( - container, - currentContainer, - false, - CancellationToken.None) ?? Task.CompletedTask); - - if (ItemsPanelRoot is StackContentViewPanel panel) - panel.AnimationToBeDone = task; - - await task; - - base.ClearContainerForItemOverride(container); - _lastContainer = currentContainer; - } - - public bool HasContent - { - get => GetValue(HasContentProperty); - private set => SetValue(HasContentProperty, value); - } - - public object? CurrentView => Items.LastOrDefault(); - - public async Task PushViewAsync(object view, - NavigateType navigateType, - CancellationToken cancellationToken = default) - { - await _semaphoreSlim.WaitAsync(cancellationToken); - try - { - var current = CurrentView; - - if (current != null && current == view) return; - if (view is not Control control) return; - - // Bring to front if exists in collection - if (Items.Contains(control)) - Items.Remove(control); - Items.Add(control); - - UpdateCurrentView(current, control, navigateType, false); - - await OnContentUpdateAsync(control, cancellationToken); - } - finally - { - _semaphoreSlim.Release(); - } - } - - protected virtual void UpdateCurrentView(object? from, object? to, NavigateType navigateType, bool removed) - { - //TODO: Apply specific animation type - } - - public async Task RemoveViewAsync(object view, NavigateType navigateType, CancellationToken cancellationToken) - { - await _semaphoreSlim.WaitAsync(cancellationToken); - try - { - if (!Items.Contains(view)) return false; - var from = CurrentView; - - Items.Remove(view as Control); - - var to = CurrentView; - if (from != to) - { - UpdateCurrentView(from, to, navigateType, true); - await OnContentUpdateAsync(CurrentView, cancellationToken); - } - return true; - } - finally - { - _semaphoreSlim.Release(); - } - } - - protected virtual Task OnContentUpdateAsync(object? view, CancellationToken cancellationToken) - { - HasContent = Items.Count > 0; - return Task.CompletedTask; - } - - public Task ClearStackAsync(CancellationToken cancellationToken) - { - while (Items.Count > 1) - Items.RemoveAt(0); - - return Task.CompletedTask; - } + private readonly SemaphoreSlim _semaphoreSlim = new(1, 1); + private NavigateType? _pendingNavigateType; + + public static readonly StyledProperty HasContentProperty = + AvaloniaProperty.Register(nameof(HasContent)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty PageTransitionProperty = + AvaloniaProperty.Register( + nameof(PageTransition), + defaultValue: PlatformSetup.TransitionForPage); + + /// + /// Gets or sets the animation played when content appears and disappears. + /// + public IPageTransition? PageTransition + { + get => GetValue(PageTransitionProperty); + set => SetValue(PageTransitionProperty, value); + } + + public bool HasContent + { + get => GetValue(HasContentProperty); + private set => SetValue(HasContentProperty, value); + } + + public object? CurrentView => Children.LastOrDefault(); + + public async Task PushViewAsync(object view, + NavigateType navigateType, + CancellationToken cancellationToken = default) + { + await _semaphoreSlim.WaitAsync(cancellationToken); + try + { + var current = CurrentView; + + if (current != null && current == view) return; + if (view is not Control control) return; + + // Bring to front if exists in collection + if (Children.Contains(control)) + Children.Remove(control); + Children.Add(control); + + await OnContentUpdateAsync(control, cancellationToken); + await UpdateCurrentViewAsync(current, control, navigateType, false, cancellationToken); + } + finally + { + _semaphoreSlim.Release(); + } + } + + protected virtual Task UpdateCurrentViewAsync( + object? from, + object? to, + NavigateType navigateType, + bool removed, + CancellationToken cancellationToken) + { + return PageTransition?.Start( + from as Visual, + to as Visual, + !removed, + cancellationToken) ?? Task.CompletedTask; + } + + public async Task RemoveViewAsync(object view, NavigateType navigateType, CancellationToken cancellationToken) + { + await _semaphoreSlim.WaitAsync(cancellationToken); + try + { + if (!Children.Contains(view)) return false; + + if (CurrentView == view) + { + var to = Children.Count > 1 ? Children[^2] : null; + await UpdateCurrentViewAsync(view, to, navigateType, true, cancellationToken); + } + + Children.Remove(view as Control); + await OnContentUpdateAsync(CurrentView, cancellationToken); + + return true; + } + finally + { + _semaphoreSlim.Release(); + } + } + + protected virtual Task OnContentUpdateAsync(object? view, CancellationToken cancellationToken) + { + HasContent = Children.Count > 0; + return Task.CompletedTask; + } + + public Task ClearStackAsync(CancellationToken cancellationToken) + { + while (Children.Count > 1) + Children.RemoveAt(0); + + return Task.CompletedTask; + } } diff --git a/src/AvaloniaInside.Shell/Theme/StackContentView.axaml b/src/AvaloniaInside.Shell/Theme/StackContentView.axaml index 665cd26..b6eb5ba 100644 --- a/src/AvaloniaInside.Shell/Theme/StackContentView.axaml +++ b/src/AvaloniaInside.Shell/Theme/StackContentView.axaml @@ -1,21 +1,6 @@ - - - - - - - - - - diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 5f86bb8..31a9a50 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,5 +1,5 @@ - 11.0.10 + 11.1.1 diff --git a/src/Example/ShellExample/ShellExample.iOS/ShellExample.iOS.csproj b/src/Example/ShellExample/ShellExample.iOS/ShellExample.iOS.csproj index 7be2a18..1ef4603 100644 --- a/src/Example/ShellExample/ShellExample.iOS/ShellExample.iOS.csproj +++ b/src/Example/ShellExample/ShellExample.iOS/ShellExample.iOS.csproj @@ -1,7 +1,7 @@  Exe - net7.0-ios + net8.0-ios 13.0 automatic enable diff --git a/src/Example/ShellExample/ShellExample/ShellExample.csproj b/src/Example/ShellExample/ShellExample/ShellExample.csproj index 08aff80..67f83c1 100644 --- a/src/Example/ShellExample/ShellExample/ShellExample.csproj +++ b/src/Example/ShellExample/ShellExample/ShellExample.csproj @@ -15,7 +15,7 @@ - + diff --git a/src/Example/ShellExample/ShellExample/Styles.axaml b/src/Example/ShellExample/ShellExample/Styles.axaml index 557ab1e..c1360e2 100644 --- a/src/Example/ShellExample/ShellExample/Styles.axaml +++ b/src/Example/ShellExample/ShellExample/Styles.axaml @@ -60,6 +60,8 @@