#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
в GestureDetector5. Если этот код запускается много раз при пролистывании вправо, вы можете использовать это решение (возможно, есть лучший способ сделать это, но это работает для меня) 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:
Попробуйте использовать просмотр страницы. Вы можете предоставить ему набор страниц, по которым затем сможете перемещаться, проводя пальцем вправо.
Комментарии:
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;
});
}