Creating Custom Gestures in Flutter

Learn how to add custom gestures in your Flutter app by working on a fun lock-picking app. By Alejandro Ulate Fallas.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Implementing a Common Gesture

Build and run the project, then start a new game. Try to guess the first lock combination by tapping the lock’s core a bunch of times. As you can see, the player has to tap the lock to rotate it. Instead, the user should be using two fingers to rotate the lock. You’ll replace this gesture later on in the tutorial.

Another issue is that you can’t rotate backward if you want to retry any previous combinations. This makes all past combinations impossible to get. You’ll fix that now by using double-tap gestures to signify that the lock should rotate backwards.

A double-tap gesture happens when the user taps the screen in the same location twice in quick succession. Flutter establishes that these two-tap events have to be within a time of 300 milliseconds to recognize them as a double-tap gesture.

Open lib/presentation/widgets/rotating_lock_core.dart again and copy this code in line 70, above the child property:

// 1
onDoubleTap: () {
  setState(() {
    // 2
    currentAngle -= 2 * math.pi / 180;
    final angleDegrees = (currentAngle * 180 ~/ math.pi).abs();

    final isCorrect = context.read<Game>().tryCombination(angleDegrees);

    if (isCorrect) currentAngle = 0;
  });
},

After this, your build method should look like this:

Widget build(BuildContext context) {
  // TODO: Convert to KeyMotionGesture
  return GestureDetector(
    onTap: () { ... },
    // 1
    onDoubleTap: () {
      setState(() {
        // 2
        currentAngle -= 2 * math.pi / 180;
        final angleDegrees = (currentAngle * 180 ~/ math.pi).abs();

        final isCorrect = context.read<Game>().tryCombination(angleDegrees);

        if (isCorrect) currentAngle = 0;
      });
    },
    child: Stack( ... ),
  );
}

Here’s a breakdown of what the new code is doing:

  1. First, you’re adding a new gesture to the parent GestureDetector. This time, you’re using onDoubleTap to detect double taps.
  2. Next, you’re using the same math that you used in the onTap callback but subtracting from the angle instead of adding to it. This will emulate the lock rotating backwards instead of forwards after a double tap.

Hot restart the app, and when you start a new game, double-tap the lock to see the rotation go counterclockwise. Remember the 300-millisecond timeout when trying it!

Keymotion app lock core pointer turning clockwise then counterclockwise

Wow, adding that gesture was quick and easy!

Creating a Custom Gesture

As you’ve already seen, Flutter provides interpreters for many gestures: GestureRecognizers. GestureDetector can identify gestures because it has recognizers defined for them.

Gesture recognizers are responsible for defining how a gesture behaves. Most of the time, recognizers involve math calculations to define pointers and locations. They also provide callbacks on different situations like starting or ending a gesture. For example, ScaleGestureRecognizer can identify when the user attempts scale gestures. It has callbacks when the gesture starts, updates or ends. But when does this gesture actually start or end? And also, what triggers an update callback?

In ScaleGestureRecognizer‘s case, the gesture starts when two fingers touch the screen. Then, each time one or both fingers move around, that corresponds to an update of the gesture, which includes angle and rotation changes. Finally, when the user lifts one or both fingers from the screen, the gesture ends.

You can also create your own gesture recognizer by extending GestureRecognizer. You’d then need to override the appropriate pointer-events to match the behavior you expect. Chances are you won’t need to define your own GestureRecognizer from scratch since Flutter comes packed with all of the recognizers you’d normally need. If you wanted to detect something like the user drawing a circle or star pattern, you’d need to create your own GestureRecognizer.

In Keymotion’s case, you’ll use ScaleGestureRecognizer as a base for the rotation gesture you’re going to create. The new recognizer will be called RotateGestureRecognizer. Extending from ScaleGestureRecognizer helps you delegate most of the heavy lifting to an already tested recognizer.

Once completed, RotateGestureRecognizer should be able to recognize two fingers pressing the screen and rotating clockwise or counterclockwise.

The gesture looks like this:

Rotation Gesture Demonstration

Time to start coding! Open lib/presentation/gestures/rotate_gesture_recognizer.dart, and replace // TODO: Finish RotateGestureRecognizer implementation with the code below:

// 1
double previousRotationAngle = 0;
//2
GestureRotationStartCallback? onRotationStart;
// 3
GestureRotationUpdateCallback? onRotationUpdate;
// 4
GestureRotationEndCallback? onRotationEnd;

// TODO: Bypass scale start gesture events

// TODO: Bypass scale update gesture events

// TODO: Bypass scale end gesture events

The code above defines how the recognizer should behave and the callbacks it'll have while processing the gesture. Here's a step-by-step explanation of what this code means:

  1. previousRotationAngle represents the last rotation angle the gesture recognized. This property helps calculate the acceleration between each change update callback.
  2. onRotationStart is the first callback for the gesture recognizer. It behaves in the same way as ScaleGestureRecognizer's onStart callback.
  3. onRotationUpdate can happen many times between onRotationStart and onRotationEnd. It provides a callback for rotation angle and acceleration updates.
  4. onRotationEnd triggers when the user lifts one or both fingers from the screen.

For RotateGestureRecognizer to provide custom callbacks, you'll need to override certain behaviors from the parent ScaleGestureRecognizer. This also allows you to inform gesture detectors on relevant information about the gesture itself.

Now, replace // TODO: Bypass scale start gesture events with the code below:

@override
GestureScaleStartCallback? get onStart => _scaleStarts;

void _scaleStarts(ScaleStartDetails details) {
  onRotationStart?.call(RotationStartDetails());
}

By overriding onStart, you're bypassing ScaleGestureRecognizer's behavior. This way, you're only passing relevant details about the custom gesture's start. RotationStartDetails defines what the details contain. But, in this case, you'll use the default empty constructor.

Next, you need to do the same for the update callback. Copy the code below, and replace // TODO: Bypass scale update gesture events with it.

@override
GestureScaleUpdateCallback? get onUpdate => _scaleUpdates;

void _scaleUpdates(ScaleUpdateDetails details) {
  onRotationUpdate?.call(
 RotationUpdateDetails(
      rotationAngle: details.rotation,
      acceleration: (details.rotation - previousRotationAngle).abs(),
    ),
  );
  previousRotationAngle = details.rotation;
}

Here's where the magic happens! You've overridden ScaleGestureRecognizer's onUpdate callback and provided your own implementation. In this case, you're taking advantage of ScaleUpdateDetails to define your own rotation and acceleration. ScaleUpdateDetails calculates the current rotation in radians using trigonometry functions, which you'll need now.

Next, you create a new RotationUpdateDetails instance to provide insights about rotation angle and acceleration through your custom callback onRotationUpdate.

Now, you'll bypass onEnd as the final step. Replace // TODO: Bypass scale end gesture events with the code below:

@override
GestureScaleEndCallback? get onEnd => _scaleEnds;

void _scaleEnds(ScaleEndDetails details) {
  onRotationEnd?.call(RotationEndDetails());
}

With this, you're overriding onEnd to notify handlers that the gesture has ended and it's ready to work with the next gesture.

Build and run the project. Right now, no changes are visible.