如何在Flutter中检测滑动手势

68

我尝试使用Flutter的swipe detector插件在向右滑动时导航到新屏幕,但它无法正常工作,没有错误,并且当我调试它时断点永远不会被触发。我研究了GestureDector,但我不确定它是否适用于向右滑动的情况,我们希望它在任何地方向右滑动时都能导航到一个屏幕。

这是我使用插件的代码

 @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 ))
],
),

            ),
      ]),
    ],
      ),
   )


 )


   );
13个回答

80

使用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(),
)

为了覆盖所有区域(将父约束传递给小部件),您可以使用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(),
  ),
)

我能够包裹整个屏幕吗?我们希望在整个屏幕上具有向右滑动的功能。 - Sam Cromer
我相信,你也可以通过包装根小部件(Scaffoldbody内的小部件)来实现这一点。 - CopsOnRoad
1
向右滑动已经可以工作,但是屏幕上没有任何内容显示。 - Sam Cromer
1
child 添加到 GestureDetector。 - Taras Lukavyi
如果这段代码在向右滑动时被触发多次,那么您可以使用此解决方案(可能有更好的方法,但这对我有效)。int test = 0 if (details.delta.dx > 0 && test == 0) { test += 1 /your code/ } 并且在向左滑动时将变量(test)再次设置为0。 - Viraj Doshi
这个解决方案搞乱了垂直滑动。 - Karue Benson Karue

64
将您的Widget包装在GestureDetector中,并使用onHorizontalDragUpdate,如下所示:
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
        }
    }
);

如果您正在寻找垂直滑动的方法,可以使用以下代码:

GestureDetector(
    onVerticalDragUpdate: (details) {
        int sensitivity = 8;
        if (details.delta.dy > sensitivity) {
            // Down Swipe
        } else if(details.delta.dy < -sensitivity){
            // Up Swipe
        }
    }
)

未定义的“灵敏度”? - Kamlesh
1
灵敏度是一个整数,用于在用户向上滑动到一定程度时执行某个操作。 - Vimal Rai
4
这个很好用 - 我将灵敏度设置为8。但我想知道是否有标准的滑动距离可以被认为是“滑动”。 - b0rgBart3
在iOS中使用这段代码时,左滑手势将正常工作,但会禁用在WebView中执行垂直滑动的能力。 - undefined

55

在 @Vimal Rai 的答案基础上,我发现onHorizontalDragUpdate会在每次更新时调用该函数。这可能会导致您的应用程序出现意外行为。如果您希望在滑动时仅调用一次该函数,请选择onHorizontalDragEnd

GestureDetector(
    onHorizontalDragEnd: (DragEndDetails details) {
      if (details.primaryVelocity > 0) {
        // User swiped Left
      } else if (details.primaryVelocity < 0) {
        // User swiped Right
      }
    }
);

4
谢谢,其他人调用该函数太多次。 - Yavuz Tarhan
这仍然会检测到对角拖动。有没有办法防止或忽略它们? - Luke Irvin
details.velocity.pixelsPerSecond 提供了 x 和 y 值的偏移量。你看过那些值了吗? - Luis
注意:如果用户拖动手指,然后停留一秒钟然后抬起手指,则详细信息中报告的速度将为0。 - geisterfurz007
1
在给定的代码中,方向性被反转了... > 0 表示向右滑动,< 0 表示向左滑动。 - Chuck Batson
2
谢谢@Luis!我需要在变量“details.primaryVelocity!”后面加上感叹号,并且当它大于0时,我认为这是向右滑动。也许我看世界的方式是相反的? - tmr

26
在某些情况下,如果GestureDetector作为其他可滚动小部件的子级或父级使用,则不会触发手势事件。也许更好的方法是使用Listener小部件来检测任何手势。
Listener(
   onPointerMove: (moveEvent){
      if(moveEvent.delta.dx > 0) {
          print("swipe right");
      }
   }
    child: PageView(...) // or any other widget
)

3
谢谢,你的解决方案救了我的一天,我可以将它附加到多级页面视图上。 - malik kurosaki
非常好,非常适合滑动到小部件。 - JahidRatul

15

这是我使用GestureDetector的解决方案。我将处理程序放在onPanEnd中,因为当滑动正在进行时,onPanUpdate会被多次调用。

@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
    );
  }

11

对于我来说,其他解决方案会导致触发器在每次滑动时触发多次的问题。我最终使用了手势检测器,并使用onHorizontalDragEnd,它每次只触发一次。

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}'));
            }),
      ),
    );
  }
}

8
另一种方法是将你的小部件包装在一个可解散的组件中... 在我的例子中,这种方法非常有效,因为它具有交互式动画,让用户能够清晰地中止操作。
 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
        })

这个想法太棒了!其他答案会多次触发滑动功能,但这个非常好。谢谢! - batuhankrbb
一开始看起来很有前途,但似乎没有办法仅在单个特定水平方向(即左或右)中使用Dismissible进行滑动。 - Chuck Batson
有一个属性 "direction" 可以设置为 dismiss 方向:`Dismissible( ... direction: DismissDirection.endToStart, - sciack

2
你可以使用GestureDetector类中的onPanUpdate方法来检测滑动。
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");
});

2
作为我想要使用滑动在图库中浏览照片,我尝试了第一个答案,使用onPanUpdate。然而,在滑动期间它会多次触发,这在这里是不可行的。
因此,我最终使用了这段代码:
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;
  }
}

这当然可以根据所提出的问题进行调整。


2

这个解决方案基于“pan”而不是“drag”。如果你只想检测一个轴上的滑动,且不想强制施加最小位移或最大交叉轴位移的限制,则“drag”更简单且可以完成工作。如果你需要更多控制或想要检测4个方向(水平开始但变为垂直被检测为水平),则“pan”是你的好朋友。

此解决方案添加了HitTestBehaviour.opaque以允许在可点击元素上方进行滑动,并修复了一种情况,即在滑动的起点和终点位置处用两根手指轻触会触发滑动(不需要的行为)。

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();
        }
      },
    );
  }
}

使用方法

@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... 

我能否获取更多关于这个的上下文信息 --> tapOperator(widget.settings.swipeUpOperator) - Luke Irvin
@LukeIrvin 哎呀,那不应该在那里。那只是在检测到滑动操作时要执行的函数。 - Bugzilla

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接