♪ [intro music] ♪
Have you ever wondered how Flutter translates
the movements of your users' grimy fingers
into actual human-recognizable concepts
like taps, drags, or pinches?
If so, you're in good company
because it's a complicated topic.
Like most things in Flutter, it all just works,
but there are some occasional gotchas to watch out for.
Many Flutter developers have already met the GestureDetector widget,
which is responsible for interpreting user taps, clicks, and drags, and so on,
so this adventure is primarily about how that widget works,
who decides what happens if there are multiple gesture detectors,
and what happens if they each detect multiple gestures.
Behind the scenes,
when you register a listener with a GestureDetector,
it spins up a specialized gesture recognizer
and attaches your listener.
So when we write this code,
Flutter turns it into this.
That
knows how to tell if raw interaction data is a tap,
and it has a pointer to the code that you want it to run
when it recognizes one.
And what does this raw interaction data look like?
Well, take that GestureDetector we had before,
fill an entire page with it,
and then tap in a few places,
and you'll start to see raw coordinates print to the screen.
Though, this is actually a little misleading
because our functions don't actually get called
until the end of a long, arduous journey.
So this is not the raw interaction data
that I mentioned earlier.
For a more realistic look at that raw interaction data,
put
Start with a Listener widget,
which is vaguely similar to a GestureDetector,
but for receiving completely unprocessed interaction data
from the operating system,
and add some callbacks.
To follow click events,
subscribe to
That PointerEvent class contains everything you could care about,
including the location, time, device,
type of interaction,
and even how hard the user is pressing, if applicable.
To track things like swipes, pans, and zooms,
provide an
To see this in action, run your app again, touch the screen,
move your finger around,
and get ready for that sweet, sweet raw interaction data.
This is what Flutter has to deal with when it's trying to figure out
what your users are up to with their grimy fingers.
Why is no one washing their hands?
Looking at that data stream, can
Were there a lot of taps?
Were any of them double taps?
How about long presses?
Unless you're that guy from
who stares at falling green cliffs all day,
of course you can't tell.
And this highlights the job of the GestureDetector.
It wraps that Listener widget and interprets its primitive data stream,
exposing convenient handlers for human-recognizable events
like tapping, swiping, and pinching.
But there's an additional layer of complexity here
we have yet to consider.
Imagine this GestureDetector is receiving that same stream of data
from its inner Listener widget,
except consider its perspective immediately after
the first data event arrives.
As that event appears, how is the GestureDetector to know
whether this is a tap,
or the start of a double tap,
or a long press?
Without a crystal ball to look into the future,
it has no idea.
Making sense of all this is called "gesture disambiguation,"
and it's super fun.
We're finally ready to learn about the Gesture Arena,
where competing ideas about what the user is up to,
battle it out in a winner-takes-all fight to the death.
[dramatically] This summer...
one user, one gesture,
multiple recognizers.
Who... will... survive?
Gesture Arena,
coming to a mobile app near you.
Remember how earlier I said specific gesture callbacks
get attached to recognizers like this?
Well, our GestureDetector
has three families of gestures to consider:
tap, double tap, and long press.
So it would create three distinct recognizers.
Then when a fresh event comes in,
all three are admitted into the arena
for hardcore gladiatorial action,
played with the following rules:
1) At any time, a recognizer can declare defeat
and leave the arena.
If there's ever only one recognizer left in the arena,
that recognizer is the winner.
And 2) at any time, a recognizer can declare victory,
which causes it to win
and all the other remaining recognizers to lose.
[swords tinkling]
Well, actually, that sounds rather peaceful!
♪ [peaceful music] ♪
Each recognizer has an algorithm that it uses to decide
whether it should declare victory
or bow out.
The TapGestureRecognizer's algorithm is relatively simple.
It never declares victory,
and it only admits defeat if the pointer strays too far
from the down location
before a PointerUpEvent is received,
as that would suggest that the user is panning and not tapping.
The DoubleTapGestureRecognizer's rules
aren't too bad either.
After a single tap appears, it sets a 300-millisecond timer,
after which, if it hasn't seen another tap,
it admits defeat.
It's like the world's shortest snooze alarm.
Of course, if it does see a tap during that window,
it declares victory.
♪ [victorious music] ♪
And the LongPressGestureRecognizer uses a similar timer,
but with the opposite effect.
It sets a 500-millisecond timer,
after which, if the user is still holding their press,
it declares victory.
But if a PointerUpEvent appears before that timer elapses,
the LongPressGestureRecognizer admits defeat.
Of course, there's recognizers for all the other gestures as well:
pan, scale, force press, horizontal drag, and vertical drag.
And they all have their own algorithms, but you get the idea.
To wrap this up, let's consider a few scenarios
and think about how it will all play out in the arena.
First, let's imagine that loaded-up GestureDetector
with all of its recognizers in the arena after one PointerDownEvent,
which we'll say happened at time 0.
Then a mere 50 milliseconds later, a PointerUpEvent appears,
causing the long press recognizer
to bow out long before it ever reaches its 500-millisecond victory threshold.
tapRecognizer
are both still in the running.
Then it's all quiet on the gesture front
until the 300-millisecond mark,
when the doubleTapRecognizer's timer elapses,
causing it to admit defeat.
At last, the Gesture Arena has reached one of its victory conditions.
If there's ever only one recognizer left in the arena,
that recognizer is the winner.
Our
and Flutter calls its onTap method.
Note that because our GestureDetector has registered listeners
that don't make up their mind for up to half a second,
our single-tap listener had to wait a while
before it got called.
You can test this yourself in the DartPad linked below.
Toggle whether the Gesture Detector's onTap method is the only parameter
and notice the tiny change in latency.
And for one more scenario,
imagine that same set of GestureArena combatants,
but this time, there's a second PointerDownEvent
at 150 milliseconds
and a PointerUpEvent at 200 milliseconds.
In this scenario, the
and escapes the arena victorious.
Its onDoubleTap handler is called immediately,
and everyone else misses out.
And by the way, user interaction events bubble up your widget tree,
with descendant GestureDetector widgets taking precedence.
The first GestureDetector to rule the arena wins,
meaning if your GestureDetector has a parent GestureDetector
that implements the same listeners,
the child's recognizers will always win.
There are plenty of other interesting layers to this system
like the behavior parameter
and its opaque versus transparent options.
But for a deeper dive on those and other gesture topics,
check out Guillaume Diallo-Mulliez's talk
at FlutterVikings,
also linked below.
And for everything else on Flutter,
head to
♪ [outro music] ♪