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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Creating Custom Gestures in Flutter
25 mins
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".
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:
- You replaced
GestureDetector
with your own customKeymotionGestureDetector
, which now handles rotation gestures. - 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 updatingcurrentAngle
to match the rotation provided. - 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:
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.
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.
Aaaaaand voilá! Keymotion is now up and running, and it's all thanks to you.