SimpleShell allows you to define custom page transitions. There are two types of transitions that can be used:
- Platform-specific transitions — transitions provided by the platform-specific APIs and controls. These transitions are used by default.
- Universal transitions — fully cross-platform transitions that are defined using only .NET MAUI APIs.
These two types cannot be combined in one application. You can only use one or the other.
Platform-specific transitions are transitions provided by the platform-specific APIs and controls:
- View animations and fragment transactions on Android
UIViewanimations andUINavigationControllertransitions on iOS/Mac CatalystEntranceThemeTransitionandNavigationTransitionInfoobjects and theFramecontrol on Windows (WinUI)
There are predefined platform-specific transitions in SimpleShell, which are used by default. Defining your own custom platform-specific transitions requires knowledge of the underlying technologies.
By using platform-specific APIs more directly, transition animations of this type are quite a bit smoother than the universal ones. On the other hand, these animations will probably never look the same on all supported platforms.
I recommend choosing this type of transitions when platform-specific look and feel of your transitions is required and when you do not need to take a full control over your transitions appearance.
Custom platform-specific transition configuration is represented by a PlatformSimpleShellTransition object, which contains different properties by platform. The PlatformSimpleShellTransition class implements the ISimpleShellTransition interface.
Transitions can be defined separately for each state of the navigation. There are three states:
- Switching — new root page (
ShellContent) is being set - Pushing — new page is being pushed to the navigation stack
- Popping — existing page is being popped from the navigation stack
These are all the PlatformSimpleShellTransition properties by platform:
-
Android:
SwitchingEnterAnimation— a method returning the ID of an animation that is applied to the entering page on page switchingSwitchingLeaveAnimation— a method returning the ID of an animation that is applied to the leaving page on page switchingPushingEnterAnimation— a method returning the ID of an animation or animator that is applied to the entering page on page pushingPushingLeaveAnimation— a method returning the ID of an animation or animator that is applied to the leaving page on page pushingPoppingEnterAnimation— a method returning the ID of an animation or animator that is applied to the entering page on page poppingPoppingLeaveAnimation— a method returning the ID of an animation or animator that is applied to the leaving page on page poppingDestinationPageInFrontOnSwitching— a method returning whether the destination page should be displayed in front of the origin page on page switchingDestinationPageInFrontOnPushing— a method returning whether the destination page should be displayed in front of the origin page on page pushingDestinationPageInFrontOnPopping— a method returning whether the destination page should be displayed in front of the origin page on page popping
Tip
Visit the official documentation for more information about the view animations.
-
iOS/Mac Catalyst:
DestinationPageInFrontOnSwitching— a method returning whether the destination page should be displayed in front of the origin page on page switchingSwitchingAnimation— a method returning a switching animation represented by an action with two parameters for platform views (of typeUIView) of the origin and destination pages. This is where you change any animatable properties of the platform views. The change will be automatically animated.SwitchingAnimationDuration— a method returning a duration of the switching animationSwitchingAnimationStarting— a method returning an action which is called before the switching animation starts. All preparatory work (such as setting initial values of the animated properties) should be done in this action. This action has two parameters for platform views (of typeUIView) of the origin and destination pagesSwitchingAnimationFinished— a method returning an action which is called right after the switching animation plays. All cleaning work (such as setting the values of the animated properties back to initial values) should be done in this action. This action has two parameters for platform views (of typeUIView) of the origin and destination pagesPushingAnimation— a method returning an object of typeIUIViewControllerAnimatedTransitioning, which represents the animation that is played on page pushingPoppingAnimation— a method returning an object of typeIUIViewControllerAnimatedTransitioning, which represents the animation that is played on page popping
Tip
Visit the official documentation for more information about the UIView animations, IUIViewControllerAnimatedTransitioning interface and UINavigationController transitions.
-
Windows (WinUI):
SwitchingAnimation— a method returning anEntranceThemeTransitionobject that is applied on page switchingPushingAnimation— a method returning aNavigationTransitionInfoobject that is applied on page pushingPoppingAnimation— a method returning aNavigationTransitionInfoobject that is applied on page popping
Tip
Visit the official WinUI documentation for more information about the EntranceThemeTransition and NavigationTransitionInfo classes.
If a property is set to null, the default option is used.
Each of these methods takes a SimpleShellTransitionArgs object as a parameter. This object provides useful information which can help deciding which animation should be used:
OriginPageof typeVisualElement— page from which the navigation is initiatedDestinationPageof typeVisualElement— destination page of the navigationOriginShellSectionContainerof typeVisualElement—ShellSectioncontainer from which the navigation is initiated. Can benullif no container is definedDestinationShellSectionContainerof typeVisualElement— destinationShellSectioncontainer of the navigation. Can benullif no container is definedOriginShellItemContainerof typeVisualElement—ShellItemcontainer from which the navigation is initiated. Can benullif no container is definedDestinationShellItemContainerof typeVisualElement— destinationShellItemcontainer of the navigation. Can benullif no container is definedShell— current instance ofSimpleShellIsOriginPageRoot— whether the origin page is a root pageIsDestinationPageRoot— whether the destination page is a root pageProgress— progress of the transition. Number from 0 to 1. For platform-specific transitions, this property is always set to 0TransitionType— type of the transition that is represented bySimpleShellTransitionTypeenumeration:Switching— new root page (ShellContent) is being setPushing— new page is being pushed to the navigation stackPopping— existing page is being popped from the navigation stack
Let's define a new class called Transitions and create a new CustomPlatformTransition property for custom platform-specific transitions configuration:
public static class Transitions
{
public static PlatformSimpleShellTransition CustomPlatformTransition => new PlatformSimpleShellTransition
{
#if ANDROID
SwitchingEnterAnimation = static (args) => Resource.Animation.nav_default_enter_anim,
SwitchingLeaveAnimation = static (args) => Resource.Animation.nav_default_exit_anim,
PushingEnterAnimation = static (args) => Resource.Animator.flip_right_in,
PushingLeaveAnimation = static (args) => Resource.Animator.flip_right_out,
PoppingEnterAnimation = static (args) => Resource.Animator.flip_left_in,
PoppingLeaveAnimation = static (args) => Resource.Animator.flip_left_out,
#elif IOS || MACCATALYST
SwitchingAnimationDuration = static (args) => 0.3,
SwitchingAnimation = static (args) => static (from, to) =>
{
from.Alpha = 0;
to.Alpha = 1;
},
SwitchingAnimationStarting = static (args) => static (from, to) =>
{
to.Alpha = 0;
},
SwitchingAnimationFinished = static (args) => static (from, to) =>
{
from.Alpha = 1;
},
PushingAnimation = static (args) => new AppleTransitioning(true),
PoppingAnimation = static (args) => new AppleTransitioning(false),
#elif WINDOWS
PushingAnimation = static (args) => new Microsoft.UI.Xaml.Media.Animation.DrillInNavigationTransitionInfo(),
PoppingAnimation = static (args) => new Microsoft.UI.Xaml.Media.Animation.DrillInNavigationTransitionInfo(),
#endif
};
}Note
This code can also be found in the Sample.SimpleShell project.
As you can see, conditional compilation is used to access properties of different platforms. For each platform, different transitions are defined:
Android
On Android, just IDs of predefined or custom-defined Android animations and animators are returned. These IDs can be accessed through the Resource class.
Custom Android animations and animators can be defined in the /Platforms/Android/Resources/anim and /Platforms/Android/Resources/animator folders. The flip_right_in animator, for example, looks like this:
<set
xmlns:android="http://schemas.android.com/apk/res/android"
android:ordering="together">
<!-- Before rotating, immediately set the alpha to 0. -->
<objectAnimator
android:valueFrom="1.0"
android:valueTo="0.0"
android:propertyName="alpha"
android:duration="0" />
<!-- Rotate. -->
<objectAnimator
android:valueFrom="180"
android:valueTo="0"
android:propertyName="rotationY"
android:interpolator="@android:interpolator/accelerate_decelerate"
android:duration="300" />
<!-- Halfway through the rotation, set the alpha to 1. See startOffset. -->
<objectAnimator
android:valueFrom="0.0"
android:valueTo="1.0"
android:propertyName="alpha"
android:startOffset="150"
android:duration="1" />
</set>iOS/Mac Catalyst implementation
On iOS/Mac Catalyst, the switching transition animation is just a simple fade animation. The pushing and popping transtions are represented by the custom AppleTransitioning class:
public class AppleTransitioning : Foundation.NSObject, UIKit.IUIViewControllerAnimatedTransitioning
{
private readonly bool pushing;
public AppleTransitioning(bool pushing)
{
this.pushing = pushing;
}
public void AnimateTransition(UIKit.IUIViewControllerContextTransitioning transitionContext)
{
var fromView = transitionContext.GetViewFor(UIKit.UITransitionContext.FromViewKey);
var toView = transitionContext.GetViewFor(UIKit.UITransitionContext.ToViewKey);
var container = transitionContext.ContainerView;
var duration = TransitionDuration(transitionContext);
if (pushing)
container.AddSubview(toView);
else
container.InsertSubviewBelow(toView, fromView);
var currentFrame = pushing ? toView.Frame : fromView.Frame;
var offsetFrame = new CoreGraphics.CGRect(x: currentFrame.Location.X, y: currentFrame.Height, width: currentFrame.Width, height: currentFrame.Height);
if (pushing)
{
toView.Frame = offsetFrame;
toView.Alpha = 0;
}
var animations = () =>
{
UIKit.UIView.AddKeyframeWithRelativeStartTime(0.0, 1, () =>
{
if (pushing)
{
toView.Frame = currentFrame;
toView.Alpha = 1;
}
else
{
fromView.Frame = offsetFrame;
fromView.Alpha = 0;
}
});
};
UIKit.UIView.AnimateKeyframes(
duration,
0,
UIKit.UIViewKeyframeAnimationOptions.CalculationModeCubic,
animations,
(finished) =>
{
container.AddSubview(toView);
transitionContext.CompleteTransition(!transitionContext.TransitionWasCancelled);
toView.Alpha = 1;
fromView.Alpha = 1;
});
}
public double TransitionDuration(UIKit.IUIViewControllerContextTransitioning transitionContext)
{
return 0.25;
}
}Windows (WinUI)
On Windows, only the pushing and popping transtions are set to the DrillInNavigationTransitionInfo object.
PlatformSimpleShellTransition can be set to any page via the SimpleShell.Transition attached property. If you set a transition on your SimpleShell object, that transition will be used as the default transition for all pages. Transitions can be set on each page individually.
When navigating from one page to another, transition of the destination page is played.
Setting a transition can be simplified using the SetTransition() extension method:
public static void SetTransition(
this Page page,
ISimpleShellTransition transition)Setting Transitions.CustomPlatformTransition on AppShell in its constructor:
public AppShell()
{
InitializeComponent();
Routing.RegisterRoute(nameof(YellowDetailPage), typeof(YellowDetailPage));
this.SetTransition(Transitions.CustomPlatformTransition);
}This is how these custom platform-specific transitions look:
| Android | iOS |
|---|---|
android_simpleshell_platform_transitions.mp4 |
ios_simpleshell_platform_transitions.mp4 |
| macOS | Windows |
macos_simpleshell_platform_transitions.mov |
windows_simpleshell_platform_transitions.mp4 |
Although, platform-specific transition animations can be modified, it is quite limited (especially on Windows). If you want to take full control over the transitions, you need to disable the platform-specific ones by setting the usePlatformTransitions parameter of the UseSimpleShell() extension method to false and define your own platform-independent (universal) animations:
builder.UseSimpleShell(usePlatformTransitions: false);Universal transitions are fully cross-platform transitions that are defined using only .NET MAUI APIs. SimpleShell comes with no predefined universal transitions.
Each universal transition is represented by a SimpleShellTransition object which contains these read-only properties settable via its class constructors:
Callback— a method that is called when progress of the transition changes. Progress of the transition is passed to the method through a parameter of typeSimpleShellTransitionArgsStarting— a method that is called when the transition startsFinished— a method that is called when the transition finishesDuration— a method returning duration of the transitionDestinationPageInFront— a method returning whether the destination page should be displayed in front of the origin page when navigating from one page to anotherEasing— a method returning an easing of the transition animation
Each of these methods takes a SimpleShellTransitionArgs object as a parameter. Useful information about currently running transition can be obtained from this object.
The SimpleShellTransition class implements the ISimpleShellTransition interface.
SimpleShellTransition can be set on any page via SimpleShell.Transition attached property. If you set a transition on your SimpleShell object, that transition will be used as the default transition for all pages.
When navigating from one page to another, transition of the destination page is played.
Setting transition can be simplified using several extension methods. These are headers of the methods:
public static void SetTransition(
this Page page,
ISimpleShellTransition transition)
public static void SetTransition(
this Page page,
Action<SimpleShellTransitionArgs> callback,
Func<SimpleShellTransitionArgs, uint> duration = null,
Action<SimpleShellTransitionArgs> starting = null,
Action<SimpleShellTransitionArgs> finished = null,
Func<SimpleShellTransitionArgs, bool> destinationPageInFront = null,
Func<SimpleShellTransitionArgs, Easing> easing = null)
public static void SetTransition(
this Page page,
Action<SimpleShellTransitionArgs> switchingCallback = null,
Action<SimpleShellTransitionArgs> pushingCallback = null,
Action<SimpleShellTransitionArgs> poppingCallback = null,
Func<SimpleShellTransitionArgs, uint> duration = null,
Action<SimpleShellTransitionArgs> starting = null,
Action<SimpleShellTransitionArgs> finished = null,
Func<SimpleShellTransitionArgs, bool> destinationPageInFront = null,
Func<SimpleShellTransitionArgs, Easing> easing = null)The default transition for all pages can be set, for example, in the constructor of your AppShell:
public AppShell()
{
InitializeComponent();
Routing.RegisterRoute(nameof(YellowDetailPage), typeof(YellowDetailPage));
this.SetTransition(
callback: static args =>
{
switch (args.TransitionType)
{
case SimpleShellTransitionType.Switching:
if (args.OriginShellSectionContainer == args.DestinationShellSectionContainer)
{
// Navigatating within the same ShellSection
args.OriginPage.Opacity = 1 - args.Progress;
args.DestinationPage.Opacity = args.Progress;
}
else
{
// Navigatating between different ShellSections
(args.OriginShellSectionContainer ?? args.OriginPage).Opacity = 1 - args.Progress;
(args.DestinationShellSectionContainer ?? args.DestinationPage).Opacity = args.Progress;
}
break;
case SimpleShellTransitionType.Pushing:
// Hide the page until it is fully measured
args.DestinationPage.Opacity = args.DestinationPage.Width < 0 ? 0.01 : 1;
// Slide the page in from right
args.DestinationPage.TranslationX = (1 - args.Progress) * args.DestinationPage.Width;
break;
case SimpleShellTransitionType.Popping:
// Slide the page out to right
args.OriginPage.TranslationX = args.Progress * args.OriginPage.Width;
break;
}
},
finished: static args =>
{
args.OriginPage.Opacity = 1;
args.OriginPage.TranslationX = 0;
args.DestinationPage.Opacity = 1;
args.DestinationPage.TranslationX = 0;
if (args.OriginShellSectionContainer is not null)
args.OriginShellSectionContainer.Opacity = 1;
if (args.DestinationShellSectionContainer is not null)
args.DestinationShellSectionContainer.Opacity = 1;
},
destinationPageInFront: static args => args.TransitionType switch
{
SimpleShellTransitionType.Popping => false,
_ => true
},
duration: static args => args.TransitionType switch
{
SimpleShellTransitionType.Switching => 300u,
_ => 200u
},
easing: static args => args.TransitionType switch
{
SimpleShellTransitionType.Pushing => Easing.SinIn,
SimpleShellTransitionType.Popping => Easing.SinOut,
_ => Easing.Linear
});
}Universal transitions look the same on all platforms:
SimpleShell.Transitions.mp4
Transitions can be set on each page individually. Default transition will be overridden if it is already set:
public YellowDetailPage()
{
InitializeComponent();
this.SetTransition(
callback: args => args.DestinationPage.Scale = args.Progress,
starting: args => args.DestinationPage.Scale = 0,
finished: args => args.DestinationPage.Scale = 1,
duration: args => 500u);
}Two transitions can be combined into one when you want to use different transitions under different conditions:
public YellowDetailPage()
{
InitializeComponent();
this.SetTransition(new SimpleShellTransition(
callback: args => args.DestinationPage.Scale = args.Progress,
starting: args => args.DestinationPage.Scale = 0,
finished: args => args.DestinationPage.Scale = 1,
duration: args => 500u)
.CombinedWith(
transition: SimpleShell.Current.GetTransition(),
when: args => args.TransitionType != SimpleShellTransitionType.Pushing));
}when delegate determines when the second transition should be used.
Note
In this example, scale transition is used only when the page is being pushed to the navigation stack, otherwise the default transition defined in the shell is used.