flutter web: InteractiveViewer - flutter

There are 10 boxes on the Artboard and the screen is big enough, you can see all 10 boxes.
On the other hand, if the screen size is small, all 10 boxes are not displayed on the screen.
I tried to move the 10 piece so that I could see the below, some of the boxes were cut off, so it didn't come out
I want to see all 10 boxes even when the screen is small. How do I solve this problem?
class ArtBoard extends StatefulWidget {
const ArtBoard({Key? key}) : super(key: key);
#override
_ArtBoardState createState() => _ArtBoardState();
}
class _ArtBoardState extends State<ArtBoard> {
TransformationController _transformController = TransformationController();
double _scale = 1;
Offset startOffset = Offset.zero;
#override
void initState() {
super.initState();
}
#override
void dispose() {
_transformController.dispose();
super.dispose();
}
void _handleViewerTransformed() =>
_scale = _transformController.value.row0[0];
#override
Widget build(BuildContext context) {
RandomColor _randomColor = RandomColor();
return Scaffold(
resizeToAvoidBottomInset: false,
backgroundColor: Colors.transparent,
body: Stack(
fit: StackFit.expand,
children: [
Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
color: Colors.grey.shade500,
child: Center(
child: Text('Design Board',
style: TextStyle(fontSize: 8, color: Colors.white70))),
),
InteractiveViewer(
transformationController: _transformController,
boundaryMargin: EdgeInsets.all(double.infinity),
constrained: true,
minScale: 0.1,
maxScale: 5.0,
panEnabled: true,
scaleEnabled: true,
onInteractionUpdate: (ScaleUpdateDetails details) {
//_transformationController.value.scale(1);
//_transformationController.value.scale(details.scale);
//print('Scale update:$details');
},
onInteractionEnd: (ScaleEndDetails details) {
double correctScaleValue =
_transformController.value.getMaxScaleOnAxis();
// unzoom when interaction has ended
setState(() {
print('${_transformController.value}');
_transformController.toScene(Offset.zero);
});
},
child: ListView.builder(
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: 10,
itemBuilder: (BuildContext context, int index) {
return Align(
alignment: Alignment.center,
child: Container(
alignment: Alignment.center,
width: 100,
height: 80,
color: _randomColor.randomColor(),
child: Text('Box :${index}')),
);
},
),
),
],
),
);
}
}

This is a problem flutter is known for because on like native code it doesn't handle size for you, I recommend using a package like sizer from https://pub.dev
SizedBox(
width: 100.sp.clamp(10,100)
)
Within this example ".sp" is a number (int and double) extension from the sizer package that gives you the size relative to the screen size then adding a ".clamp" makes it that you can have the size within your desired size.
10.sp.clamp(lowestPoint, maxPoint);

your this to get the longest side
final double longerSide = 100.w>100.h ? 100.w:100.h;
final double shorterSide = 100.w>100.h ? 100.h:100.w;
/// This builds a box using the lonerSide length and the shortSide length
Widget buildBox({required double lSide, required double sSide}) {
final averageHeight = longerSide / 10;
final averageWidth = shorterSide / 2;
return Container(width: averageWidth, height: averageHeight, color: Colors.red);
}

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

How do I resize an image within a container using a pinch gesture?

I am pulling an image from the camera or gallery and need to make is so that image can be moved and resized with a pinch gesture freely within the bounds of a container.
The movement of the image is fine however I am having trouble getting the image to resize when zoomed in or out on. I am currently using GestureDetector but cannot get it to work.
Based on the attached code is there a better method of achieving the pinch zoom or am I misusingGestureDetector or putting it in the wrong place?
Code:
import 'package:flutter/material.dart';
import 'dart:io';
double canvasWidth = 0.7;
double canvasHeight = 0.5;
double imageCurrentScale = 1.0;
double imageBaseScale = 1.0;
class CanvasAreaWidget extends StatefulWidget {
const CanvasAreaWidget({super.key});
#override
State<CanvasAreaWidget> createState() => CanvasArea();
}
class CanvasArea extends State<CanvasAreaWidget> {
// Setup for canvas properties
final textWidgetKey = GlobalKey();
double titleLeftPosition = 0;
double imageScale = 1.0;
double prevImageScale = 1.0;
Offset postition = const Offset(0,0);
void initPosition() {
setState(() {
postition = Offset((MediaQuery.of(context).size.width*canvasWidth/2) - 25, (MediaQuery.of(context).size.height*canvasHeight/2) -25);
});
}
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => initPosition());
}
void updateScale(double zoom) => setState(() => imageScale = prevImageScale * zoom);
void commitScale() => setState(() => prevImageScale = imageScale);
void updatePosition(Offset newPosition) {
setState(() {
newPosition = newPosition - Offset(MediaQuery.of(context).size.width*((1-canvasWidth)/2), MediaQuery.of(context).size.width*((1-canvasHeight)/2));
postition = newPosition;
});
}
#override
Widget build(BuildContext context) {
//initPosition();
return Scaffold(
// The appBar contains the title of the page (current section of the app) as well as the back key to return to the main menu.
extendBodyBehindAppBar: true,
appBar: AppBar(
leading: const Icon(Icons.chevron_left),
title: const Text('Card Editor'),
centerTitle: true,
backgroundColor: Colors.transparent,
elevation: 0,
),
// Parent stack that all other widgets will be children of.
body: Stack(
children: [
Positioned(
// Position of drag area.
left: MediaQuery.of(context).size.width * ((1-canvasWidth)/2),
top: MediaQuery.of(context).size.width * ((1-canvasHeight)/2),
child: ClipRect(
child: Stack(
children: [
Container(
// Size of the drag area.
width: MediaQuery.of(context).size.width * canvasWidth,
height: MediaQuery.of(context).size.height * canvasHeight,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
),
),
Positioned(
left: postition.dx,
top: postition.dy,
child: GestureDetector(
onScaleUpdate: (ScaleUpdateDetails details) => updateScale(details.scale),
onScaleEnd: (ScaleEndDetails details) => commitScale(),
child: Draggable(
maxSimultaneousDrags: 1,
feedback: const ImageItemWidget(),
childWhenDragging: Container(),
onDragEnd: (details) => updatePosition(details.offset),
child: const ImageItemWidget(),
),
),
),
],
),
),
),
],
),
);
}
}
File? recievedImage;
class ImageItemWidget extends StatefulWidget {
const ImageItemWidget ({super.key});
#override
State<ImageItemWidget> createState() => ImageItem();
}
class ImageItem extends State<ImageItemWidget> {
static void getImageFile(File? imageFile) {
recievedImage = imageFile;
print("File received");
}
#override
Widget build(BuildContext context) {
return SizedBox(
width: MediaQuery.of(context).size.width * canvasWidth,
height: MediaQuery.of(context).size.height * canvasHeight,
child: Image.file(
recievedImage!,
),
);
}
}

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

Multiple GestureDetector onPan event in Stack not working

I have to implement a custom analog clock where the clock hands indicate the breakfast,lunch and dinner time and user is able to reset the time by rotating the hands. I was able to draw the clock and hands with the help of canvas and CustomPaint and i haved stacked up in Stack Widget.I have added GestureDetector for each hand and using onPan events am able to rotate the hands also.But the issue is that when multiple hands are put in Stack only the last added clock hand is moving with the gesture others are not detecting the gesture.How to pass down the event to the stack so that all the gesture detectors detects it?
For drawing clock hands
class ClockHandComponent extends StatefulWidget {
final String title;
final int time;
final Color color;
const ClockHandComponent({Key key, this.title, this.time, this.color})
: super(key: key);
#override
ClockHandComponentState createState() {
return new ClockHandComponentState();
}
}
class ClockHandComponentState extends State<ClockHandComponent> {
int time;
double angle;
ClockHandPainter _clockHandPainter;
#override
void initState() {
time = widget.time;
angle = 2 * pi / 24 * time;
initialize();
super.initState();
}
#override
void didUpdateWidget(ClockHandComponent oldWidget) {
initialize();
super.didUpdateWidget(oldWidget);
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onPanDown: _onPanDown,
onPanUpdate: _onPanUpdate,
onPanEnd: _onPanEnd,
child: new Container(
width: double.infinity,
height: double.infinity,
child: new CustomPaint(
painter: _clockHandPainter,
)),
);
}
_onPanUpdate(DragUpdateDetails details) {
print(details);
RenderBox renderBox = context.findRenderObject();
var position = renderBox.globalToLocal(details.globalPosition);
angle = -coordinatesToRadians(_clockHandPainter.center, position);
print(angle);
initialize();
setState(() {});
}
_onPanEnd(_) {}
_onPanDown(DragDownDetails details) {
print(details);
print(angle);
}
initialize() {
_clockHandPainter =
new ClockHandPainter(widget.title, time, widget.color, angle);
}
double coordinatesToRadians(Offset center, Offset coords) {
var a = coords.dx - center.dx;
var b = center.dy - coords.dy;
return atan2(b, a);
}
}
For drawing clock face
class ClockFaceComponent extends StatefulWidget {
#override
ClockFaceComponentState createState() {
return new ClockFaceComponentState();
}
}
class ClockFaceComponentState extends State<ClockFaceComponent> {
#override
Widget build(BuildContext context) {
return new Padding(
padding: const EdgeInsets.all(10.0),
child: new AspectRatio(
aspectRatio: 1.0,
child: new Container(
width: double.infinity,
decoration: new BoxDecoration(
shape: BoxShape.circle,
color: ColorResource.clockBackgroundColor,
),
child: new Stack(
fit: StackFit.expand,
children: <Widget>[
//dial and numbers
new Container(
width: double.infinity,
height: double.infinity,
child: new CustomPaint(
painter: new ClockDialPainter(),
),
),
new Center(
child: new Container(
width: 40.0,
height: 40.0,
decoration: new BoxDecoration(
shape: BoxShape.circle,
color: Colors.white,
),
)),
//centerpoint
new Center(
child: new Container(
width: 15.0,
height: 15.0,
decoration: new BoxDecoration(
shape: BoxShape.circle,
color: ColorResource.clockCenterColor,
),
),
),
getClockHands()
],
),
),
),
);
}
getClockHands() {
return new AspectRatio(
aspectRatio: 1.0,
child: new Stack(
fit: StackFit.expand,
children: getHands(),
));
}
getHands() {
List<Widget> widgets = new List();
List<Hands> hands = Hands.getHands();
for (Hands hand in hands) {
widgets.add(ClockHandComponent(
title: hand.title,
time: hand.time,
color: hand.color,
));
}
return widgets;
}
}

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

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!