Why does my Flutter custom ScrollPhysics break GestureDetectors in the Scrollable? - flutter

Why does my Flutter custom ScrollPhysics break GestureDetectors in my Scrollable? I am using a SingleChildScrollView that uses a custom ScrollPhysics I wrote, and for some reason the GestureDetectors I have in the ScrollView don't react to touch at all, UNLESS the ScrollView is in overscroll.
Basically, unless I just recently scrolled to the scroll extent and the ScrollView is stopped at the scroll extent, I can't detect any gestures inside of the custom physics ScrollView. Detecting gestures inside of a normal ScrollView works just fine, of course.
Here's a video of my predicament; the blue ScrollView on the left uses the default ScrollPhysics and the amber one on the right uses my custom ScrollPhysics, with five tappable boxes in each ScrollView:
Here's the GitHub:
And here's the code itself:
import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';
import 'package:flutter/widgets.dart';
import 'dart:math' as math;
void main() {
runApp(FlutterSingleChildScrollViewAbsorbsGesturesExample());
}
class FlutterSingleChildScrollViewAbsorbsGesturesExample extends StatefulWidget {
#override
State<FlutterSingleChildScrollViewAbsorbsGesturesExample> createState() =>
FlutterSingleChildScrollViewAbsorbsGesturesExampleState();
}
class FlutterSingleChildScrollViewAbsorbsGesturesExampleState
extends State<FlutterSingleChildScrollViewAbsorbsGesturesExample> {
Color colorOfRegularPhysicsBoxOne = Colors.black;
Color colorOfRegularPhysicsBoxTwo = Colors.black;
Color colorOfRegularPhysicsBoxThree = Colors.black;
Color colorOfRegularPhysicsBoxFour = Colors.black;
Color colorOfRegularPhysicsBoxFive = Colors.black;
Color colorOfCustomPhysicsBoxOne = Colors.black;
Color colorOfCustomPhysicsBoxTwo = Colors.black;
Color colorOfCustomPhysicsBoxThree = Colors.black;
Color colorOfCustomPhysicsBoxFour = Colors.black;
Color colorOfCustomPhysicsBoxFive = Colors.black;
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'An example of how the SingleChildScrollView with custom ScrollPhysics looks like it is eating gestures '
'meant for its descendants',
home: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
SingleChildScrollView(
child: Container(
height: 1400.0,
width: 200.0,
color: Colors.lightBlue,
child: Center(
child: Column(
children: <Widget>[
GestureDetector(
onTap: () => setState(() {
colorOfRegularPhysicsBoxOne = Colors.white;
}),
child: Container(
color: colorOfRegularPhysicsBoxOne,
height: 100.0,
width: 100.0,
),
),
GestureDetector(
onTap: () => setState(() {
colorOfRegularPhysicsBoxTwo = Colors.white;
}),
child: Container(
color: colorOfRegularPhysicsBoxTwo,
height: 100.0,
width: 100.0,
),
),
GestureDetector(
onTap: () => setState(() {
colorOfRegularPhysicsBoxThree = Colors.white;
}),
child: Container(
color: colorOfRegularPhysicsBoxThree,
height: 100.0,
width: 100.0,
),
),
GestureDetector(
onTap: () => setState(() {
colorOfRegularPhysicsBoxFour = Colors.white;
}),
child: Container(
color: colorOfRegularPhysicsBoxFour,
height: 100.0,
width: 100.0,
),
),
GestureDetector(
onTap: () => setState(() {
colorOfRegularPhysicsBoxFive = Colors.white;
}),
child: Container(
color: colorOfRegularPhysicsBoxFive,
height: 100.0,
width: 100.0,
),
),
],
),
),
),
),
SingleChildScrollView(
physics: CustomSnappingScrollPhysicsForTheControlPanelHousing(stoppingPoints: [
0.0,
100.0,
200.0,
300.0,
400.0,
]),
child: Container(
height: 1400.0,
width: 200.0,
color: Colors.amberAccent,
child: Center(
child: Column(
children: <Widget>[
GestureDetector(
onTap: () => setState(() {
colorOfCustomPhysicsBoxOne = Colors.white;
}),
child: Container(
color: colorOfCustomPhysicsBoxOne,
height: 100.0,
width: 100.0,
),
),
GestureDetector(
onTap: () => setState(() {
colorOfCustomPhysicsBoxTwo = Colors.white;
}),
child: Container(
color: colorOfCustomPhysicsBoxTwo,
height: 100.0,
width: 100.0,
),
),
GestureDetector(
onTap: () => setState(() {
colorOfCustomPhysicsBoxThree = Colors.white;
}),
child: Container(
color: colorOfCustomPhysicsBoxThree,
height: 100.0,
width: 100.0,
),
),
GestureDetector(
onTap: () => setState(() {
colorOfCustomPhysicsBoxFour = Colors.white;
}),
child: Container(
color: colorOfCustomPhysicsBoxFour,
height: 100.0,
width: 100.0,
),
),
GestureDetector(
onTap: () => setState(() {
colorOfCustomPhysicsBoxFive = Colors.white;
}),
child: Container(
color: colorOfCustomPhysicsBoxFive,
height: 100.0,
width: 100.0,
),
),
],
),
),
),
),
],
),
);
}
}
class CustomSnappingScrollPhysicsForTheControlPanelHousing extends ScrollPhysics {
List<double> stoppingPoints;
SpringDescription springDescription = SpringDescription(mass: 100.0, damping: .2, stiffness: 50.0);
#override
CustomSnappingScrollPhysicsForTheControlPanelHousing({#required this.stoppingPoints, ScrollPhysics parent})
: super(parent: parent) {
stoppingPoints.sort();
}
#override
CustomSnappingScrollPhysicsForTheControlPanelHousing applyTo(ScrollPhysics ancestor) {
return new CustomSnappingScrollPhysicsForTheControlPanelHousing(
stoppingPoints: stoppingPoints, parent: buildParent(ancestor));
}
#override
Simulation createBallisticSimulation(ScrollMetrics scrollMetrics, double velocity) {
double targetStoppingPoint = _getTargetStoppingPointPixels(scrollMetrics.pixels, velocity, 0.0003, stoppingPoints);
return ScrollSpringSimulation(springDescription, scrollMetrics.pixels, targetStoppingPoint, velocity,
tolerance: Tolerance(velocity: .00003, distance: .003));
}
double _getTargetStoppingPointPixels(
double initialPosition, double velocity, double drag, List<double> stoppingPoints) {
double endPointBeforeSnappingIsCalculated =
initialPosition + (-velocity / math.log(drag)).clamp(stoppingPoints[0], stoppingPoints.last);
if (stoppingPoints.contains(endPointBeforeSnappingIsCalculated)) {
return endPointBeforeSnappingIsCalculated;
}
if (endPointBeforeSnappingIsCalculated > stoppingPoints.last) {
return stoppingPoints.last;
}
for (int i = 0; i < stoppingPoints.length; i++) {
if (endPointBeforeSnappingIsCalculated < stoppingPoints[i] &&
endPointBeforeSnappingIsCalculated < stoppingPoints[i] - (stoppingPoints[i] - stoppingPoints[i - 1]) / 2) {
double stoppingPoint = stoppingPoints[i - 1];
debugPrint(stoppingPoint.toString());
return stoppingPoint;
} else if (endPointBeforeSnappingIsCalculated < stoppingPoints[i] &&
endPointBeforeSnappingIsCalculated > stoppingPoints[i] - (stoppingPoints[i] - stoppingPoints[i - 1]) / 2) {
double stoppingPoint = stoppingPoints[i];
debugPrint(stoppingPoint.toString());
return stoppingPoint;
}
}
throw Error.safeToString('Failed finding a new scroll simulation endpoint for this scroll animation');
}
}

I found my own answer -
It turns out that my Scrollable's ScrollPosition was constantly calling goBallistic(velocity:0.0) on itself, which means that it wasn't animating, but it never went idle, so it was locked into a state where it didn't respond to pointer events.
The problem was with the BallisticScrollActivity, the portion of a fling on a scrollable that occurs after the pointer comes off the screen. Once a BallisticScrollAcitivity ends, its ScrollPosition calls ScrollPositionWithSingleContext.goBallistic(velocity: 0.0), which creates a simulation using the current ScrollPhysics' ScrollPhysics.createBallisticSimulation(scrollMetrics:this, velocity:0.0).
BallisticScrollActivity._end() =>
ScrollPositionWithSingleContext.goBallistic(velocity: 0.0) =>
ScrollPhysics.createBallisticSimulation(scrollMetrics:this, velocity:0.0)
However, the default ScrollPhysics has an if statement that says to return null if the current velocity is zero:
if (velocity.abs() < tolerance.velocity)
return null;
and ScrollPositionWithSingleContext.createBallisticSimulation calls ScrollPositionWithSingleContext.goIdle() if the Simulation it receives is null:
final Simulation simulation = physics.createBallisticSimulation(this, velocity);
if (simulation != null) {
beginActivity(new BallisticScrollActivity(this, simulation, context.vsync));
} else {
goIdle();
}
which disposes of the BallisticScrollActivity and Animations and allows the Scrollable to respond to touch events again.
So all I had to do was add
if (velocity.abs() < .0003) {
return null;
}
to my CustomScrollPhysics' createBallisticSimulation() before I returned my CustomScrollSimulation, and everything works pretty well.
Of course, there's the problem that there is no way to register a tap on a moving GestureDetector without first stopping the Scrollable from scrolling, which feels awful, but every app has that problem.
Hope this helps!

Related

How can I drag items onto an InteractiveViewer in the correct spot?

I'm trying to build an app which will have a page where users can drag objects onto a battlefield to create their own map. I am trying to have a little menu on the right side of the screen which will have the objects you can place, and the main area is an InteractiveViewer with a stack where I want to put the objects. In the code below, I can drag items onto the viewer, but I can't figure out how to put the objects in the right spot on the viewer. By "right spot", I mean that I want the objects to stay in the spot I put them, but I can't figure out how to get the coordinates of where the objects should be relative to the current viewport. I can't figure out how to get the right x and y offsets.
Here is my code:
class Battlefield extends StatefulWidget {
double mapWidth = 0;
double mapHeight = 0;
Battlefield({super.key}) {
mapHeight = 200 * 50.0; // note, these values will change
mapWidth = 200 * 50.0;
}
#override
_BattlefieldState createState() => _BattlefieldState();
}
class _BattlefieldState extends State<Battlefield> {
TransformationController _controller = TransformationController();
Box _box = Box();
List<Widget> _children = [];
List<Widget> getChildren() {
List<Widget> children = _children;
// this box I add so I can have a reference point for debugging
children.add(Positioned(
child: SizedBox(
child: ColoredBox(color: Colors.red),
width: 500,
height: 500,
),
left: widget.mapWidth / 2 - 250,
top: widget.mapHeight / 2 - 250,
));
return children;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Battlefield"),
),
body: ResponsiveScreen(
squarishMainArea: InteractiveViewer(
constrained: false,
transformationController: _controller,
minScale: 0.1,
maxScale: 3.0,
child: CustomPaint(
painter: GridBackground(),
child: SizedBox(
width: widget.mapWidth,
height: widget.mapHeight,
child: DragTarget<Box>(
builder: (context, candidateData, rejectedData) {
return Stack(
children: getChildren(),
);
},
onAccept: (box) {
setState(() {
_children.add(Positioned(
child: SizedBox(
child: ColoredBox(color: Colors.black),
width: 50,
height: 50,
),
left: box.xPos,
top: box.yPox,
));
});
},
),
),
)),
rectangularMenuArea: Container(
decoration: BoxDecoration(color: Colors.blue),
child: Draggable<Box>(
data: _box,
feedback: SizedBox(
child: ColoredBox(color: Colors.grey),
width: 50,
height: 50,
),
child: SizedBox(
child: ColoredBox(color: Colors.black),
width: 50,
height: 50,
),
onDragEnd: (details) {
// here is where I am setting the position of the box.
// I want it to be a property of the box so I can save it to a db
final x = _controller.value.getMaxScaleOnAxis();
final offset = _controller.value.getTranslation();
final viewportOffsetX = offset.x;
final viewportOffsetY = offset.y;
_box.xPos = details.offset.dx - (viewportOffsetX * 1 / x);
_box.yPox = details.offset.dy - (viewportOffsetY * 1 / x);
},
),
)),
);
}
}
class Box {
double xPos = 0;
double yPox = 0;
Box();
}
For reference, the ResponsiveScreen widget I am using is from here: https://github.com/flutter/samples/blob/main/game_template/lib/src/style/responsive_screen.dart

Flutter Resizable Container using Gestures

I want to create a resizable container which can be resized by user using horizontal drag.
This Gif can explain the requirement:
I tried GestureDetector.horizontal drag but the results are way off:
Container(
color: primary,
width: size.width,
height: 40,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
GestureDetector(
onHorizontalDragUpdate: (details) {
final double newWidth;
if (details.delta.dx > 0) {
// movement in positive direction
final newWidth = size.width + 1;
print(newWidth);
final updatedSize = Size(newWidth, size.height);
setState(() {
size = updatedSize;
});
} else {
// movement in negative direction
final newWidth = math.max(size.width - 1, 5).toDouble();
print(newWidth);
final updatedSize = Size(newWidth, size.height);
setState(() {
size = updatedSize;
});
}
},
child: const Icon(
Icons.navigate_before_rounded,
color: Colors.white,
),
),
GestureDetector(
onHorizontalDragUpdate: (det) {
if (det.delta.dx > 1) {
var newWidth = size.width + 1;
final updatedSize = Size(newWidth, size.height);
setState(() {
size = updatedSize;
});
} else {
var newWidth = size.width - 1;
newWidth = math.max(newWidth, 10);
final updatedSize = Size(newWidth, size.height);
setState(() {
size = updatedSize;
});
}
},
child: const Icon(
Icons.navigate_next_rounded,
color: Colors.white,
),
),
],
),
),
I am looking for a way to get size change of one side using drag, also the width should decrease or increase on the basis of dragging direction and if possible the whole container can be moved as well.
You can use Stack with Positioned widget to handle container sizing and to drag the full container I am using transform.
Run on dartPad
You can play with this widget.
class FContainer extends StatefulWidget {
FContainer({Key? key}) : super(key: key);
#override
State<FContainer> createState() => _FContainerState();
}
class _FContainerState extends State<FContainer> {
///initial position
double leftPos = 33;
double rightPos = 33;
double transformX = 0;
#override
Widget build(BuildContext context) {
return Center(
child: LayoutBuilder(
builder: (context, constraints) => Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
transform: Matrix4.translationValues(transformX, 0, 0),
height: 60,
width: constraints.maxWidth,
child: Stack(
children: [
Positioned(
top: 0,
bottom: 0,
left: leftPos,
right: rightPos,
child: Row(
children: [
GestureDetector(
onTap: () {},
onHorizontalDragUpdate: (details) {
leftPos = details.globalPosition.dx;
setState(() {});
debugPrint(leftPos.toString());
},
child: Container(
height: 60,
color: Colors.purple,
child: const Icon(
Icons.navigate_next_rounded,
color: Colors.black,
),
),
),
Expanded(
child: GestureDetector(
onTap: () {},
onHorizontalDragUpdate: (details) {
final midPos = details.delta;
transformX += midPos.dx;
setState(() {});
debugPrint(midPos.toString());
},
child: Container(
color: Colors.purple,
),
),
),
GestureDetector(
onTap: () {},
onHorizontalDragUpdate: (details) {
rightPos = constraints.maxWidth -
details.globalPosition.dx;
setState(() {});
debugPrint(rightPos.toString());
},
child: Container(
height: 60,
color: Colors.purple,
child: const Icon(
Icons.navigate_before_rounded,
color: Colors.black,
),
),
),
],
),
)
],
),
)
],
),
),
);
}
}
You can wrap with if condition to avoid getting out of the screen.

Flutter remove OverlayEntry if touch outside

I have a CustomDropDown, done with a OverlayEntry. The problem is that I have a StatefulWidget for that, which I place in my Screen simply like that:
CustomDropDownButton(
buttonLabel: 'Aus Vorauswahl wählen',
options: [
'1',
'2',
'3',
'4',
],
),
Now inside that CustomDropDownButton I can simply call floatingDropdown.remove(); where ever I want but how can I call that from a Parent-Widget?? I hope you understand my problem. Right now the only way to remove the overlay is by pressing the DropDownButton again, but it should be removed everytime the user taps outside the actual overlay.
I am quite lost here so happy for every help! Let me know if you need any more details!
This is the code for my CustomDropDownButton if that helps:
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../../../constants/styles/colors.dart';
import '../../../constants/styles/text_styles.dart';
import '../../../services/size_service.dart';
import 'drop_down.dart';
class CustomDropDownButton extends StatefulWidget {
String buttonLabel;
final List<String> options;
CustomDropDownButton({
required this.buttonLabel,
required this.options,
});
#override
_CustomDropdownState createState() => _CustomDropdownState();
}
class _CustomDropdownState extends State<CustomDropDownButton> {
late GlobalKey actionKey;
late double height, width, xPosition, yPosition;
bool _isDropdownOpened = false;
int _selectedIndex = -1;
late OverlayEntry floatingDropdown;
#override
void initState() {
actionKey = LabeledGlobalKey(widget.buttonLabel);
super.initState();
}
#override
Widget build(BuildContext context) {
return GestureDetector(
key: actionKey,
onTap: () {
setState(() {
if (_isDropdownOpened) {
floatingDropdown.remove();
} else {
findDropdownData();
floatingDropdown = _createFloatingDropdown();
Overlay.of(context)!.insert(floatingDropdown);
}
_isDropdownOpened = !_isDropdownOpened;
});
},
child: Container(
height: scaleWidth(50),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(width: 1.0, color: AppColors.black),
),
color: AppColors.white,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: scaleWidth(10),
),
Text(
widget.buttonLabel,
style: AppTextStyles.h5Light,
),
Spacer(),
_isDropdownOpened
? SvgPicture.asset(
'images/icons/arrow_down_primary.svg',
width: scaleWidth(21),
)
: SvgPicture.asset(
'images/icons/arrow_up_primary.svg',
width: scaleWidth(21),
),
SizedBox(
width: scaleWidth(10),
),
],
),
),
);
}
void findDropdownData() {
RenderBox renderBox =
actionKey.currentContext!.findRenderObject()! as RenderBox;
height = renderBox.size.height;
width = renderBox.size.width;
Offset? offset = renderBox.localToGlobal(Offset.zero);
xPosition = offset.dx;
yPosition = offset.dy;
}
OverlayEntry _createFloatingDropdown() {
return OverlayEntry(builder: (context) {
return Positioned(
left: xPosition,
width: width,
top: yPosition + height,
height: widget.options.length * height + scaleWidth(5),
child: DropDown(
itemHeight: height,
options: widget.options,
onOptionTap: (selectedIndex) {
setState(() {
widget.buttonLabel = widget.options[selectedIndex];
_selectedIndex = selectedIndex;
floatingDropdown.remove();
_isDropdownOpened = !_isDropdownOpened;
});
},
selectedIndex: _selectedIndex,
),
);
});
}
}
1. Return a ListView instead GestureDetector
2. Under Listview use that GestureDetector containing DropDown as one of the children.
3. Add another children(widgets) as GestureDetector and set onTap of each one as:
GestureDetector(
onTap: () {
if(isDropdownOpened){
floatingDropDown!.remove();
isDropdownOpened = false;
}
},
child: Container(
height: 200,
color: Colors.black,
),
)
In short you have to add GestureDetector to the part wherever you want the tapping should close overlay entry
** Full Code **
//This is to close overlay when you navigate to another screen
#override
void dispose() {
// TODO: implement dispose
floatingDropDown!.remove();
super.dispose();
}
Widget build(BuildContext context) {
return ListView(
children: [
Padding(
padding: EdgeInsets.all(20),
child: GestureDetector(
key: _actionKey,
onTap: () {
setState(() {
if (isDropdownOpened) {
floatingDropDown!.remove();
} else {
findDropDownData();
floatingDropDown = _createFloatingDropDown();
Overlay.of(context)!.insert(floatingDropDown!);
}
isDropdownOpened = !isDropdownOpened;
});
},
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8), color: Colors.orangeAccent),
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Row(
children: <Widget>[
Text(
widget.text,
style: TextStyle(color: Colors.white, fontSize: 20),
),
Spacer(),
Icon(
Icons.arrow_drop_down,
color: Colors.white,
),
],
),
),
),
),
GestureDetector(
onTap: () {
if(isDropdownOpened){
floatingDropDown!.remove();
isDropdownOpened = false;
}
},
child: Container(
height: 200,
color: Colors.black,
),
)
],
);
}
Let me know whether it helped or not
Listen to full screen onTapDown gesture and navigation event.
The screen' s gesture event:
#override
Widget build(BuildContext context) {
return RawGestureDetector(
gestures: {
PenetrableTapRecognizer: GestureRecognizerFactoryWithHandlers<PenetrableTapRecognizer>(
() => PenetrableTapRecognizer(),
(instance) {
instance.onTapDown = (_) => _handleGlobalGesture();
},
),
},
behavior: HitTestBehavior.opaque,
child: Scaffold(
),
);
}
void _handleGlobalGesture {
// insert or remove the popup menu
// a bool flag maybe helpful
}
class PenetrableTapRecognizer extends TapGestureRecognizer {
#override
void rejectGesture(int pointer) {
acceptGesture(pointer);
}
}

Flutter- per my code how to make material button invisible on swiping left and re-appear on swiping right

I am creating an APP which has a lot of emphasis on the image in the background as such, their is text in arabic on that image per line and I want to add "material buttons" on top of this text. I was able to do this ...but then I want the button to be invisible once I swipe left, and re-appear when I swipe to the right, I did use gesture Detector and it does print on the screen if I swipe right or swipe left ..I was trying to input the gesture detector within the material button but everytime I try this it sends an error that's why I have the gesture detector on the bottom of the whole code please help ...
please help
import 'dart:io';
import 'package:Quran_highlighter/main.dart';
import 'package:flutter/rendering.dart';
import 'package:system_shortcuts/system_shortcuts.dart';
import 'package:Quran_highlighter/Widgets/NavDrawer.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:zoom_widget/zoom_widget.dart';
import 'package:flutter/gestures.dart';
class Aliflaammeem extends StatefulWidget {
#override
_AliflaammeemState createState() => _AliflaammeemState();
}
class _AliflaammeemState extends State<Aliflaammeem> {
var nameList = new List<String>();
final items = List<String>.generate(20, (i) => "Item ${i + 1}");
List<MaterialButton> buttonsList = new List<MaterialButton>();
#override
void initState(){
super.initState();
nameList.add("I love");
nameList.add("my ALLAH");
nameList.add("SWT Very Much");
List<Widget> buildButtonswithName(){
int length = nameList.length;
for (int i=0; i<length; i++){
buttonsList.add(new MaterialButton(
height:40.0,
minWidth: 300.0,
color: Colors.blue,
textColor: Colors.white,
));
}
} }
List<String> labels = ['apple', 'banana', 'pineapple', 'kiwi'];
// List<VoidCallback> actions = [_buyApple, _doSomething, _downloadData, () => print('Hi')
// ];
bool _visible = true;
int _counter = 0;
double _initial = 0.0;
var textHolder = "";
changeTextEnglish() {
setState(() {
bool _visible = true;
_visible = _visible;
textHolder = "All Praise and Thanks is to Allah the lord of the worlds";
});
}
changeTextArabic() {
bool _visible = true;
setState(() {
_visible = _visible;
});
}
#override
Widget build(BuildContext context) {
final title = 'Dismissing Items';
// appBar: AppBar(
// title: Text('Para 1, Pg2'),
// backgroundColor: Colors.teal[400],
// SystemChrome.setPreferredOrientations(
// [DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]);
return Scaffold(
body: Center(
child: Stack(fit: StackFit.expand, children: <Widget>[
Stack(
children: <Widget>[
SingleChildScrollView(
scrollDirection: Axis.vertical,
child: SafeArea(
top: true,
bottom: true,
right: true,
left: true,
child: Image(
image: AssetImage('test/assets/quranpg0.png'),
fit: BoxFit.cover
),
),
),
],
),
Stack(
children: <Widget>[
// for(int i = 0; i< labels.length; i++)
// weather_app/lib/page/settings_page.dart -- line ~81
// ListView.builder(
// itemCount: items.length,
// itemBuilder: (context, index) {
// final item = items[index];
// return Dismissible(
// // Each Dismissible must contain a Key. Keys allow Flutter to
// // uniquely identify widgets.
// key: Key(item),
// // Provide a function that tells the app
// // what to do after an item has been swiped away.
// onDismissed: (direction) {
// // Remove the item from the data source.
// setState(() {
// items.removeAt(index);
// });
// // Then show a snackbar.
// Scaffold.of(context)
// .showSnackBar(SnackBar(content: Text("$item dismissed")));
// },
// // Show a red background as the item is swiped away.
// background: Container(color: Colors.green),
// secondaryBackground: Container(color: Colors.red),
// child: ListTile(title: Text('$item'))
// );
// },
// ),
Container(
child: Align(
alignment: Alignment(.27, 0.1
),
// child: Visibility(
// visible: _visible,
// maintainSize: true,
// maintainAnimation: true,
// maintainState: true,
child: MaterialButton(
height: 70.0,
// minWidth: 36.5,
minWidth: 85.0,
onPressed: () => changeTextArabic(),
onLongPress: () => changeTextEnglish(),
// child: Text(labels[i]),
child: Text('$textHolder'),
color: Colors.cyan[400],
// color: Colors.purple[300],
highlightColor: Colors.blue,
textColor: Colors.white,
padding: EdgeInsets.only(left: 10, top: 2, right: -1, bottom: 5
),
),
),
),
for(int i = 0; i< labels.length; i++)
Container(
child: Align(
alignment: Alignment(-.5, 0.1
),
// child: Text("The Most Loving",
// style: TextStyle(
// fontSize: 15.0,
// backgroundColor: Colors.cyan,
// height: 1.0,
// fontWeight: FontWeight.bold
// ),
child: MaterialButton(
height: 70.0,
minWidth: 36.5,
onPressed: () => changeTextArabic(),
onLongPress: () => changeTextEnglish(),
// Positioned(
// top: 21,
child: Text(labels[i]),
disabledTextColor: Colors.transparent,
color: Colors.cyan[300],
// color: Colors.purple[300],
highlightColor: Colors.blue,
textColor: Colors.white,
padding: EdgeInsets.only(left: 46, top: 2, right: -20, bottom: 5),
),
// ),
),
)
],
),
GestureDetector(onPanUpdate: (DragUpdateDetails details) {
if (details.delta.dx > 0) {
print("right swipe english");
changeTextEnglish();
setState(() {
});
} else if (details.delta.dx < 0) {
print("left swipe arabic");
changeTextArabic();
setState(() {
});
}
})
])));
}
}
I think I got want you want.
First I added a condition to display the MaterialButton like so:
(_visible) ? MaterialButton(...) : Container()
Then inside "changeTextEnglish" and "changeTextArabic":
I changed _visible to absolute value
I deleted your lines "bool _visible = ..." because here you where creating local variable inside the function and therefore could no longer access _visible as the attribute of _AliflaammeemState.
So "changeTextEnglish" and "changeTextArabic" became:
changeTextEnglish() {
setState(() {
_visible = true;
textHolder = "All Praise and Thanks is to Allah the lord of the worlds";
});
}
changeTextArabic() {
setState(() {
_visible = false;
});
}
Which fives me the following working code (I deleted your comment to be able to see the issue so maybe don't copy paste the entire code.
class Aliflaammeem extends StatefulWidget {
#override
_AliflaammeemState createState() => _AliflaammeemState();
}
class _AliflaammeemState extends State<Aliflaammeem> {
var nameList = new List<String>();
final items = List<String>.generate(20, (i) => "Item ${i + 1}");
List<MaterialButton> buttonsList = new List<MaterialButton>();
#override
void initState() {
super.initState();
nameList.add("I love");
nameList.add("my ALLAH");
nameList.add("SWT Very Much");
}
List<String> labels = ['apple', 'banana', 'pineapple', 'kiwi'];
bool _visible = true;
int _counter = 0;
double _initial = 0.0;
var textHolder = "";
changeTextEnglish() {
setState(() {
_visible = true;
textHolder = "All Praise and Thanks is to Allah the lord of the worlds";
});
}
changeTextArabic() {
setState(() {
_visible = false;
});
}
#override
Widget build(BuildContext context) {
final title = 'Dismissing Items';
return Scaffold(
body: Center(
child: Stack(fit: StackFit.expand, children: <Widget>[
Stack(
children: <Widget>[
SingleChildScrollView(
scrollDirection: Axis.vertical,
child: SafeArea(
top: true,
bottom: true,
right: true,
left: true,
child: Image(image: AssetImage('test/assets/quranpg0.png'), fit: BoxFit.cover),
),
),
],
),
Stack(
children: <Widget>[
Container(
child: Align(
alignment: Alignment(.27, 0.1),
child: _visible
? MaterialButton(
height: 70.0,
minWidth: 85.0,
onPressed: () => changeTextArabic(),
onLongPress: () => changeTextEnglish(),
child: Text('$textHolder'),
color: Colors.cyan[400],
highlightColor: Colors.blue,
textColor: Colors.white,
padding: EdgeInsets.only(left: 10, top: 2, right: -1, bottom: 5),
)
: Container(),
),
),
for (int i = 0; i < labels.length; i++)
Container(
child: Align(
alignment: Alignment(-.5, 0.1),
child: MaterialButton(
height: 70.0,
minWidth: 36.5,
onPressed: () => changeTextArabic(),
onLongPress: () => changeTextEnglish(),
child: Text(labels[i]),
disabledTextColor: Colors.transparent,
color: Colors.cyan[300],
highlightColor: Colors.blue,
textColor: Colors.white,
padding: EdgeInsets.only(left: 46, top: 2, right: -20, bottom: 5),
),
// ),
),
)
],
),
GestureDetector(onPanUpdate: (DragUpdateDetails details) {
if (details.delta.dx > 0) {
print("right swipe english");
changeTextEnglish();
setState(() {});
} else if (details.delta.dx < 0) {
print("left swipe arabic");
changeTextArabic();
setState(() {});
}
})
])));
}
}

Flutter: How to set boundaries for a Draggable widget?

I'm trying to create a drag and drop game. I would like to make sure that the Draggable widgets don't get out of the screen when they are dragged around.
I couldn't find an answer to this specific question. Someone asked something similar about constraining draggable area Constraining Draggable area but the answer doesn't actually make use of Draggable.
To start with I tried to implement a limit on the left-hand side.
I tried to use a Listener with onPointerMove. I've associated this event with a limitBoundaries method to detect when the Draggable exits from the left side of the screen. This part is working as it does print in the console the Offset value when the Draggable is going out (position.dx < 0). I also associated a setState to this method to set the position of the draggable to Offset(0.0, position.dy) but this doesn't work.
Could anybody help me with this?
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Draggable Test',
home: GamePlay(),
);
}
}
class GamePlay extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: <Widget>[
Row(
children: [
Container(
width: 360,
height: 400,
decoration: BoxDecoration(
color: Colors.lightGreen,
border: Border.all(
color: Colors.green,
width: 2.0,
),
),
),
Container(
width: 190,
height: 400,
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(
color: Colors.purple,
width: 2.0,
),
),
),
],
),
DragObject(
key: GlobalKey(),
initPos: Offset(365, 0.0),
id: 'Item 1',
itmColor: Colors.orange),
DragObject(
key: GlobalKey(),
initPos: Offset(450, 0.0),
id: 'Item 2',
itmColor: Colors.pink,
),
],
),
);
}
}
class DragObject extends StatefulWidget {
final String id;
final Offset initPos;
final Color itmColor;
DragObject({Key key, this.id, this.initPos, this.itmColor}) : super(key: key);
#override
_DragObjectState createState() => _DragObjectState();
}
class _DragObjectState extends State<DragObject> {
GlobalKey _key;
Offset position;
Offset posOffset = Offset(0.0, 0.0);
#override
void initState() {
WidgetsBinding.instance.addPostFrameCallback(_afterLayout);
_key = widget.key;
position = widget.initPos;
super.initState();
}
void _getRenderOffsets() {
final RenderBox renderBoxWidget = _key.currentContext.findRenderObject();
final offset = renderBoxWidget.localToGlobal(Offset.zero);
posOffset = offset - position;
}
void _afterLayout(_) {
_getRenderOffsets();
}
void limitBoundaries(PointerEvent details) {
if (details.position.dx < 0) {
print(details.position);
setState(() {
position = Offset(0.0, position.dy);
});
}
}
#override
Widget build(BuildContext context) {
return Positioned(
left: position.dx,
top: position.dy,
child: Listener(
onPointerMove: limitBoundaries,
child: Draggable(
child: Container(
width: 80,
height: 80,
color: widget.itmColor,
),
feedback: Container(
width: 82,
height: 82,
color: widget.itmColor,
),
childWhenDragging: Container(),
onDragEnd: (drag) {
setState(() {
position = drag.offset - posOffset;
});
},
),
),
);
}
}
Try this. I tweaked this from: Constraining Draggable area .
ValueNotifier<List<double>> posValueListener = ValueNotifier([0.0, 0.0]);
ValueChanged<List<double>> posValueChanged;
double _horizontalPos = 0.0;
double _verticalPos = 0.0;
#override
void initState() {
super.initState();
posValueListener.addListener(() {
if (posValueChanged != null) {
posValueChanged(posValueListener.value);
}
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: <Widget>[
_buildDraggable(),
]));
}
_buildDraggable() {
return SafeArea(
child: Container(
margin: EdgeInsets.only(bottom: 100),
color: Colors.green,
child: Builder(
builder: (context) {
final handle = GestureDetector(
onPanUpdate: (details) {
_verticalPos =
(_verticalPos + details.delta.dy / (context.size.height))
.clamp(.0, 1.0);
_horizontalPos =
(_horizontalPos + details.delta.dx / (context.size.width))
.clamp(.0, 1.0);
posValueListener.value = [_horizontalPos, _verticalPos];
},
child: Container(
child: Container(
margin: EdgeInsets.all(12),
width: 110.0,
height: 170.0,
child: Container(
color: Colors.black87,
),
decoration: BoxDecoration(color: Colors.black54),
),
));
return ValueListenableBuilder<List<double>>(
valueListenable: posValueListener,
builder:
(BuildContext context, List<double> value, Widget child) {
return Align(
alignment: Alignment(value[0] * 2 - 1, value[1] * 2 - 1),
child: handle,
);
},
);
},
),
),
);
}
I've found a workaround for this issue. It's not exactly the output I was looking for but I thought this could be useful to somebody else.
Instead of trying to control the drag object during dragging, I just let it go outside of my screen and I placed it back to its original position in case it goes outside of the screen.
Just a quick note if someone tries my code, I forgot to mention that I'm trying to develop a game for the web. The output on a mobile device might be a little bit odd!
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Draggable Test',
home: GamePlay(),
);
}
}
class GamePlay extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: <Widget>[
Row(
children: [
Container(
width: 360,
height: 400,
decoration: BoxDecoration(
color: Colors.lightGreen,
border: Border.all(
color: Colors.green,
width: 2.0,
),
),
),
Container(
width: 190,
height: 400,
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(
color: Colors.purple,
width: 2.0,
),
),
),
],
),
DragObject(
key: GlobalKey(),
initPos: Offset(365, 0.0),
id: 'Item 1',
itmColor: Colors.orange),
DragObject(
key: GlobalKey(),
initPos: Offset(450, 0.0),
id: 'Item 2',
itmColor: Colors.pink,
),
],
),
);
}
}
class DragObject extends StatefulWidget {
final String id;
final Offset initPos;
final Color itmColor;
DragObject({Key key, this.id, this.initPos, this.itmColor}) : super(key: key);
#override
_DragObjectState createState() => _DragObjectState();
}
class _DragObjectState extends State<DragObject> {
GlobalKey _key;
Offset position;
Offset posOffset = Offset(0.0, 0.0);
#override
void initState() {
WidgetsBinding.instance.addPostFrameCallback(_afterLayout);
_key = widget.key;
position = widget.initPos;
super.initState();
}
void _getRenderOffsets() {
final RenderBox renderBoxWidget = _key.currentContext.findRenderObject();
final offset = renderBoxWidget.localToGlobal(Offset.zero);
posOffset = offset - position;
}
void _afterLayout(_) {
_getRenderOffsets();
}
#override
Widget build(BuildContext context) {
return Positioned(
left: position.dx,
top: position.dy,
child: Listener(
child: Draggable(
child: Container(
width: 80,
height: 80,
color: widget.itmColor,
),
feedback: Container(
width: 82,
height: 82,
color: widget.itmColor,
),
childWhenDragging: Container(),
onDragEnd: (drag) {
setState(() {
if (drag.offset.dx > 0) {
position = drag.offset - posOffset;
} else {
position = widget.initPos;
}
});
},
),
),
);
}
}
I'm still interested if someone can find a proper solution to the initial issue :-)
you could use the property onDragEnd: of the widget Draggable and before setting the new position compare it with the height or width of your device using MediaQuery and update only if you didn't pass the limits of your screen, else set the new position to the initial one.
Example bellow :
Positioned(
left: position.dx,
top: position.dy,
child: Draggable(
maxSimultaneousDrags: 1,
childWhenDragging:
Opacity(opacity: .2, child: rangeEvent(context)),
feedback: rangeEvent(context),
axis: Axis.vertical,
affinity: Axis.vertical,
onDragEnd: (details) => updatePosition(details.offset),
child: Transform.scale(
scale: scale,
child: rangeEvent(context),
),
),
)
In the method updatePosition, you verify the new position before updating:
void updatePosition(Offset newPosition) => setState(() {
if (newPosition.dy > 10 &&
newPosition.dy < MediaQuery.of(context).size.height * 0.9) {
position = newPosition;
} else {
position = const Offset(0, 0);// initial possition
}
});