DEV Community

Cover image for Joystick Navigation UI in .NET MAUI
Luis Beltran
Luis Beltran

Posted on

Joystick Navigation UI in .NET MAUI

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, and Views.

Project structure

Step 2. Views

  • In the Views folder, add your pages (views) with your content. For example, this sample project includes 4 ContentPages: UpView.xaml, DownView.xaml, RightView.xaml, and LeftView.xaml. Each page is displayed in the app when the user navigates to a specific direction using the joystick. For instance, the app navigates to UpView.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>
Enter fullscreen mode Exit fullscreen mode

The other views have similar code.

Step 3. Routes class

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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 a DirectionHelper.cs class, which defines a static helper class for determining joystick movement direction based on x and y 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;
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

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 a ContentView element named JoystickControl.

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>
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode
  • 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 the PanUpdated 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 its TranslationX and TranslationY property values.
    • When Completed, it uses the GetDirection method from DirectionHelper 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 (see RouteTypeMap from Routes class), and ResetThumb to animate the thumb back to the center.
  • The Navigate method uses Shell navigation if the direction is not Routes.None. You can use a different navigation experience if you want, such as NavigationPage, depending on your app. In this case, we have AppShell 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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
  • We iterate over the RouteTypeMap dictionary from the Routes 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>
Enter fullscreen mode Exit fullscreen mode
  • The JoystickNavigationApp.Controls namespace is imported as controls 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 both VerticalOptions and HorizontalOptions to End 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.

Demo

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)