67

I've tried using the swipe detector plugin for flutter to achieve navigating to a new screen on swipe right but it's not working, no errors are thrown and the breakpoint is never hit when I debug it. I looked into the GestureDector but I wasn't sure it would work for a swipe right scenario, we want it to navigate to a screen when anywhere on the screen is swiped right.

Here's my code using the plugin

 @override
 Widget build(BuildContext context){
return new Scaffold(
 appBar : LBAppBar().getAppBar(),
  //drawer: new LBDrawer().getDrawer(),
 body:  Container(
decoration: BoxDecoration(
    gradient: new LinearGradient(
        colors: [Color.fromRGBO(1,89,99, 1.0), Colors.grey],
        begin: Alignment.bottomLeft,
        end: Alignment.topRight
    )
),
 child: 
 SwipeDetector(
     onSwipeRight: () {
       Navigator.of(context).push(new MaterialPageRoute(builder: (BuildContext context) => new WidgetsPage())
        ); },
  child: Column(
  mainAxisAlignment: MainAxisAlignment.center,    
  children:[
    Row(
            children: [
          Container(
            margin: EdgeInsets.only(left: 20.0,top: 10.0, bottom: 10.0, right:30.0),
            child: Column(
  children: <Widget>[



            Text("Hi ${user.firstName}, Today is " + formatDate(), style: new TextStyle( color: Colors.white70, fontWeight: FontWeight.bold, fontSize: 19.0 
 )),




        ],

         ),
                 ),
                  ]
            ),

      Row(
        //ROW 1
        children: [
          Container(
            margin: EdgeInsets.only(left: 30.0,top: 60.0, bottom: 30.0, right:30.0),
            child: Column(
      children: <Widget>[
           GestureDetector(
                 child: Icon(
                 FontAwesomeIcons.checkSquare,
                 size: 50.0,
                 color: Colors.white70,
         ),
              onTap: () {
                        Navigator.of(context).push(new MaterialPageRoute(builder: (BuildContext context) => new CheckIn()));
                      }),
                Text("Check In", style: new TextStyle( color: Colors.white70, fontWeight: FontWeight.normal ))
  ],
 ),
              ),
              Container(
           margin: EdgeInsets.only(left: 50.0,top: 60.0, bottom: 30.0, right:30.0),
                child: Column(
      children: <Widget>[
           GestureDetector(

                 child: Icon(
                 FontAwesomeIcons.list,
                 size: 50.0,
                  color: Colors.white70,
         ),
              onTap: () {
                    Navigator.of(context).push(new MaterialPageRoute(builder: (BuildContext context) => new DayAtAGlance()));
                      }),
            Text("My Day", style: new TextStyle( color: Colors.white70, fontWeight: FontWeight.normal ))
],
 ),
               ),
            Container(
           margin: EdgeInsets.only(left: 30.0,top: 60.0, bottom: 30.0, right:30.0),
            child: Column(
  children: <Widget>[
       GestureDetector(
                 child: Icon(
                 FontAwesomeIcons.phone,
                 size: 45.0,
                  color: Colors.white70,
         ),
              onTap: () {
                    Navigator.of(context).push(new MaterialPageRoute(builder: (BuildContext context) => new CommunicationLinks()));
                      }),
              Text("Communication", style: new TextStyle( color: Colors.white70, fontWeight: FontWeight.normal ))
  ],
 ),
             ),
             ]
              ),
         Row(//ROW 2
          children: [
        Container(
           margin: EdgeInsets.only(left: 32.0,top: 50.0, bottom: 30.0),
            child: Column(
  children: <Widget>[
       GestureDetector(
                 child: Icon(
                 FontAwesomeIcons.dollarSign,
                 size: 50.0,
                  color: Colors.white70,
           ),
              onTap: () {
                   Navigator.of(context).push(new MaterialPageRoute(builder: (BuildContext context) => new Budget()));
                      }),
            Text("Budget", style: new TextStyle( color: Colors.white70, fontWeight: FontWeight.normal ))
],
 ),

              ),
        Container(
           margin: EdgeInsets.only(left: 75.0, top: 50.0, bottom: 30.0, right: 30.0),
            child: Column(
  children: <Widget>[
       GestureDetector(
                 child: Icon(
                 FontAwesomeIcons.trophy,
                 size: 50.0,
                  color:  Colors.white70,
         ),
              onTap: () {
                    print("Pressed");
                      }),
            Text("Goals", style: new TextStyle( color:  Colors.white70, fontWeight: FontWeight.normal ))
],
),

            ),
         Container(
           margin: EdgeInsets.only(left: 50.0, top: 50.0, bottom: 30.0, right: 20.0),
            child: Column(
  children: <Widget>[
       GestureDetector(
                 child: Icon(
                 FontAwesomeIcons.calendar,
                 size: 50.0,
                  color: Colors.white70,
         ),
              onTap: () {

                   Navigator.of(context).push(new MaterialPageRoute(builder: (BuildContext context) => new CalendarsPage()));
                      }),
            Text("Calendar", style: new TextStyle( color: Colors.white70, fontWeight: FontWeight.normal ))
  ],
),
            )
         ]),
      Row(// ROW 3
          children: [
        Container(
           margin: EdgeInsets.only(left: 30.0, top: 50.0, bottom: 30.0, right: 30.0),
            child: Column(
  children: <Widget>[
       GestureDetector(
                 child: Icon(
                 FontAwesomeIcons.comments,
                 size: 50.0,
                  color: Colors.white70,
         ),
              onTap: () {
                    print("Pressed");
                      }),
            Text("Community", style: new TextStyle( color: Colors.white70, fontWeight: FontWeight.normal ))
  ],
),

            ),
        Container(
           margin: EdgeInsets.only(left: 20.0, top: 50.0, bottom: 30.0, right: 20.0),
            child: Column(
  children: <Widget>[
       GestureDetector(
                 child: Icon(
                 FontAwesomeIcons.shoppingCart,
                 size: 50.0,
                 color: Colors.white70,
         ),
              onTap: () {
                     Navigator.of(context).push(new MaterialPageRoute(builder: (BuildContext context) => new ShoppingList()));
                      }),
            Text("Shopping", style: new TextStyle( color: Colors.white70, fontWeight: FontWeight.normal ))
  ],
),

              ),
          Container(
           margin: EdgeInsets.only(left: 50.0, top: 50.0, bottom: 30.0, right: 40.0),
            child: Column(
  children: <Widget>[
       GestureDetector(
                 child: Icon(
                 FontAwesomeIcons.solidCheckSquare,
                 size: 50.0,
                  color: Colors.white70,
         ),
              onTap: () {
                    Navigator.of(context).push(new MaterialPageRoute(builder: (BuildContext context) => new CheckOut()));
                      }),
            Text("Check Out", style: new TextStyle( color: Colors.white70, fontWeight: FontWeight.bold ))
],
),

            ),
      ]),
    ],
      ),
   )


 )


   );
Sam Cromer
  • 2,063
  • 10
  • 28
  • 50

13 Answers13

76

Use GestureDetector.onPanUpdate:

GestureDetector(
  onPanUpdate: (details) {
    // Swiping in right direction.
    if (details.delta.dx > 0) {}
  
    // Swiping in left direction.
    if (details.delta.dx < 0) {}
  },
  child: YourWidget(),
)

To cover all the area (passing the parent constraints to the widget), you can include SizedBox.expand.

SizedBox.expand(
  child: GestureDetector(
    onPanUpdate: (details) {
      // Swiping in right direction.
      if (details.delta.dx > 0) {}

      // Swiping in left direction.
      if (details.delta.dx < 0) {}
    },
    child: YourWidget(),
  ),
)
CopsOnRoad
  • 237,138
  • 77
  • 654
  • 440
62

Wrap your Widget in GestureDetector and use onHorizontalDragUpdate as,

GestureDetector(
    onHorizontalDragUpdate: (details) {  
        // Note: Sensitivity is integer used when you don't want to mess up vertical drag
        int sensitivity = 8;
        if (details.delta.dx > sensitivity) {
            // Right Swipe
        } else if(details.delta.dx < -sensitivity){
            //Left Swipe
        }
    }
);

Or if you're looking for a vertical swipe, you can use this code:

GestureDetector(
    onVerticalDragUpdate: (details) {
        int sensitivity = 8;
        if (details.delta.dy > sensitivity) {
            // Down Swipe
        } else if(details.delta.dy < -sensitivity){
            // Up Swipe
        }
    }
)
BananaNeil
  • 10,322
  • 7
  • 46
  • 66
Vimal Rai
  • 757
  • 5
  • 6
51

To piggy back off of @Vimal Rai's answer. I found that onHorizontalDragUpdate calls the function with every update. That could lead to unwanted behavior in your app. If you want the function to be called just once upon swiping, go with onHorizontalDragEnd:

GestureDetector(
    onHorizontalDragEnd: (DragEndDetails details) {
      if (details.primaryVelocity > 0) {
        // User swiped Left
      } else if (details.primaryVelocity < 0) {
        // User swiped Right
      }
    }
);
gil.fernandes
  • 12,978
  • 5
  • 63
  • 76
Luis
  • 701
  • 8
  • 7
  • 4
    thanx, others call the func too many times – Yavuz Tarhan Jan 23 '21 at 12:45
  • This still detects diagonal drags. Is there a way to prevent that or to ignore those? – Luke Irvin Oct 01 '21 at 15:40
  • details.velocity.pixelsPerSecond provides an offset for x and y values. Have you looked at those? – Luis Oct 02 '21 at 05:24
  • Note: If the user dragged, then stopped their finger for a second *then* lift their finger, the reported velocity in the details will be 0. – geisterfurz007 Mar 09 '22 at 10:21
  • 1
    The directionality is inverted in the given code... `> 0` is a swipe right, `< 0` is a swipe left. – Chuck Batson Oct 07 '22 at 17:39
  • 1
    thank you @Luis! i needed to put exclamation after the variable 'details.primaryVelocity!' and when it was > 0, i think that's swipe right. maybe i see the world backwards? – tmr Jan 21 '23 at 04:20
26

In some cases GestureDetector does not fire gesture events if it's used as a child or parent of other scrollable widgets. Maybe a better! way to detect any gesture is to use Listener widget.

Listener(
   onPointerMove: (moveEvent){
      if(moveEvent.delta.dx > 0) {
          print("swipe right");
      }
   }
    child: PageView(...) // or any other widget
)
Pars
  • 4,932
  • 10
  • 50
  • 88
14

This is my solution using GestureDetector. I put the handler inside onPanEnd, as onPanUpdate gets called multiple times when the swiping is in progress.

@override
  Widget build(BuildContext context) {
    String? swipeDirection;

    return GestureDetector(
      onPanUpdate: (details) {
        swipeDirection = details.delta.dx < 0 ? 'left' : 'right';
      },
      onPanEnd: (details) {
        if (swipeDirection == null) {
          return;
        }
        if (swipeDirection == 'left') {
          //handle swipe left event
        }
        if (swipeDirection == 'right') {
          //handle swipe right event
        }
      },
      child: //child widget
    );
  }
Bukunmi
  • 2,504
  • 1
  • 18
  • 15
Dean
  • 974
  • 2
  • 17
  • 28
10

For me the other solutions here were causing issues like having the trigger fire many times per swipe. I ended up using a gesture detector with onHorizontalDragEnd and it only fires once per swipe.

class MyPageView extends StatefulWidget {
  @override
  _MyPageViewState createState() => _MyPageViewState();
}

class _MyPageViewState extends State<MyPageView> {
  PageController _pageController;
  Duration pageTurnDuration = Duration(milliseconds: 500);
  Curve pageTurnCurve = Curves.ease;

  @override
  void initState() {
    super.initState();
    // The PageController allows us to instruct the PageView to change pages.
    _pageController = PageController();
  }

  void _goForward() {
    _pageController.nextPage(duration: pageTurnDuration, curve: pageTurnCurve);
  }

  void _goBack() {
    _pageController.previousPage(
        duration: pageTurnDuration, curve: pageTurnCurve);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: GestureDetector(
        // Using the DragEndDetails allows us to only fire once per swipe.
        onHorizontalDragEnd: (dragEndDetails) {
          if (dragEndDetails.primaryVelocity < 0) {
            // Page forwards
            print('Move page forwards');
            _goForward();
          } else if (dragEndDetails.primaryVelocity > 0) {
            // Page backwards
            print('Move page backwards');
            _goBack();
          }
        },
        child: PageView.builder(
            itemCount: 10,
            controller: _pageController,
            // NeverScrollableScrollPhysics disables PageView built-in gestures.
            physics: NeverScrollableScrollPhysics(),
            itemBuilder: (context, index) {
              return new Center(child: Text('item ${++index}'));
            }),
      ),
    );
  }
}
Merritt
  • 445
  • 1
  • 6
  • 14
8

Another approach is to wrap your widget in a dismissible.. this worked well in my case because it has an interactive animation, allowing a clear way for user to abort.

 Dismissible(
        key: UniqueKey(),
        child: yourWidget, //the widget you want the swipe to be detected on
        direction: DismissDirection.up, // or whatever
        confirmDismiss: (direction) {
          if (direction == DismissDirection.up) { // or other directions
            // Swiped up do your thing.
          }
          return Future.value(false); // always deny the actual dismiss, else it will expect the widget to be removed
        })
Lee Higgins
  • 2,966
  • 1
  • 13
  • 8
  • This idea is brilliant! Other answers triggers the swipe function many times but this is so good. Thanks! – batuhankrbb Jun 04 '22 at 12:51
  • This seemed promising at first, but there does not appear to be a way to use `Dismissible` for a swipe in only a single specific horizontal direction (i.e. either left or right). – Chuck Batson Oct 07 '22 at 17:23
  • There is a properties "direction" to set for the dismiss direction to consider: `Dismissible( ... direction: DismissDirection.endToStart, ` – sciack Dec 05 '22 at 03:12
3

This solution is based on pan rathen than drag. If you only want to detect swipe in one axis and you don't want to enforce restrictions like minimal displacement or maximun cross axis displacement, then drag is simpler and gets the job done. If you need more control or want to detect 4 directions (drap started horizontally but changed to vertical is detected as horizontal), pan is your friend.

This solution adds HitTestBehaviour.opaque to allow swipe above clickable elements and also fixes one case where taps with two fingers at start and end position of a swipe will trigger a swipe (unwanted behaviour).

import 'package:flutter/material.dart';

class SwipeDetector extends StatelessWidget {
  static const double minMainDisplacement = 50;
  static const double maxCrossRatio = 0.75;
  static const double minVelocity = 300;

  final Widget child;

  final VoidCallback? onSwipeUp;
  final VoidCallback? onSwipeDown;
  final VoidCallback? onSwipeLeft;
  final VoidCallback? onSwipeRight;

  SwipeDetector({
    required this.child,
    this.onSwipeUp,
    this.onSwipeDown,
    this.onSwipeLeft,
    this.onSwipeRight,
  });

  @override
  Widget build(BuildContext context) {
    DragStartDetails? panStartDetails;
    DragUpdateDetails? panUpdateDetails;

    return GestureDetector(
      onTapDown: (_) => panUpdateDetails = null,  // This prevents two fingers quick taps from being detected as a swipe
      behavior: HitTestBehavior.opaque, // This allows swipe above other clickable widgets
      child: child,
      onPanStart: (startDetails) => panStartDetails = startDetails,
      onPanUpdate: (updateDetails) => panUpdateDetails = updateDetails,
      onPanEnd: (endDetails) {
        if (panStartDetails == null || panUpdateDetails == null) return;

        double dx = panUpdateDetails!.globalPosition.dx -
            panStartDetails!.globalPosition.dx;
        double dy = panUpdateDetails!.globalPosition.dy -
            panStartDetails!.globalPosition.dy;

        int panDurationMiliseconds =
            panUpdateDetails!.sourceTimeStamp!.inMilliseconds -
                panStartDetails!.sourceTimeStamp!.inMilliseconds;

        double mainDis, crossDis, mainVel;
        bool isHorizontalMainAxis = dx.abs() > dy.abs();

        if (isHorizontalMainAxis) {
          mainDis = dx.abs();
          crossDis = dy.abs();
        } else {
          mainDis = dy.abs();
          crossDis = dx.abs();
        }

        mainVel = 1000 * mainDis / panDurationMiliseconds;

        // if (mainDis < minMainDisplacement) return;
        // if (crossDis > maxCrossRatio * mainDis) return;
        // if (mainVel < minVelocity) return;

        if (mainDis < minMainDisplacement) {
          debugPrint(
              "SWIPE DEBUG | Displacement too short. Real: $mainDis - Min: $minMainDisplacement");
          return;
        }
        if (crossDis > maxCrossRatio * mainDis) {
          debugPrint(
              "SWIPE DEBUG | Cross axis displacemnt bigger than limit. Real: $crossDis - Limit: ${mainDis * maxCrossRatio}");
          return;
        }
        if (mainVel < minVelocity) {
          debugPrint(
              "SWIPE DEBUG | Swipe velocity too slow. Real: $mainVel - Min: $minVelocity");
          return;
        }

        // dy < 0 => UP -- dx > 0 => RIGHT
        if (isHorizontalMainAxis) {
          if (dx > 0)
            onSwipeRight?.call();
          else
            onSwipeLeft?.call();
        } else {
          if (dy < 0)
            onSwipeUp?.call();
          else
            onSwipeDown?.call();
        }
      },
    );
  }
}

Usage

@override
Widget build(BuildContext context) {
return SwipeDetector(
      onSwipeUp:    // your on swipe ↑ handler,
      onSwipeRight: // your on swipe → handler,
      onSwipeDown:  // your on swipe ↓ handler,
      onSwipeLeft:  // your on swipe ← handler,
      child: Center... 
Bugzilla
  • 1,856
  • 1
  • 10
  • 16
2

You can detect swipes using the onPanUpdate method from GestureDetector class.

GestureDetector(onPanUpdate: (details) {
  if (details.delta.dx > 0)
    print("Dragging in +X direction");
  else
    print("Dragging in -X direction");

  if (details.delta.dy > 0)
    print("Dragging in +Y direction");
  else
    print("Dragging in -Y direction");
});
1

Try using PageView. You can give it a set of Pages which you can then navigate through by swiping right.

https://api.flutter.dev/flutter/widgets/PageView-class.html

sakina
  • 109
  • 1
  • 6
1

As I wanted to use swipe to move through pictures in gallery I tried the first answer with onPanUpdate. This, however, fired several times during the swipe, which is not viable here.

Therefore I ended up using this code:

child: GestureDetector(
  onHorizontalDragEnd: (details) => controller.swipe(details),
  child: Image.file(
      controller.photos[controller.iterator.value]),
),
void swipe(DragEndDetails details) {
  if (details.primaryVelocity == null) {
    return;
  }
  if (details.primaryVelocity! < 0) {
    int next = iterator.value + 1;
    if (next >= photos.length) next = 0;
    iterator.value = next;
  }
  if (details.primaryVelocity! > 0) {
    int next = iterator.value - 1;
    if (next < 0) next = photos.length - 1;
    iterator.value = next;
  }
}

which is of course adjustable to question asked.

narrei
  • 516
  • 2
  • 6
  • 19
0

Declare Varaibles like this:

Offset? _initialSwipeOffset;
Offset? _finalSwipeOffset;
SwipeDirection? _previousDirection;
SimpleSwipeConfig swipeConfig = const SimpleSwipeConfig();

Declare all these methods below your build method in State Class:

1)

void _onHorizontalDragStart(DragStartDetails details) {
 _initialSwipeOffset = details.globalPosition;
}
void _onHorizontalDragUpdate(DragUpdateDetails details) {
  _finalSwipeOffset = details.globalPosition;
  if (swipeConfig.swipeDetectionBehavior == SwipeDetectionBehavior.singularOnEnd) {
     return;
  }

final initialOffset = _initialSwipeOffset;
final finalOffset = _finalSwipeOffset;

if (initialOffset != null && finalOffset != null) {
  final offsetDifference = initialOffset.dx - finalOffset.dx;

  if (offsetDifference.abs() > swipeConfig.horizontalThreshold) {
    _initialSwipeOffset = swipeConfig.swipeDetectionBehavior == SwipeDetectionBehavior.singular ? null : _finalSwipeOffset;

    final direction = offsetDifference > 0 ? SwipeDirection.left : SwipeDirection.right;

    if (swipeConfig.swipeDetectionBehavior == SwipeDetectionBehavior.continuous || _previousDirection == null || direction != _previousDirection) {
      _previousDirection = direction;
    }
  }
 }
}
void _onHorizontalDragEnd(DragEndDetails details) {
if (swipeConfig.swipeDetectionBehavior == SwipeDetectionBehavior.singularOnEnd) 
 {
  final initialOffset = _initialSwipeOffset;
  final finalOffset = _finalSwipeOffset;

  if (initialOffset != null && finalOffset != null) {
    final offsetDifference = initialOffset.dx - finalOffset.dx;

    if (offsetDifference.abs() > swipeConfig.horizontalThreshold) {
      final direction = offsetDifference > 0 ? SwipeDirection.left : SwipeDirection.right;
      if (direction == SwipeDirection.left) {
        print("Swiping to Left");
        return;
      }
      print("Swiping to Right");
    }
  }
}

 _initialSwipeOffset = null;
 _previousDirection = null;
}

Declare Swipe Gesture Configuration at the end of the file like this:

class SimpleSwipeConfig {
  final double verticalThreshold;
  final double horizontalThreshold;
  final SwipeDetectionBehavior swipeDetectionBehavior;
  const SimpleSwipeConfig({
   this.verticalThreshold = 50.0,
   this.horizontalThreshold = 50.0,
   this.swipeDetectionBehavior = SwipeDetectionBehavior.singularOnEnd,
 });
}

Simple declare Two enums at the end like this:

enum SwipeDetectionBehavior {
 singular,
 singularOnEnd,
 continuous,
 continuousDistinct,

}

enum SwipeDirection { left, right, up, down }

Now, simple call these functions in your GestureDetector

GestureDetector(
  onHorizontalDragStart: _onHorizontalDragStart,
  onHorizontalDragEnd: _onHorizontalDragEnd,
  onHorizontalDragUpdate: _onHorizontalDragUpdate,
  child:...
 );

Note: Copy & paste above code as it is and it will work with charm :)

Ahmad hassan
  • 769
  • 4
  • 14
0

I use this for swiping up and down

GestureDetector(
      onVerticalDragEnd: (details) => swipe(details),
      child: ...
);
void swipe(DragEndDetails details) {
if (details.primaryVelocity == null) {
  return;
}
if (details.primaryVelocity! < 0) {
   //'up'
  setState(() {
    _value = true;
  });
}
if (details.primaryVelocity! > 0) {
 // 'down'
  setState(() {
    _value = false;
  });
}