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
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:
- First, you’re adding a new gesture to the parent
GestureDetector
. This time, you’re usingonDoubleTap
to detect double taps. - 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!
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:
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:
-
previousRotationAngle
represents the last rotation angle the gesture recognized. This property helps calculate the acceleration between each change update callback. -
onRotationStart
is the first callback for the gesture recognizer. It behaves in the same way asScaleGestureRecognizer
'sonStart
callback. -
onRotationUpdate
can happen many times betweenonRotationStart
andonRotationEnd
. It provides a callback for rotation angle and acceleration updates. -
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.