Flutter Resizable Container using Gestures - flutter

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.

Related

How can I align little circles equally into a big circle?

I've made one big rounded container and now I want to align different tiny circle containers equally into it. How can I do that without adjusting the position all the time? Is there a better way so that even when I change the size of the tiny circles, the position remain equally?
this is a piece of code:
return Material(
color: Colors.black,
child: Center(
child: Stack(
textDirection: TextDirection.ltr,
children: [
DialBottomCircle,
Positioned(
//maybe as buttons
child: CircleNumbers(
textNumber: '0',
),
bottom: 0,
top: 260.0,
left: 245.0,
),
Positioned(
child: CircleNumbers(
textNumber: '9',
),
bottom: -70.0,
top: 225.0,
left: 175.0),
use CustomMultiChildLayout, something like this:
#override
Widget build(BuildContext context) {
var numbers = List.generate(10, (index) => '$index');
return Material(
shape: CircleBorder(),
color: Colors.red,
child: CustomMultiChildLayout(
delegate: FooDelegate(numbers.length),
children: [
for (var i = 0; i < numbers.length; i++)
LayoutId(
id: i,
child: Material(
elevation: 4,
color: Colors.white,
shape: CircleBorder(),
clipBehavior: Clip.antiAlias,
child: InkWell(
splashColor: Colors.orange,
onTap: () => print('${numbers[i]} pressed'),
child: FittedBox(
child: Text(numbers[i]),
),
),
),
),
],
),
);
);
class FooDelegate extends MultiChildLayoutDelegate {
final int numChildren;
FooDelegate(this.numChildren);
#override
void performLayout(Size size) {
final s = Size.square(size.shortestSide / 6.5);
final radius = (size.shortestSide - s.shortestSide) * 0.45;
final childConstraints = BoxConstraints.tight(s);
final delta = ((size - s) as Offset) / 2;
for (var i = 0; i < numChildren; i++) {
layoutChild(i, childConstraints);
var angle = i * math.pi / 6;
var offset = Offset(math.cos(angle), math.sin(angle)) * radius;
positionChild(i, offset + delta);
}
}
#override
bool shouldRelayout(covariant MultiChildLayoutDelegate oldDelegate) => true;
}

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 expand TextField using dragging and button click

How can I increase/decrease the number of maxLines in TextField with dragging and clicking button?
Screenshot:
Full code:
class YourPage extends StatefulWidget {
#override
_YourPageState createState() => _YourPageState();
}
class _YourPageState extends State<YourPage> {
double _maxHeight = 200, _minHeight = 44, _height = 44, _dividerHeight = 56, _offset = 19;
int _maxLines = 1;
static final Duration _fixDuration = Duration(milliseconds: 500);
Duration _duration = _fixDuration;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Padding(
padding: const EdgeInsets.all(20),
child: SizedBox(
height: _maxHeight,
child: Column(
children: <Widget>[
AnimatedContainer(
duration: _duration,
height: _height,
child: TextField(
decoration: InputDecoration(hintText: "Enter a message"),
maxLines: _maxLines,
),
),
Container(
height: _dividerHeight,
width: 200,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
IconButton(
icon: Icon(Icons.arrow_downward),
onPressed: () {
if (_height <= _maxHeight - _offset - _dividerHeight) {
setState(() {
_duration = _fixDuration;
_height += _offset;
_maxLines++;
});
}
},
),
GestureDetector(
child: Icon(Icons.drag_handle),
onPanUpdate: (details) {
setState(() {
_height += details.delta.dy;
_duration = Duration.zero;
// prevent overflow if height is more/less than available space
var maxLimit = _maxHeight - _dividerHeight;
var minLimit = 44.0;
if (_height > maxLimit)
_height = maxLimit;
else if (_height < minLimit) _height = minLimit;
_maxLines = 100;
});
},
),
IconButton(
icon: Icon(Icons.arrow_upward),
onPressed: () {
if (_height >= _minHeight + _offset) {
setState(() {
_duration = _fixDuration;
_height -= _offset;
});
}
},
),
],
),
)
],
),
),
),
);
}
}

Flutter sliding container into another container to show or hide some icons like with toolbar

in container you suppose i have AppBar() witch that i want to have another invisible container, like with this screen shot:
in that two other container are invisible and i want to show them by sliding from top to bottom or bottom to top for change visibility on visible or invisible
sliding from top to bottom to show or sliding that to top to hide
sliding from bottom to top to show or sliding that to bottom to hide
is any library to implementing this sliding animation?
Click on arrows to bring the top/bottom containers, and after that to hide those new containers, you can either drag them up/down or simply touch them.
void main() => runApp(MaterialApp(home: HomePage()));
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
static double _height = 100, _one = -_height, _two = _height;
final double _oneFixed = -_height;
final double _twoFixed = _height;
Duration _duration = Duration(milliseconds: 5);
bool _top = false, _bottom = false;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Slide")),
body: SizedBox(
height: _height,
child: Stack(
children: <Widget>[
Positioned(
left: 0,
right: 0,
height: _height,
child: GestureDetector(
onVerticalDragEnd: (details) {
if (details.velocity.pixelsPerSecond.dy >= 0) _toggleTop();
else _toggleBottom();
},
child: _myContainer(
color: Colors.yellow[800],
text: "Old Container",
child1: IconButton(
icon: Icon(Icons.arrow_downward),
onPressed: _toggleTop,
),
child2: IconButton(
icon: Icon(Icons.arrow_upward),
onPressed: _toggleBottom,
),
),
),
),
Positioned(
left: 0,
right: 0,
top: _one,
height: _height,
child: GestureDetector(
onTap: _toggleTop,
onPanEnd: (details) => _toggleTop(),
onPanUpdate: (details) {
_one += details.delta.dy;
if (_one >= 0) _one = 0;
if (_one <= _oneFixed) _one = _oneFixed;
setState(() {});
},
child: _myContainer(
color: _one >= _oneFixed + 1 ? Colors.red[800] : Colors.transparent,
text: "Upper Container",
),
),
),
Positioned(
left: 0,
right: 0,
top: _two,
height: _height,
child: GestureDetector(
onTap: _toggleBottom,
onPanEnd: (details) => _toggleBottom(),
onPanUpdate: (details) {
_two += details.delta.dy;
if (_two <= 0) _two = 0;
if (_two >= _twoFixed) _two = _twoFixed;
setState(() {});
},
child: _myContainer(
color: _two <= _twoFixed - 1 ? Colors.green[800] : Colors.transparent,
text: "Bottom Container",
),
),
),
],
),
),
);
}
void _toggleTop() {
_top = !_top;
Timer.periodic(_duration, (timer) {
if (_top) _one += 2;
else _one -= 2;
if (_one >= 0) {
_one = 0;
timer.cancel();
}
if (_one <= _oneFixed) {
_one = _oneFixed;
timer.cancel();
}
setState(() {});
});
}
void _toggleBottom() {
_bottom = !_bottom;
Timer.periodic(_duration, (timer) {
if (_bottom) _two -= 2;
else _two += 2;
if (_two <= 0) {
_two = 0;
timer.cancel();
}
if (_two >= _twoFixed) {
_two = _twoFixed;
timer.cancel();
}
setState(() {});
});
}
Widget _myContainer({Color color, String text, Widget child1, Widget child2, Function onTap}) {
Widget child;
if (child1 == null || child2 == null) {
child = Text(text, style: TextStyle(fontSize: 32, color: Colors.white, fontWeight: FontWeight.bold));
} else {
child = Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
child1,
child2,
],
);
}
return GestureDetector(
onTap: onTap,
child: Container(
color: color,
alignment: Alignment.center,
child: child,
),
);
}
}
Output:
If I got you correctly, this is the solution.
void main() => runApp(MaterialApp(home: HomePage()));
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
static double _height = 100, _offset = 10, _one = -(_height - _offset), _two = (_height - _offset);
final double _oneFixed = -(_height - _offset);
final double _twoFixed = (_height - _offset);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Testing")),
body: SizedBox(
height: _height,
child: Stack(
children: <Widget>[
Positioned(
left: 0,
right: 0,
height: _height,
child: _myContainer(color: Colors.grey[800], text: "Old Container"),
),
Positioned(
left: 0,
right: 0,
top: _one,
height: _height,
child: GestureDetector(
onPanUpdate: (details) {
_one += details.delta.dy;
if (_one >= 0) _one = 0;
if (_one <= _oneFixed) _one = _oneFixed;
setState(() {});
},
child: _myContainer(color: _one >= _oneFixed + 1 ? Colors.red[800] : Colors.transparent, text: "Upper Container"),
),
),
Positioned(
left: 0,
right: 0,
top: _two,
height: _height,
child: GestureDetector(
onPanUpdate: (details) {
_two += details.delta.dy;
if (_two <= 0) _two = 0;
if (_two >= _twoFixed) _two = _twoFixed;
setState(() {});
},
child: _myContainer(color: _two <= _twoFixed - 1 ? Colors.green[800] : Colors.transparent, text: "Bottom Container"),
),
),
],
),
),
);
}
Widget _myContainer({Color color, String text}) {
return Container(
color: color,
alignment: Alignment.center,
child: Text(text, style: TextStyle(fontSize: 32, color: Colors.white, fontWeight: FontWeight.bold)),
);
}
}

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!