Ivy Walobwa Ivy is a Flutter developer and technical writer who is interested in creating awesome developer experiences. She is currently a Program Assistant for the Google Africa Developer Scholarship (GADS), where she helps facilitate the learning of students across Africa.

Handling gestures in Flutter

6 min read 1754

Handling Gestures in Flutter

When it comes to creating applications, you have to handle user gestures such as touch and drags. This makes your application interactive.

To effectively handle gestures, you need to listen to the gestures and respond to them. Flutter offers a variety of widgets that help add interactivity to your apps.

In this article, we go through handling gestures with the GestureDetector widget.

Introduction

Some widgets, like Container and Card widgets, don’t have an inbuilt way of detecting gestures. Such widgets are wrapped in the GestureDetector widget which is purely used for detecting gestures and does not give any visual response like a ripple effect.

The GestureDetector widget works by recognizing gestures that have callbacks defined and responding accordingly to the event. If a gesture is to be disabled, a null value is passed to the callback.

The following are common gestures captured by the GestureDetector widget, their corresponding events, and possible applications (all illustrations are credit to Luke Wroblewski’s Touch Gesture Reference Guide):

Tap

The user briefly touched the screen with a fingertip.

Tap

  • onTapDown — triggered when user makes contact with screen, might be a tap
  • onTapUp — triggered when user stops making contact with the screen
  • onTap — triggered when user briefly touches the screen
  • onTapCancel — triggered when the event that fired onTapDown is not a tap

Possible applications for the tap gesture include:

  1. Select
  2. Cancel
  3. Submit

Double-tap

The user tapped the screen at the same location twice in quick succession.

Double-tap

  • onDoubleTapDown — triggered when user makes contact with screen, might be a double tap
  • onDoubleTap — triggered when user taps the screen at the same location twice in quick succession
  • onDoubleTapCancel — triggered when the event that fired onDoubleTapDown is not a double tap

Possible applications for the double-tap gesture include:

  1. Like/dislike
  2. Screen on/off
  3. Resize an image

Long press

The user made contact with the screen at the same location for a long period of time.
Long press

  • onLongPressDown — triggered when user makes contact with screen, might be a long press
  • onLongPressStart — triggered when the start of a long press has been detected
  • onLongPress — triggered when a long press has been detected
  • onLongPressMoveUpdate — triggered when long press has been detected and user has drag-moved finger
  • onLongPressEnd — triggered when the end of a long press has been detected
  • onLongPressUp — triggered when the end of a long press has been detected; contact has been removed after long press
  • onLongPressCancel — triggered when the event that fired onLongPressDown is not a long press

Possible applications for the long-press gesture include:

  1. Show more options
  2. Move an icon

Scale

The user pinched or spread the screen.

Spread gesture FlutterPinch gesture

  • onScaleStart — triggered when contact with the screen has established a focal point and initial scale of 1.0
  • onScaleUpdate — triggered when contact with the screen has indicated a new focal point and/or scale
  • onScaleEnd — triggered when user is no longer making contact with screenPossible application for the scale gesture

Uses for scale gestures include:

  1. Zoom in/zoom out
  2. Rotation

Vertical Drag

The user made contact with the screen and moved their fingertip in a steady manner vertically.

Vertical drag

  • onVerticalDragDown — triggered when user makes contact with screen, might move vertically
  • onVerticalDragStart — triggered when user has made contact with screen and began to move vertically
  • onVerticalDragUpdate — triggered when contact that is moving vertically has moved in a vertical direction once again
  • onVerticalDragEnd — triggered when the end of a vertical drag has been detected
  • onVerticalDragCancel — triggered when the event that fired onVerticalDragDown is not a vertical drag

Possible applications for the vertical drag gesture include:

  1. Scroll

Horizontal drag

The user made contact with the screen and moved their fingertip in a steady manner horizontally.

Horizontal drag

  • onHorizontalDragDown — triggered when user makes contact with screen, might move horizontally
  • onHorizontalDragStart — triggered when user has made contact with screen and began to move horizontally
  • onHorizontalDragUpdate — triggered when contact that is moving horizontally has moved in a horizontal direction once again
  • onHorizontalDragEnd — triggered when the end of a horizontal drag has been detected
  • onHorizontalDragCancel — triggered when the event that fired onHorizontalDragDown is not a horizontal drag

Possible applications for the horizontal drag gesture include:

  1. Delete
  2. Archive
  3. Navigate to a different view

This is not a complete list of the gestures detected. Check the official documentation for a complete list.



Let’s try it out!

Getting started

To use the GestureDetector widget:

  1. Wrap desired widget with the GestureDetector widget.
  2. Pass callback for the gesture you wish to detect.
  3. Update the app accordingly

We will build a simple demo app that handles the tap, double-tap, long press, and scale gestures.

Create a new Flutter app

Create a new Flutter application and clear the default code in your main.dart file.

Update UI

We will create the four files below. You can view the folder structure here.

main.dart

import 'package:flutter/material.dart';
import 'presentation/my_app_widget.dart';
void main() {
  runApp(const MyApp());
}

my_app_widget.dart

import 'package:flutter/material.dart';
import 'home_page.dart';
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Gesture Detector Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home:  const HomePage(),
    );
  }
}

home_page.dart

import 'package:flutter/material.dart';
import 'widgets/widgets.dart';
class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    final height = MediaQuery.of(context).size.height;
    final width = MediaQuery.of(context).size.width;
    return Scaffold(
      body: Padding(
        padding: EdgeInsets.symmetric(
            horizontal: width * 0.1, vertical: height * 0.2),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children:  const [
            MyCardWidget(),
            MyFavoriteIconWidget()
          ],
        ),
      ),
    );
  }
}

my_card_widget.dart

import 'dart:math';
import 'package:flutter/material.dart';
class MyCardWidget extends StatefulWidget {
  const MyCardWidget({
    Key? key,
  }) : super(key: key);
  @override
  State<MyCardWidget> createState() => _MyCardWidgetState();
}
class _MyCardWidgetState extends State<MyCardWidget> {
  @override
  Widget build(BuildContext context) {
    return const Card(
      child: SizedBox(
        height: 300,
        width: 300,
      ),
      color: Colors.yellow,
    );
  }
}

my_favorite_icon_widget.dart

import 'package:flutter/material.dart';
class MyFavoriteIconWidget extends StatefulWidget {
  const MyFavoriteIconWidget({
    Key? key,
  }) : super(key: key);

  @override
  State<MyFavoriteIconWidget> createState() => _MyFavoriteIconWidgetState();
}

class _MyFavoriteIconWidgetState extends State<MyFavoriteIconWidget> {
  @override
  Widget build(BuildContext context) {
    return const Icon(
     Icons.favorite_border,
      size: 40,
    );
  }
}

Your final app should look like this:

Demo app Flutter gesture

Now that we have our UI ready, let’s handle some gestures.

Handling the tap gesture

In your my_favorite_icon_widget.dart file:

  1. Add a selected flag property to the StatefulWidget
    bool isSelected = false;
  2. Wrap the Icon widget with the GestureDetector widget
  3. Provide a non-null callback to the onTap property
  4. Change the icon and icon color based on the value of the flag property value
class _MyFavoriteIconWidgetState extends State<MyFavoriteIconWidget> {
  bool isSelected = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
        onTap: (){
          setState(() {
            isSelected = !isSelected;
          });
        },
        child:  Icon(
          isSelected ? Icons.favorite: Icons.favorite_border,
          size: 40,
          color: isSelected? Colors.red: Colors.black ,
        ));
  }
}

Handling the double-tap gesture

In your my_card_widget.dart file:

  1. add a color property
  2. wrap the Card widget with the GestureDetector widget
  3. provide a non-null callback to the onDoubleTap property
  4. change the color of the card based on the value of the color property
class _MyCardWidgetState extends State<MyCardWidget> {
  Color bgColor = Colors.yellow;
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onDoubleTap: (){
        setState(() {
          bgColor = Colors.primaries[Random().nextInt(Colors.primaries.length)];
        });
      },
      child:   Card(
        child: const SizedBox(
          height: 300,
          width: 300,
        ),
        color: bgColor,
      ),
    );
  }
}

Handling the long press gesture

In your my_card_widget.dart file:
1. Add a makeCircular flag property
2. Provide a non-null callback to the onLongPress property
3. Change the shape of the card based on the value of the makeCircular property

class _MyCardWidgetState extends State<MyCardWidget> {
  Color bgColor = Colors.yellow;
  bool makeCircular = false;
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onLongPress: (){
        setState(() {
          makeCircular = !makeCircular;
        });
      },
      child:   Card(
        shape: makeCircular? const CircleBorder(): const RoundedRectangleBorder(),
        child: const SizedBox(
          height: 300,
          width: 300,
        ),
        color: bgColor,
      ),
    );
  }
}

Handling the scale gesture

In your my_card_widget.dart file:
1. Add a _scaleFactor property
2. Add a _baseFactor property
3. Provide a non-null callback to the onScaleStart property — establish an initial scale
4. Provide a non-null callback to the onScaleUpdate property — establish a new scale
5. Provide a non-null callback to the onScaleEnd property — return to initial scale
6. Wrap the Card widget with Transorm.scale widget
7. Change the scale property based on the value of the _scaleFactor

class _MyCardWidgetState extends State<MyCardWidget> {
  Color bgColor = Colors.yellow;
  bool makeCircular = false;
  double _scaleFactor = 0.5;
  double _baseScaleFactor = 0.5;
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onScaleStart: (details){
        _baseScaleFactor = _scaleFactor;
      },
      onScaleUpdate: (details){
        setState(() {
          _scaleFactor = _baseScaleFactor * details.scale;
        });
      },
      onScaleEnd: (details){
        // return to initial scale
        _scaleFactor = _baseScaleFactor;
      },
      child:   Transform.scale(
        scale: _scaleFactor,
        child: Card(
          shape: makeCircular? const CircleBorder(): const RoundedRectangleBorde(),
          child: const SizedBox(
            height: 300,
            width: 300,
          ),
        color: bgColor,
      ),
    );
  }
}

The video below shows the implemented gestures:

Gesture Disambiguation

So what happens when we provide the onGestureDown event callback for tap and double-tap, and two delayed, brief touch events occur?

Consider the illustration:

Gesture Disambiguation Flutter

When two or more gesture events with non-null callbacks are recognized, Flutter disambiguates which gesture the user intends by having each recognizer join the gesture arena. In the gesture arena, the events “battle” and the winning event takes effect while the losing events are canceled. 

The gesture arena takes into account the following factors:

  1. The length of time the user touches the screen
  2. The number of pixels moved in each direction
  3. Which gesture is in the arena
  4. Which gesture declares victory

These are the battle states:

  • Maybe — might be the gesture
  • Hold — might be the gesture if it evolves in a particular way; for our case, one tap occurred and might be a double tap if the second tap occurs within expected time
  • Yes — declaration of victory
  • Cancel — withdrawn from battle

For example, say the following occur:

1. onTapDown and onDoubleTapDown are triggered
2. The two gestures compete
3. The tap gesture wins and the callback is executed (the onTap callback)
4. The double-tap gesture loses and gets canceled (onDoubleTapCancel triggered)

For our case, the tap gesture won because:

  • The duration between the two taps was delayed
  • The tap gesture declared victory with a “yes”
  • The tap gesture is the remaining gesture after double-tap got canceled, with no other competitor

Conclusion

We have gone through the GestureDetector widget and learned how it works. We have learned how to use it to add interactivity to our application, and we have implemented some of the common gestures, like tap, long press, double-tap, and scale. We finally looked at gesture disambiguation.


More great articles from LogRocket:


With this knowledge, we now have a better understanding of the GestureDetector widget and can comfortably use any of its properties to recognize gestures. Feel free to play around with the different gestures. You can find the demo app on GitHub.

Get set up with LogRocket's modern error tracking in minutes:

  1. Visit https://logrocket.com/signup/ to get an app ID
  2. Install LogRocket via npm or script tag. LogRocket.init() must be called client-side, not server-side
  3. $ npm i --save logrocket 

    // Code:

    import LogRocket from 'logrocket';
    LogRocket.init('app/id');
    Add to your HTML:

    <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
    <script>window.LogRocket && window.LogRocket.init('app/id');</script>
  4. (Optional) Install plugins for deeper integrations with your stack:
    • Redux middleware
    • NgRx middleware
    • Vuex plugin
Get started now
Ivy Walobwa Ivy is a Flutter developer and technical writer who is interested in creating awesome developer experiences. She is currently a Program Assistant for the Google Africa Developer Scholarship (GADS), where she helps facilitate the learning of students across Africa.

Leave a Reply