How to easily handle cross platform hover gestures in Flutter

Published

Here comes one of the most useful Flutter tricks there is: handling a hover state in Flutter. It’s no secret that Flutter was first and foremost a Mobile framework and that the more recent Web and Desktop targets have been playing catch up ever since their official announcements.

One of these catch up points is something that we’ve all grown used to when navigating the web or doing anything on our computers really.

A hover state is not just a fancy gadget. It’s become a necessity for UX and answers questions like :

  • Can I actually click this thing?
  • Is this a link?

I’ll also be showing some code relative to the pointer style so you can convey .

So withtout further ado, let’s look at how to handle hover states dependency-free in flutter.

The MouseRegion Widget

As usual with Flutter, there’s actually an existing lesser known Widget that covers our usecase: MouseRegion.

MouseRegion exposes a few events related to, you guessed it, your mouse. These events are:

  • OnEnter, which will trigger when your mouse enters the Widget.
  • OnExit, which will trigger when your mouse leaves the Widget.
  • OnHover, which will trigger both when your mouse enters the Widget and on every subsequent move inside the Widget.

All of these actually are PointerEvents which return all sorts of informations regarding the user’s pointer. Here, we’re going to be looking at the current position of the user’s mouse inside the containing Widget which is stored under the guise of an Offset.

Here’s an example in action which requires a Stateful Widget:


    // Removed for brevity

    class _MyHoverableWidgetState extends State<MyHoverableWidget>{
        // Our state
        bool amIHovering = false;
        Offset exitFrom = Offset(0,0);
    
        return MouseRegion(
            onEnter: (PointerDetails details) => setState(() => amIHovering = true),
            onExit: (PointerDetails details) => setState(() { 
                amIHovering = false;
                exitFrom = details.localPosition; // You can use details.position if you are interested in the global position of your pointer.
            }),
            child: Container(
                width: 200,
                height: 200,
                child: Center(
                    child:Text(amIHovering ? "Look mom, I'm hovering" : "I exited from: $exitFrom"),
                ), 
            ),
        );

    }

Let’s go through this code for a bit. You might be tempted to think of the MouseRegion as a sort of GestureDetector. This is actually not the case at all.

As the name implies, MouseRegion focuses on the Mouse, the “pointer” if you wish, itself and what it does at a given time.

GestureDetector is on the other hand meant as a way to handle user gestures that usually take longer to run their course, such as a onLongPress or a onSecondaryTap.

You could theoretically tweak the MouseRegion and its exposed PointerEvents in order to get most of what GestureDetector brings, but you would have to handle tap timings on multiple starts or get the position deltas yourself to make your own gestures. This sort of stuff.

My mouse cursor can’t be this cute

As promised, our next talking point is about playing around with your MouseCursor.

We’ve said earlier that Flutter is coming to Computer and Web and is playing catch up to provide first class support to them. Part of the True Computer Experience is having your computer start hanging and leave you with a circling indicator until you figure out a way to kill the process.

Guess what, you can now do this in Flutter! You don’t even need to have your computer break down to experience this marvel of technology, here’s how:

Flutter has brought us the MouseCursor class which gives us access to another newcomer which is of interest to us: SystemMouseCursors.

All usable cursors are listed, ranging from the regular “normal” cursor to the forbidden one. You can also show a Grab cursor or if you are a prankster, you can actually hide the user’s mouse. At the time of writing this article, the docs are set to the Flutter v1.20.4 stable branch.

The pointer we’re looking for is SystemMouseCursors.progress which is already usable on dartpad. And I’ve written a small piece of code to showcase how this all works.

Here’s the dartpad link.

You’ll notice the addition of the FocusableActionDetector which is our entrypoint to the MouseCursor. Meaning that if you decide to change the current cursor, it will only be changed inside this Widget.

If applicable, don’t forget to reset the MouseCursor back to the its default. You might confuse your user otherwise.

As you can see, this is actually a pretty simple subject thanks to the continued impressive work of the Flutter team. Since Desktop support is a pretty important subject, you might find interesting to check out Flutter’s medium page which regularly showcases the work that is being done on the subject.