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 3 of 4 of this article. Click here to view the first page.

Explaining the Chain of Responsibility Pattern

One more thing you should know about gestures is how Flutter delegates user input.

Time for a pop quiz: In the code below, after tapping the red Container from the widget once, what does the system print?

Widget build(BuildContext context) {
 return GestureDetector(
    onTap: () => print('Tapped parent!'),
    onDoubleTap: () => print('Double tapped parent!'),
    child: GestureDetector(
      onTap: () => print('Tapped child!'),
      child: Container(width: 200, height: 200, color: Colors.red),
    ),
  );
}

Have your answer? Yes? Well, then, the correct answer is Tapped child!. This is because of a design pattern called chain of responsibility. Chain of responsibility is a behavioral design pattern that lets you pass events along a chain of handlers.

Upon receiving an event, each handler decides whether to process the event or pass it to the next handler in the chain, hence the name "chain of responsibility".

Diagram explaining chain of responsibility

In the quiz, the tap gesture occurs in the red Container, then it looks up the widget tree to find the first widget that can handle the event. The parent of the red Container, which is the child GestureDetector, has an onTap definition that handles the event and then stops propagating.

Here's another pop quiz for you: What does the system print after double-tapping the red Container from the widget in the code above?

Yes! The first widget that can handle the double-tap gesture is the parent GestureDetector, so the app would print Double tapped parent!.

Understanding the chain of responsibility design pattern will help you throughout your adventure of building custom gestures because it's the most used pattern when responding to user input events. As a side bonus, it'll also help you avoid lots of headaches when combining common and custom gestures.

Understanding RawGestureDetector

Remember when you read about how awesome GestureDetector is and how you've already interacted with it? Well... the same thing happens with RawGestureDetector. Underneath, GestureDetector uses it to interpret common gestures in a more "raw" interaction.

RawGestureDetector can wrap a widget the same way GestureDetector does, but it requires you to define what gestures you want. It does this by accepting a gesture map of the recognizers you want to accept.

If you're wondering when to use GestureDetector or RawGestureDetector, the answer is simple. If you're looking to use built-in gestures, then using GestureDetector is enough. But if you're creating your own recognizers, then you're better off using RawGestureDetector.

Please note that working with RawGestureDetector can result in code duplication. This is why you should wrap it with another widget around your app. For example, you'll use KeymotionGestureDetector to wrap RawGestureDetector. This makes recognizing the rotate gesture easier to add and maintain in different parts of the app.

Open lib/presentation/widgets/keymotion_gesture_detector.dart, and look at RawGestureDetector:

Widget build(BuildContext context) {
  return RawGestureDetector(
    child: child,
    gestures: <Type, GestureRecognizerFactory>{
      // TODO: Add rotate gesture recognizer.
      // TODO: Add secret key bonus.
    },
  );
}

As you can see, the gestures property is a map of GestureRecognizer to GestureRecognizerFactory.

Copy the code below and replace the comment // TODO: Add rotate gesture recognizer.:

RotateGestureRecognizer:
 GestureRecognizerFactoryWithHandlers<RotateGestureRecognizer>(
  () => RotateGestureRecognizer()
    ..onRotationStart = onRotationStart
    ..onRotationUpdate = onRotationUpdate
    ..onRotationEnd = onRotationEnd,
  (instance) {},
),

First, you're registering the type RotateGestureRecognizer as a key in gestures. This helps RawGestureDetector redirect interactions to the recognizer. It also determines if the widget should process the interaction. Then, you're creating a new GestureRecognizerFactoryWithHandlers for your custom gesture recognizer. This is a factory for creating gesture recognizers that delegates to callbacks.

At this point, KeymotionGestureDetector can intercept rotation gestures, but you still need to add it to your game. Open lib/presentation/widgets/rotating_lock_core.dart and replace build with the following:

@override
Widget build(BuildContext context) {
  final radius = MediaQuery.of(context).size.height / 4;
  // 1.
  return KeymotionGestureDetector(
    onRotationUpdate: (details) {
      setState(() {
        // 2.
        final angleDegrees = (details.rotationAngle * 180 ~/ math.pi).abs();
        currentAngle = details.rotationAngle;
        // 3.
        if (details.acceleration <= 0.0025) {
          final isCorrect = context.read<Game>().tryCombination(angleDegrees);

          if (isCorrect) currentAngle = 0;
        }
      });
    },
    // TODO: Add secret key gesture.
    child: Stack(
      alignment: Alignment.center,
      children: [
        Transform.rotate(
          angle: ((currentAngle * 180.0) / math.pi) / 10,
          child: ConstrainedBox(
            constraints: BoxConstraints.tightForFinite(
              height: radius,
            ),
            child: Image.asset('assets/images/lock_core.png'),
          ),
        ),
        AnimatedOpacity(
          opacity: widget.combinationsLeft == 0 ? 0.4 : 1,
          duration: const Duration(milliseconds: 200),
          child: Column(
            children: [
              Text(
                widget.combinationsLeft.toString(),
                style: const TextStyle(
                  fontSize: 24,
                  color: KeyMotionColors.tint1,
                ),
              ),
              const SizedBox(height: 8),
              const Icon(
                FeatherIcons.shield,
                color: KeyMotionColors.tint1,
              ),
            ],
          ),
        ),
      ],
    ),
  );
}

Here's an overview of the changes you just made:

  1. You replaced GestureDetector with your own custom KeymotionGestureDetector, which now handles rotation gestures.
  2. You're now listening for the onRotationUpdate callback. Since the event details contain the rotation angle in radians, you need to convert it to degrees before trying the combination. You're also updating currentAngle to match the rotation provided.
  3. Finally, you're checking that the acceleration doesn't surpass an arbitrary tolerance value. This forces the user to rotate slowly to verify the combination.

Add the following import to the top of rotating_lock_core.dart:

import 'keymotion_gesture_detector.dart';

And there you have it! You're now using the awesome RotateGestureRecognizer you built! Build and run the project. At this point, you'll see the following:

Keymotion lock core rotating smoothly until the lock opens

You might notice the lock makes a quick jump when making two to four complete turns. This happens because the formula used for calculating the rotation needs tweaking. It doesn't think your fingers can return to the initial position. So, when this occurs, it thinks the gesture started over and ends in jumping rotations.

If you need to be exact when rotating, you can create your own formula to define rotations. In this case, it works fine since the combinations created by Keymotion don't need that much rotation.

Note: If you're using the simulator for iOS, you can use the Option key to simulate two fingers and Shift to move around said fingers. Then press and hold — while pressing Alt — and you'll be able to perform rotation gestures.

Combining Common and Custom Gestures

The last feature you'll add to Keymotion is a bonus system where a user can unlock a free key by double tapping the lock. Double-tapping the lock should award the player a bonus key each time the game starts. This gives an edge to players who are paying attention to the gestures in the game.

OK, enough about the concept, now you'll add this feature.

Taking advantage of the fact that you've already built a custom gesture detector widget, open lib/presentation/widgets/keymotion_gesture_detector.dart and replace // TODO: Add secret key bonus. with the following code:

DoubleTapGestureRecognizer:
 GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>(
  () => DoubleTapGestureRecognizer()..onDoubleTap = onDoubleTap,
  (instance) {},
),

This adds another entry to RawGestureDetector's map of gestures. Now, open lib/presentation/widgets/rotating_lock_core.dart again and replace // TODO: Add secret key gesture. with the following code:

onDoubleTap: () => context.read<Game>().foundSecretKey(),

Build and run. You can now unlock keys by rotating the lock with two fingers or get the bonus key by double-tapping the lock.

Secret Double Tap for bonus key demo

Aaaaaand voilá! Keymotion is now up and running, and it's all thanks to you.