Как обнаружить свайп при флаттере

#flutter #dart #flutter-layout #swipe #swipe-gesture

#флаттер #dart #флаттер-макет #свайп #свайп-жест

Вопрос:

Я пытался использовать плагин swipe detector для flutter, чтобы перейти к новому экрану при пролистывании вправо, но это не работает, ошибок не выдается, и точка останова никогда не достигается при ее отладке. Я заглянул в 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 ))
],
),

            ),
      ]),
    ],
      ),
   )


 )


   );
  

Ответ №1:

Используйте 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(),
  ),
)
  

Комментарии:

1. Могу ли я обернуть весь экран? Мы хотим, чтобы на всем экране были возможности пролистывания вправо.

2. Я полагаю, вы тоже можете это сделать, обернув свой корневой виджет (тот, что внутри body Scaffold )

3. Свайп вправо работает, но содержимое ни одного из экранов не отображается.

4. добавить child в GestureDetector

5. Если этот код запускается много раз при пролистывании вправо, вы можете использовать это решение (возможно, есть лучший способ сделать это, но это работает для меня) int test = 0 if (подробности. delta.dx > 0 amp;amp; test == 0) { test = 1 /*ваш код * / } и при свайпе влево снова установите переменной (test) значение 0

Ответ №2:

Оберните свой 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
        }
    }
)
  

Комментарии:

1. неопределенная «чувствительность»?

2. Чувствительность — это целое число, используемое для выполнения действия, когда пользователь провел пальцем до некоторой степени.

3. Это отлично работает — я установил чувствительность на 8. Но мне любопытно, существует ли стандартная сумма, которая считалась бы «свайпом».

Ответ №3:

Чтобы откреститься от ответа @ Vimal Rai. Я обнаружил, что onHorizontalDragUpdate вызывает функцию при каждом обновлении. Это может привести к нежелательному поведению в вашем приложении. Если вы хотите, чтобы функция вызывалась только один раз при пролистывании, используйте onHorizontalDragEnd :

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

Комментарии:

1. спасибо, другие вызывают функцию слишком много раз

2. Это по-прежнему определяет перетаскивание по диагонали. Есть ли способ предотвратить это или игнорировать их?

3. details.velocity.pixelsPerSecond предоставляет смещение для значений x и y. Вы смотрели на них?

4. В данном коде направление инвертировано… > 0 это свайп вправо, < 0 это свайп влево.

5. спасибо @Luis! мне нужно было поставить восклицание после переменной ‘details.primaryVelocity!’ и когда оно было > 0, я думаю, что это свайп вправо. может быть, я вижу мир задом наперед?

Ответ №4:

В некоторых случаях GestureDetector не запускает события жестов, если он используется в качестве дочернего или родительского элемента других виджетов с возможностью прокрутки. Может быть, лучше! способ распознать любой жест — использовать Listener виджет.

 Listener(
   onPointerMove: (moveEvent){
      if(moveEvent.delta.dx > 0) {
          print("swipe right");
      }
   }
    child: PageView(...) // or any other widget
)
  

Комментарии:

1. Спасибо, ваше решение спасло мне день, я могу прикрепить его к многоуровневому просмотру страницы

2. очень хороший, идеальный вариант для свайпа к виджету

Ответ №5:

Это мое решение с использованием 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
    );
  }
  

Ответ №6:

Для меня другие решения здесь вызывали проблемы, такие как многократное срабатывание триггера за свайп. В итоге я использовал детектор жестов с 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}'));
            }),
      ),
    );
  }
}
  

Ответ №7:

Другой подход заключается в том, чтобы обернуть ваш виджет в dismissible.. в моем случае это хорошо сработало, потому что в нем есть интерактивная анимация, позволяющая пользователю четко прервать выполнение.

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

Комментарии:

1. Эта идея великолепна! Другие ответы запускают функцию свайпа много раз, но это так хорошо. Спасибо!

2. Сначала это казалось многообещающим, но, похоже, не существует способа использовать Dismissible для свайпа только в одном определенном горизонтальном направлении (т. Е. влево или вправо).

3. Для направления отклонения необходимо задать свойства «direction» для рассмотрения: Dismissible( ... direction: DismissDirection.endToStart,

Ответ №8:

Это решение основано на панорамировании, а не на перетаскивании. Если вы хотите обнаружить свайп только по одной оси и не хотите применять ограничения, такие как минимальное смещение или максимальное смещение поперечной оси, то перетаскивание проще и выполняет свою работу. Если вам нужно больше контроля или вы хотите определить 4 направления (начатое перемещение по горизонтали, но измененное на вертикальное, определяется как горизонтальное), панорамирование — ваш друг.

Это решение добавляет 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... 
  

Комментарии:

1. Могу ли я получить больше контекста для этих -> tapOperator (widget.settings.swipeUpOperator),

2. @LukeIrvin упс, этого не должно было быть там. Это просто функция, которая выполняется при обнаружении свайпа

Ответ №9:

Вы можете обнаружить свайпы, используя метод onPanUpdate из класса GestureDetector.

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

Ответ №10:

Попробуйте использовать просмотр страницы. Вы можете предоставить ему набор страниц, по которым затем сможете перемещаться, проводя пальцем вправо.

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

Комментарии:

1. Это не имеет ничего общего с обнаружением свайпа.

Ответ №11:

Поскольку я хотел использовать свайп для перемещения по картинкам в галерее, я попробовал первый ответ с помощью 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;
  }
}
  

который, конечно, настраивается в соответствии с заданным вопросом.

Ответ №12:

Объявляйте переменные следующим образом:

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

Объявите все эти методы под вашим методом сборки в классе State:

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 amp;amp; 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 amp;amp; 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;
}
  

Объявите конфигурацию жеста свайпа в конце файла следующим образом:

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

Просто объявите два перечисления в конце, вот так:

 enum SwipeDetectionBehavior {
 singular,
 singularOnEnd,
 continuous,
 continuousDistinct,
  

}

 enum SwipeDirection { left, right, up, down }
  

Теперь просто вызовите эти функции в вашем GestureDetector

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

Примечание: Скопируйте и вставьте приведенный выше код как есть, и он будет работать с очарованием 🙂

Ответ №13:

Я использую это для пролистывания вверх и вниз

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