Related
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.
I have been trying to fix this bug without success. I am trying to make a Gridview.builder widget that will hold the buttons for a calculator. I want the widget to fit on the lower part of the screen without overflowing. I tried to fix it by wrapping a Gridview into a Contianer or a ConstrainedBox with a certain height and width. However, the Gridview still keeps overflowing.
Here is my code for the Gridview:
Expanded(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: ScreenHeight(context) * .3,
maxHeight: ScreenHeight(context) * .4,
),
child: Container(
margin: EdgeInsets.only(
right: ScreenHeight(context) * .02,
left: ScreenHeight(context) * .02),
child: GridView.builder(
physics: NeverScrollableScrollPhysics(),
itemCount: buttons.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: getCrossAxisCount(context)),
itemBuilder: (BuildContext context, int index) {
// Clear Button
if (index == 0) {
return CalcButton(
onTap: () {
setState(() {
userInput = '';
calcAnswer = '';
});
},
buttonText: buttons[index],
color: Colors.blue[50],
textColor: Colors.black,
);
}
// +/- button
else if (index == 1) {
return CalcButton(
buttonText: buttons[index],
color: Colors.blue[50],
textColor: Colors.black,
);
}
// % Button
else if (index == 2) {
return CalcButton(
onTap: () {
setState(() {
userInput += buttons[index];
});
},
buttonText: buttons[index],
color: Colors.blue[50],
textColor: Colors.black,
);
}
// Delete Button
else if (index == 3) {
return CalcButton(
onTap: () {
setState(() {
userInput = userInput.substring(
0, userInput.length - 1);
});
},
buttonText: buttons[index],
color: Colors.blue[50],
textColor: Colors.black,
);
}
// Equal_to Button
else if (index == 18) {
return CalcButton(
onTap: () {
setState(() {
equalPressed();
});
},
buttonText: buttons[index],
color: Colors.orange[700],
textColor: Colors.white,
);
}
// other buttons
else {
return CalcButton(
onTap: () {
setState(() {
userInput += buttons[index];
});
},
buttonText: buttons[index],
color: isOperator(buttons[index])
? Colors.blueAccent
: Colors.white,
textColor: isOperator(buttons[index])
? Colors.white
: Colors.black,
);
}
},
),
),
),
),
Here is my code for CalcButton:
class CalcButton extends StatelessWidget {
final color;
final textColor;
final String buttonText;
final onTap;
CalcButton({
this.color,
this.textColor,
this.buttonText = "",
this.onTap,
});
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Padding(
padding: EdgeInsets.all(.3),
child: ClipRRect(
child: Container(
color: getCalcButtonColor(calcTheme,
buttonText), //gets calc button color from created method
child: Center(
child: Text(
buttonText,
style: TextStyle(
color: getButtonTxtColor(calcTheme,
buttonText), //gets button txt color from created method
fontSize: ScreenWidth(context) * .05,
fontWeight: FontWeight.bold,
),
),
),
),
),
),
);
}
}
Here is how it looks:
Image 1
Image 2
Note that the ScreenHeight() and ScreenWidth() functions are functions that return the value of MediaQuery.of(context).size.width and MediaQuery.of(context).size.height
I have also tried to remove Expanded() and only use a ConstrainedBox, but the same thing still happens.
Any help will be much appreciated.
I want to implement story items as different widgets. Like in this example:
In this picture, only images are changed, but I want to change as whole widgets as story items.
I have tried the story_view package. But, in this package, only images and videos can be added. Is there any other library for that?
As explained by https://stackoverflow.com/users/8164116/daksh-gargas, story view can be easily implemented using stack pageview and a simple gesture detector.
Made a simple story view -
import 'package:flutter/material.dart';
class CustomStoryView extends StatefulWidget{
#override
_CustomStoryViewState createState() => _CustomStoryViewState();
}
class _CustomStoryViewState extends State<CustomStoryView> with SingleTickerProviderStateMixin {
final List _colorsList = [Colors.blue, Colors.red, Colors.green, Colors.yellow, Colors.grey, Colors.brown];
final PageController _controller = PageController();
double _progressIndicators;
int _page = 0;
AnimationController _animationController;
bool dragEnded = true;
Size _pageSize;
#override
void initState() {
_animationController = AnimationController(vsync: this, duration: Duration(seconds: 2));
_animationController.addListener(animationListener);
_animationController.forward();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_pageSize = MediaQuery.of(context).size;
_progressIndicators = (_pageSize.width - 100) / 6;
});
super.initState();
}
#override
void dispose() {
_animationController?.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
PageView.builder(
controller: _controller,
physics: NeverScrollableScrollPhysics(),
itemBuilder: (context, index)=>GestureDetector(
onLongPressStart: _onLongPressStart,
onLongPressEnd: _onLongPressEnd,
onHorizontalDragEnd: _onHorizontalDragEnd,
onHorizontalDragStart: _onHorizontalDragStart,
onHorizontalDragUpdate: _onHorizontalDragUpdate,
onTapUp: _onTapDown,
child: Container(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
color: _colorsList[index],
child: Center(child: InkWell(
onTap: (){
print("thiswasclicked $index");
},
child: Text("Somee random text", style: TextStyle(fontSize: 36),)),),
),
),
itemCount: _colorsList.length,
),
Positioned(
top: 48,
left: 0,
right: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: ([0,1,2,3,4,5].map((e) =>
(e == _page) ? Stack(
children: [
Container(
width: _progressIndicators,
height: 8 ,
color: Colors.black54,
),
AnimatedBuilder(
animation: _animationController,
builder: (ctx, widget){
return AnimatedContainer(
width: _progressIndicators * _animationController.value,
height: 8 ,
color: Colors.white,
duration: Duration(milliseconds: 100),
);
},
),
],
): Container(
width: _progressIndicators,
height: 8 ,
color: (_page >= e) ? Colors.white : Colors.black54,
)).toList()),
),)
],
),
);
}
animationListener(){
if(_animationController.value == 1){
_moveForward();
}
}
_moveBackward(){
if(_controller.page != 0){
setState(() {
_page = (_controller.page - 1).toInt();
_page = (_page < 0) ? 0 : _page;
_controller.animateToPage(_page, duration: Duration(milliseconds: 100), curve: Curves.easeIn);
_animationController.reset();
_animationController.forward();
});
}
}
_moveForward(){
if(_controller.page != (_colorsList.length - 1)){
setState(() {
_page = (_controller.page + 1).toInt();
_controller.animateToPage(_page, duration: Duration(milliseconds: 100), curve: Curves.easeIn);
_animationController.reset();
_animationController.forward();
});
}
}
_onTapDown(TapUpDetails details) {
var x = details.globalPosition.dx;
(x < _pageSize.width / 2) ? _moveBackward() : _moveForward();
}
_onHorizontalDragUpdate(d){
if (!dragEnded) {
dragEnded = true;
if (d.delta.dx < -5) {
_moveForward();
} else if (d.delta.dx > 5) {
_moveBackward();
}
}
}
_onHorizontalDragStart(d) {
dragEnded = false;
}
_onHorizontalDragEnd(d) {
dragEnded = true;
}
_onLongPressEnd(_){
_animationController.forward();
}
_onLongPressStart(_){
_animationController.stop();
}
}
This can be easily achieved with Stack, Container, and a GestureDetector to switch between pages/stories.
Why Stacks?
Flutter's Stack is useful if you want to overlap several
children in a simple way, for example, having some text and an image,
overlaid with a gradient and a button attached to the bottom.
To handle your "fixed" views, which are, in this case:
Top Progress bar... you can create your custom progress bar if you want.
That image and the user name...
Let's call them myTopFixedWidgets()
Row(children: [CircleAvatar(...),Column(children: [Text(...),Text(...)],)],)
Now, put your Widget that you want to display and that changes (your "story") as the first item of the Stacks and place the Widgets 1. and 2. (mentioned above) in the second item of the list.
Maintain a variable index to choose the widget that you want to display.
Stack(
children: <Widget>[
widgetsToShowAsAStory[index],
myTopFixedWidgets() //mentioned above
],
)
Wrap it inside GestureDetector
List<Widget> widgetsToShowAsAStory = [];
var index = 0;
....
GestureDetector(
onTap: () {
//If the tap is on the LEFT side of the screen then decrement the value of the index
index-= 1; //(check for negatives)
//If the tap is on the RIGHT side of the screen then increment the value of the index
index+= 1; //(check for the size of list)
//call
setState() {}
},
child: Stack(
children: <Widget>[
widgetsToShowAsAStory[index],
myTopFixedWidgets()
],
),)
and boom, you're good to go!
I found solutions from the story_view. But it doesnot match my requirement. We can only show different widgets as stories items in story_view.We can't perform any actions on widgets. To implement this story_view and to show different widgets as stories. Do like this.
First import story_view flutter dependencies from here.
Then import this in main.dart file.
import "package:story_view/story_view.dart";
StoryView(
controller: controller,
storyItems: [
StoryItem.inlineImage(
url:
"https://images.unsplash.com/photo-1536063211352-0b94219f6212?ixid=MXwxMjA3fDB8MHxzZWFyY2h8MXx8YmVhdXRpZnVsJTIwZ2lybHxlbnwwfHwwfA%3D%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=60",
controller: controller,
),
StoryItem(
new Container(
margin: EdgeInsets.all(12),
child: StaggeredGridView.countBuilder(
crossAxisCount: 2,
crossAxisSpacing: 10,
mainAxisSpacing: 12,
itemCount: imageList.length,
itemBuilder: (context, index) {
return Container(
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.all(
Radius.circular(15))),
child: ClipRRect(
borderRadius: BorderRadius.all(
Radius.circular(15)),
child: FadeInImage.memoryNetwork(
placeholder: kTransparentImage,
image: imageList[index],
fit: BoxFit.cover,
),
),
);
},
staggeredTileBuilder: (index) {
return StaggeredTile.count(
1, index.isEven ? 1.2 : 1.8);
}),
),
duration: aLongWeekend,
shown: true),
StoryItem(
new Container(
margin: EdgeInsets.all(12),
child: StaggeredGridView.countBuilder(
crossAxisCount: 2,
crossAxisSpacing: 10,
mainAxisSpacing: 12,
itemCount: imageList.length,
itemBuilder: (context, index) {
return Container(
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.all(
Radius.circular(15))),
child: ClipRRect(
borderRadius: BorderRadius.all(
Radius.circular(15)),
child: FadeInImage.memoryNetwork(
placeholder: kTransparentImage,
image: imageList[index],
fit: BoxFit.cover,
),
),
);
},
staggeredTileBuilder: (index) {
return StaggeredTile.count(
1, index.isEven ? 1.2 : 1.8);
}),
),
duration: aLongWeekend,
shown: true),
],
onStoryShow: (s) {
print("Showing a story");
},
onComplete: () {
print("Completed a cycle");
},
progressPosition: ProgressPosition.top,
repeat: false,
inline: false,
),
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;
}
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!