Related
getting this error showing in red screen I update my packages
_TypeError Another exception was thrown: type 'Null' is not a subtype of type 'String' I tried to update the sdk and I'm not understanding where the problem is in this code. I added CircularProgressIndicator with condition statements
The relevant error-causing widget was:
quizpage
quizpage:file:///C:/Users/flutt/Downloads/quiz/flutter-quizstar-master/lib/quizpage.dart:57:18
class getjson extends StatefulWidget {
String langname;
getjson(this.langname);
#override
State<getjson> createState() => _getjsonState();
}
class _getjsonState extends State<getjson> {
late String assettoload;
// a function
setasset() {
if (widget.langname == "Science") {
assettoload = "assets/Science.json";
} else if (widget.langname == "Maths") {
assettoload = "assets/Maths.json";
} else if (widget.langname == "History") {
assettoload = "assets/History.json";
}
}
#override
Widget build(BuildContext context) {
setasset();
// and now we return the FutureBuilder to load and decode JSON
return FutureBuilder(
future:
DefaultAssetBundle.of(context).loadString(assettoload, cache: false),
builder: (context, snapshot) {
if (snapshot.hasData) {
List mydata = json.decode(snapshot.data.toString());
if (mydata == null) {
return Scaffold(
body: Center(
child: Text(
"Loading",
),
),
);
} else {
return quizpage(mydata: mydata);
}}
return CircularProgressIndicator();
},
);
}
}
class quizpage extends StatefulWidget {
final List mydata;
quizpage({required this.mydata});
#override
_quizpageState createState() => _quizpageState(mydata);
}
class _quizpageState extends State<quizpage> {
final List mydata;
_quizpageState(this.mydata);
Color colortoshow = Colors.indigoAccent;
Color right = Colors.green;
Color wrong = Colors.red;
int marks = 0;
int i = 1;
bool disableAnswer = false;
int j = 1;
int timer = 30;
String showtimer = "30";
var random_array;
Map<String, Color> btncolor = {
"a": Colors.indigoAccent,
"b": Colors.indigoAccent,
"c": Colors.indigoAccent,
"d": Colors.indigoAccent,
};
bool canceltimer = false;
genrandomarray(){
var distinctIds = [];
var rand = new Random();
for (int i = 0;; ) {
distinctIds.add(rand.nextInt(10));
random_array = distinctIds.toSet().toList();
if(random_array.length < 10){
continue;
}else{
break;
}
}
print(random_array);
}
#override
void initState() {
starttimer();
genrandomarray();
super.initState();
}
#override
void setState(fn) {
if (mounted) {
super.setState(fn);
}
}
void starttimer() async {
const onesec = Duration(seconds: 1);
Timer.periodic(onesec, (Timer t) {
setState(() {
if (timer < 1) {
t.cancel();
nextquestion();
} else if (canceltimer == true) {
t.cancel();
} else {
timer = timer - 1;
}
showtimer = timer.toString();
});
});
}
void nextquestion() {
canceltimer = false;
timer = 30;
setState(() {
if (j < 10) {
i = random_array[j];
j++;
} else {
Navigator.of(context).pushReplacement(MaterialPageRoute(
builder: (context) => resultpage(marks: marks),
));
}
btncolor["a"] = Colors.indigoAccent;
btncolor["b"] = Colors.indigoAccent;
btncolor["c"] = Colors.indigoAccent;
btncolor["d"] = Colors.indigoAccent;
disableAnswer = false;
});
starttimer();
}
void checkanswer(String k) {
// in the previous version this was
// mydata[2]["1"] == mydata[1]["1"][k]
// which i forgot to change
// so nake sure that this is now corrected
if (mydata[2][i.toString()] == mydata[1][i.toString()][k]) {
// just a print sattement to check the correct working
// debugPrint(mydata[2][i.toString()] + " is equal to " + mydata[1][i.toString()][k]);
marks = marks + 5;
// changing the color variable to be green
colortoshow = right;
} else {
// just a print sattement to check the correct working
// debugPrint(mydata[2]["1"] + " is equal to " + mydata[1]["1"][k]);
colortoshow = wrong;
}
setState(() {
btncolor[k] = colortoshow;
canceltimer = true;
disableAnswer = true;
});
// nextquestion();
// changed timer duration to 1 second
Timer(Duration(seconds: 2), nextquestion);
}
Widget choicebutton(String k) {
return Padding(
padding: EdgeInsets.symmetric(
vertical: 10.0,
horizontal: 20.0,
),
child: MaterialButton(
onPressed: () => checkanswer(k),
child: Text(
mydata[1][i.toString()][k],
style: TextStyle(
color: Colors.white,
fontFamily: "Alike",
fontSize: 16.0,
),
maxLines: 1,
),
color: btncolor[k],
splashColor: Colors.indigo[700],
highlightColor: Colors.indigo[700],
minWidth: 200.0,
height: 45.0,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(20.0)),
),
);
}
#override
Widget build(BuildContext context) {
SystemChrome.setPreferredOrientations(
[DeviceOrientation.portraitDown, DeviceOrientation.portraitUp]);
return WillPopScope(
onWillPop: () async{
return await showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(
"Welcome",
),
content: Text("You Can't Go Back At This Stage."),
actions: <Widget>[
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(
'Ok',
),
)
],
));
},
child: Scaffold(
body: Column(
children: <Widget>[
Expanded(
flex: 3,
child: Container(
padding: EdgeInsets.all(15.0),
alignment: Alignment.bottomLeft,
child: Text(
mydata[0][i.toString()] ,
style: TextStyle(
fontSize: 16.0,
fontFamily: "Quando",
),
),
),
),
Expanded(
flex: 6,
child: AbsorbPointer(
absorbing: disableAnswer,
child: Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
choicebutton('a'),
choicebutton('b'),
choicebutton('c'),
choicebutton('d'),
],
),
),
),
),
Expanded(
flex: 1,
child: Container(
alignment: Alignment.topCenter,
child: Center(
child: Text(
showtimer,
style: TextStyle(
fontSize: 35.0,
fontWeight: FontWeight.w700,
fontFamily: 'Times New Roman',
),
),
),
),
),
],
),
),
);
}
}
In your code some place your are show some variable in Text widget that might be null so change them to these:
Text(
mydata[1][i.toString()][k] ?? "",//<--- add this
style: TextStyle(
color: Colors.white,
fontFamily: "Alike",
fontSize: 16.0,
),
maxLines: 1,
)
and
Expanded(
flex: 3,
child: Container(
padding: EdgeInsets.all(15.0),
alignment: Alignment.bottomLeft,
child: Text(
mydata[0][i.toString()] ?? "",//<--- add this
style: TextStyle(
fontSize: 16.0,
fontFamily: "Quando",
),
),
),
),
Can you check if your expression is evaluating to null in Text() Widget,
just put some constant strings in text widgets and try to run.
if expression is evaluating to null, use ?? operation to provide default string value to Text Widget.
This is my code for making the star turn yellow and grey consistently, but it didn't work. Why?
Widget placeButton(BuildContext context, String name, String image, String id) {
var color;
var isClicked = true;
return Column(children: [
Padding(
padding: const EdgeInsets.fromLTRB(130.0, 0.0, 130.00, 0.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: SizedBox(
height: 300,
width: 400,
child: GestureDetector(
onTap: () {
Navigator.pushNamed(context, id);
},
child: Card(
child: ListTile(
title: Padding(
padding: const EdgeInsets.fromLTRB(20, 5, 20, 0),
child: Text(AppLocalizations.of(context)!.translate(name),
style: TextStyle(fontFamily: 'Manrope', fontSize: 18)),
),
subtitle: Image.network(
image,
height: 250,
),
),
),
),
),
),
),
Container(
height: 40,
width: 200,
padding: EdgeInsets.only(top: 0),
color: Colors.white,
child: ListTile(
leading: Text(
"Rating:",
style: TextStyle(fontFamily: 'Klasik'),
),
trailing: IconButton(
splashRadius: 20,
icon: Icon(Icons.star_border_outlined, color: color),
onPressed: () {
if (isClicked) {
color = Colors.grey;
isClicked = !isClicked;
} else {
color = Colors.yellow;
isClicked = !isClicked;
}
},
),
),
),
]);
}
P/S: Feel free to ask me about the code below. I tried setState but it didn't work. It returns error like this:
The function 'setState' isn't defined.
Try importing the library that defines 'setState', correcting the name to the name of an existing function, or defining a function named 'setState'.
setState is only available within the scope of a StatefulWidget. You might want change the StatelessWidget to a stateful one. Once that's done, you'll need to introduce the variables color and isClicked outside of the build method or any other method that's called from build (such as placeButton in this example). Then you can call setState and change those values.
Here's a simplified example
class MyWidget extends StatefulWidget {
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
// Define these outside of your build method
Color color = Colors.grey;
bool isClicked = false;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(child: placeButton(context, 'name', 'image', 'id')),
);
}
Widget placeButton(
BuildContext context,
String name,
String image,
String id,
) {
// rest of your function...
onPressed: () {
// Use setState to change the values
setState(() {
if (isClicked) {
color = Colors.grey;
isClicked = !isClicked;
} else {
color = Colors.yellow;
isClicked = !isClicked;
}
});
},
}
}
When using setState, Flutter will run the build function again and notice the values of color and isClicked have changed and use those new values to update the UI.
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 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);
}
}
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!