This article is part of the #MAUIUIJuly initiative by Matt Goldman. You'll find other helpful articles and tutorials published daily by community members and experts there, so make sure to check it out every day.
Traditional app navigation is often static — tabs, drawers, and buttons. But what if we took inspiration from video games and created a joystick to control navigation? In this tutorial, you'll build a fun and interactive joystick-style navigation system in .NET MAUI.
This tutorial is aimed at beginners. First, let's do a bit of setup before actually creating the control.
Step 1. Project Structure
Create a .NET MAUI project with the name
JoystickNavigationApp
.Add three folders:
Controls
,Helpers
, andViews
.
Step 2. Views
- In the
Views
folder, add your pages (views) with your content. For example, this sample project includes 4ContentPages
:UpView.xaml
,DownView.xaml
,RightView.xaml
, andLeftView.xaml
. Each page is displayed in the app when the user navigates to a specific direction using the joystick. For instance, the app navigates toUpView.xaml
when the joystick is pressed in the "up" direction.
Here is the code for UpView.xaml
for reference, which includes a message and background:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="JoystickNavigationApp.Views.UpView"
Title="Up View" BackgroundColor="LightBlue">
<VerticalStackLayout>
<Label Text="You navigated Up!"
HorizontalOptions="Center"
VerticalOptions="Center"
FontSize="30"/>
</VerticalStackLayout>
</ContentPage>
The other views have similar code.
Step 3. Routes class
- In the
Helpers
folder, create aRoutes.cs
class, which defines a static helper class for managing navigation routes in the app. The code is inspired by Julian Ewers-Peters'sRoutes
class implementation in his blog post Add automatic route registration to your .NET MAUI app.
Here is the code:
//Credits: https://blog.ewers-peters.de/add-automatic-route-registration-to-your-net-maui-app
using System.Collections.ObjectModel;
using JoystickNavigationApp.Views;
namespace JoystickNavigationApp.Helpers
{
public static class Routes
{
public const string Up = "up";
public const string Down = "down";
public const string Left = "left";
public const string Right = "right";
public const string None = "none";
private static Dictionary<string, Type> routeTypeMap = new()
{
{ Up, typeof(UpView) },
{ Down, typeof(DownView) },
{ Left, typeof(LeftView) },
{ Right, typeof(RightView) }
};
public static ReadOnlyDictionary<string, Type> RouteTypeMap => routeTypeMap.AsReadOnly();
}
}
Each constant represents route names for navigation directions and to refer to specific navigation targets.
The RouteTypeMap
read-only dictionary maps each route string to its corresponding page/view type (e.g., "up" → UpView
).
Now we can start implementing our joystick control
Step 4. DirectionHelper class
- In the
Helpers
folder, create aDirectionHelper.cs
class, which defines a static helper class for determining joystick movement direction based onx
andy
input values.
Here is the code:
namespace JoystickNavigationApp.Helpers
{
public static class DirectionHelper
{
private static double sensitivity = 20;
public static string GetDirection(double x, double y)
{
if (Math.Abs(x) > Math.Abs(y))
return x > sensitivity ? Routes.Right : x < -sensitivity ? Routes.Left : Routes.None;
else
return y > sensitivity ? Routes.Down : y < -sensitivity ? Routes.Up : Routes.None;
}
}
}
The GetDirection
method takes two double parameters: x
(horizontal movement) and y
(vertical movement). It returns a string representing the direction.
- First, it checks if the horizontal movement is greater than the vertical movement.
- If true, the direction is horizontal (left or right).
- Otherwise, the direction is vertical (up or down).
- The threshold value 20 is hardcoded. You can try different joystick sensitivity values.
- If both x and y are within ±20, it is considered as no change in the direction (thus, no navigation).
Step 5. JoystickControl (XAML code)
Let's define our custom .NET MAUI control.
- In the
Controls
folder, create aContentView
element namedJoystickControl
.
A ContentView
is used for creating reusable UI components.
This control visually represents a joystick with a static background and a movable thumb.
Here is the code:
<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="JoystickNavigationApp.Controls.JoystickControl"
WidthRequest="100" HeightRequest="100">
<Grid>
<Ellipse Fill="LightGray" />
<Ellipse x:Name="Thumb"
Fill="DarkSlateBlue"
WidthRequest="40"
HeightRequest="40"
TranslationX="0"
TranslationY="0" />
</Grid>
</ContentView>
The control features two overlapping ellipses:
- The light gray circle represents the joystick’s background.
- The smaller, dark blue circle represents the joystick’s movable "thumb" with its initial position set to (0, 0).
- The thumb will be referenced in the code-behind by its name in order to move it in response to user input.
The actual movement logic will be handled in the code-behind in the next step.
Step 6. JoystickControl (code-behind)
Now, let's implement the logic for the custom joystick control. The code will handle user interaction, determine direction, and trigger the navigation. Here is the code:
using JoystickNavigationApp.Helpers;
namespace JoystickNavigationApp.Controls;
public partial class JoystickControl : ContentView
{
private double _radius = 40;
public JoystickControl()
{
InitializeComponent();
var panGesture = new PanGestureRecognizer();
panGesture.PanUpdated += OnPanUpdated;
this.GestureRecognizers.Add(panGesture);
}
private void OnPanUpdated(object sender, PanUpdatedEventArgs e)
{
switch (e.StatusType)
{
case GestureStatus.Running:
double x = Math.Clamp(e.TotalX, -_radius, _radius);
double y = Math.Clamp(e.TotalY, -_radius, _radius);
Thumb.TranslationX = x;
Thumb.TranslationY = y;
break;
case GestureStatus.Completed:
var direction = DirectionHelper.GetDirection(Thumb.TranslationX, Thumb.TranslationY);
Navigate(direction);
ResetThumb();
break;
}
}
private async void Navigate(string direction)
{
if (direction != Routes.None)
await Shell.Current.GoToAsync(direction);
}
private async void ResetThumb()
{
await Thumb.TranslateTo(0, 0, 100, Easing.CubicOut);
}
}
- The
_radius
variable sets the maximum distance the joystick "thumb" can move from the center. - In the class constructor, a
PanGestureRecognizer
will handle the drag (pan) gestures. Moreover, there is also a subscription to thePanUpdated
event. The gesture recognizer is attached to the control. - The
OnPanUpdated
method handles the pan gesture updates with two cases.- When
Running
, it clamps the pan movement (e.TotalX
,e.TotalY
) to within ±40 (the radius). It then moves the thumb ellipse by setting itsTranslationX
andTranslationY
property values. - When
Completed
, it uses theGetDirection
method fromDirectionHelper
class to determine the direction based on the thumb's final position. Then, it calls two methods:Navigate(direction)
to perform navigation to the route indicated in the direction (seeRouteTypeMap
fromRoutes
class), andResetThumb
to animate the thumb back to the center.
- When
- The
Navigate
method usesShell
navigation if the direction is notRoutes.None
. You can use a different navigation experience if you want, such asNavigationPage
, depending on your app. In this case, we haveAppShell
defined from the initial template, so we will use it. - Finally, the
ResetThumb
method animates the thumb back to the center (0,0) over 100ms using a cubic easing function.
Step 7. Navigation routes registration
The last step for this control implementation is to register the navigation routes for the app. We will do it in AppShell.xaml.cs
, here is the code, which works thanks to Julian Ewers-Peters's AppShell
implementation in his blog post Add automatic route registration to your .NET MAUI app.
//Credits: https://blog.ewers-peters.de/add-automatic-route-registration-to-your-net-maui-app
using JoystickNavigationApp.Helpers;
namespace JoystickNavigationApp
{
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
foreach (var route in Routes.RouteTypeMap)
Routing.RegisterRoute(route.Key, route.Value);
}
}
}
- We iterate over the
RouteTypeMap
dictionary from theRoutes
helper. - For each route, we register a route string (like "up", "down", etc.) and its associated page type (like
UpView
,DownView
, etc.) with the .NET MAUI Shell routing system.
This way, we ensure all your navigation routes are registered at app startup, so we can eventually navigate using route strings, such as Shell.Current.GoToAsync("up")
, as seen on the Navigate
method from JoystickControl
code-behind class.
We did it! Now, we can use the control in our application
Step 8. Use the control in any page
For example, let's use the joystick control in MainPage
. Replace the existing code in MainPage.xaml
with this:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="JoystickNavigationApp.MainPage"
xmlns:controls="clr-namespace:JoystickNavigationApp.Controls">
<Grid>
<Label Text="Joystick Navigation UI"
HorizontalOptions="Center"
VerticalOptions="Start"
FontSize="24"
Margin="20" />
<controls:JoystickControl VerticalOptions="End"
HorizontalOptions="End"
Margin="20" />
</Grid>
</ContentPage>
- The
JoystickNavigationApp.Controls
namespace is imported ascontrols
to use custom controls defined there. - We can now use the custom joystick control as
controls:JoystickControl
in our XAML code to place it on the page. It is positioned at the bottom-right by setting bothVerticalOptions
andHorizontalOptions
toEnd
value, with a margin to add spacing from the edge.
The goal is to create a simple and intuitive UI for joystick-based navigation in our app.
By the way. Do not forget to delete the code-behind logic from MainPage.xaml.cs
that references the previous controls, such as the counter button
Step 9. Test your app
Now, let's build and run your app. It works on Android, Windows, and iOS. Here is a demo on Android.
What's next?
I guess we can take this control to the next level in two ways:
- By publishing it as a NuGet package
- By creating a floating joystick (so it's always available, on any screen in your app).
What do you think about this? Should I do it in the near future?
You can also help me, as the source code of this project can be found here. Happy to receive your PRs!
I hope that this post was interesting and useful for you. Thanks for your time, and enjoy the rest of the #MAUIUIJuly publications!
Top comments (0)