Detecting Modal Sheet On Drag Down Event - flutter

Is it possible to intercept the event when the bottom sheet is being drag down, and show a message when it is about to close?

This is not a definitive answer, but I'm posting here because it provides a direction and is too long for a comment (because of links).
BottomSheet class defines BottomSheetDragStartHandler which is a callback for when the user begins dragging the bottom sheet.
_ModalBottomSheet passes this as onDragStart and this as onDragEnd argument to it's BottomSheet child.
So in theory, if you really want to, you can implement custom bottom sheet following the example of _ModalBottomSheet and tailoring the stuff according to need.

Try this,
i added bool variable that check bottomSheet is open or closed,
(also this action i printout in appBar).
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
bool isOpen = false;
var bottomSheetController;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: isOpen?Text("Open Bottom Sheet"):Text("Closed Bottom Sheet"),
),
body:
FloatingActionButton(
onPressed: () {
setState(() {
isOpen = !isOpen;
});
print('clicked on the bottom sheet');
if(isOpen) {
bottomSheetController = showBottomSheet(
backgroundColor: Colors.transparent,
context: context,
builder: (ctx) {
return ClipRRect(
borderRadius: BorderRadius.only(
topRight: Radius.circular(20),
topLeft: Radius.circular(20),
),
child: Container(
height: 150,
color: Colors.black,
child: TextField()
),
);
});
bottomSheetController.closed.then((value) {
setState(() {
isOpen = !isOpen;
});
});
} else {
Navigator.of(context).pop();
setState(() {
isOpen = !isOpen;
});
}
},
child: isOpen?Icon(Icons.arrow_downward):Icon(Icons.arrow_upward),
),
);
}
}

You could use this Widget: BottomSheet.
It offers methods: onDragStart(), onDragEnd() and a more useful method:onClosing which exactly does what you're looking for.
BottomSheet(
enableDrag: true // required
onClosing: () {
/* intercept close event */
showDialog();
}
)
However, the only caveat here is, we can only use this BottomSheet in a Scaffold.
Scaffold(
appBar: AppBar(...),
body: Container(...),
bottomSheet: BottomSheet(
onClosing: () {},
builder: () => Container(...),
)
)
This results in a persistent bottom sheet. To control its visibility, you will have to use a Stateful widget and have a boolean flag for the purpose.
See here for a complete example.

Related

Main app widget: setState call -> build() builds all widgets but screen not updated

App is a simple memory/guessing game with a grid of squares. Floating action button triggers a "New game" dialog, and a Yes response triggers setState() on the main widget. The print() calls show it is building all the Tile widgets in the grid, but as it returns, the old grid values are still showing. Probably done something stupid but not seeing it. Basic code is below. TIA if anyone can see what is missing/invalid/broken/etc.
Main.dart is the usual main() that creates a stateless HomePage which creates a stateful widget which uses this State:
class MemHomePageState extends State<MemHomePage> {
GameBoard gameBoard = GameBoard();
GameController? gameController;
int gameCount = 0, winCount = 0;
#override
void initState() {
super.initState();
gameController = GameController(gameBoard, this);
}
#override
Widget build(BuildContext context) {
if (kDebugMode) {
print("MemHomepageState::build");
}
gameBoard.newGame(); // Resets secrets and grids
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: GridView.count(
crossAxisCount: Globals.num_columns,
children: List.generate(Globals.num_columns * Globals.num_rows, (index) {
int x = index~/Globals.NR, y = index%Globals.NR;
int secret = gameBoard.secretsGrid![x][y];
var t = Tile(x, y, Text('$secret'), gameController!);
gameBoard.tilesGrid![x].add(t);
if (kDebugMode) {
print("Row $x is ${gameBoard.secretsGrid![x]} ${gameBoard.tilesGrid![x][y].secret}");
}
return t;
}),
),
// Text("You have played $gameCount games and won $winCount."),
),
floatingActionButton: FloatingActionButton(
onPressed: () => newGameDialog("Start a new game?"),
tooltip: 'New game?',
child: const Icon(Icons.refresh_outlined),
),
);
}
/** Called from the FAB and also from GameController "won" logic */
void newGameDialog(String message) {
showDialog<void>(
context: context,
barrierDismissible: false, // means the user must tap a button to exit the Alert Dialog
builder: (BuildContext context) {
return AlertDialog(
title: Text("New game?"),
content: Text(message),
//),
actions: <Widget>[
TextButton(
child: const Text('Yes'),
onPressed: () {
setState(() {
gameCount++;
});
Navigator.of(context).pop();
},
),
TextButton(
child: const Text('No'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
}
);
}
The Tile class is a StatefulWidget whose state determines what that particular tile should show:
import 'package:flutter/material.dart';
import 'gamecontroller.dart';
enum TileMode {
SHOWN,
HIDDEN,
CLEARED,
}
/// Represents one Tile in the game
class Tile extends StatefulWidget {
final int x, y;
final Widget secret;
final GameController gameController;
TileState? tileState;
Tile(this.x, this.y, this.secret, this.gameController, {super.key});
#override
State<Tile> createState() => TileState(x, y, secret);
setCleared() {
tileState!.setCleared();
}
}
class TileState extends State<Tile> {
final int x, y;
final Widget secret;
TileMode tileMode = TileMode.HIDDEN;
TileState(this.x, this.y, this.secret);
_unHide() {
setState(() => tileMode = TileMode.SHOWN);
widget.gameController.clicked(widget);
}
reHide() {
print("rehiding");
setState(() => tileMode = TileMode.HIDDEN);
}
setCleared() {
print("Clearing");
setState(() => tileMode = TileMode.CLEARED);
}
_doNothing() {
//
}
#override
Widget build(BuildContext context) {
switch(tileMode) {
case TileMode.HIDDEN:
return ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.teal,
),
onPressed: _unHide,
child: Text(''));
case TileMode.SHOWN:
return ElevatedButton(
onPressed: _doNothing,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
),
child: secret);
case TileMode.CLEARED:
return ElevatedButton(
onPressed: _doNothing,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.black12,
),
child: const Icon(Icons.check));
}
}
}
it looks like you are calling the following in your build function. That would cause everything to reset everytime it builds. Perhaps it belongs in init instead?
gameBoard.newGame(); // Resets secrets and grids
The original problem is that the Tile objects, although correctly created and connected to the returned main widget, did not have distinct 'key' values so they were not replacing the originals. Adding 'key' to the Tile constructor and 'key: UniqueKey()' to each Tile() in the loop, solved this problem. It exposed a related problem but is out of scope for this question. See the github link in the OP for the latest version.

Set barrier dismissible after dialog shown

This is the way I normally set the dialog's barrierDismissible field to true or false
showDialog(
barrierDismissible: false,
builder: ...
)
However, it implies that dialog is ALWAYS true or false.
Is there any way to start a dialog barrierDismissible as false and change it to true after one second?
It looks like flutter declarative approach wasn't applied to this widget. Therefore you should do everything yourself.
First handle yourself the tap with:
A general gesture detector which will be used to dismiss the dialog.
A gesture detector around your dialog in order to prevent the tap event to bubble up if it happened in the widget inside your dialog.
Second use a variable to state if the barrierDismissible should be activated or not, and modify this variable after 1 second. This is the variable which should be used be the general gesture detector in order to know if it should dismiss the dialog or not.
Here is a quick exemple, just tap the FAB:
import 'package:flutter/material.dart';
void main() => runApp(
MaterialApp(
home: MyApp(),
),
);
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
bool barrierDismissible = false;
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
showDialog(
context: context,
builder: (BuildContext context) {
barrierDismissible = false;
Future.delayed(
Duration(seconds: 1),
() => setState(() {
barrierDismissible = true;
}));
return GestureDetector(
onTap: () {
if (barrierDismissible) {
Navigator.of(context, rootNavigator: true).pop();
}
},
child: Material(
color: Colors.transparent,
child: GestureDetector(
onTap: () {},
child: Center(
child: Container(
height: 200,
width: 200,
color: Colors.red,
),
),
),
),
);
},
);
},
),
);
}
}
create bool variable and set that to the barrierDismissible property and use Future.delayed(duration:Duration(seconds:1)) to mak one second count then when the counter completes set the variable to true like this.
onPressed:(){
bool dismissible=false;
showDialog(context: context,barrierDismissible: dismissible); //add your child
Future.delayed(Duration(seconds: 1)).whenComplete(() {
setState(() {
dismissible=true;
});
});
}

How to rebuild a screen if confirm is pressed on bottom sheet?

I have built a home screen with is rendering cards using ListView.builder. These cards have a confirm button which fetches the confirmation status from firestore. When I tap on the confirm button, a bottom sheet appears asking whether I am sure. Once I tap Yes on the bottom sheet, I want the card on homepage to be rebuilt and change the button from confirm to confirm.
I used the setState to change the value at the onPressed event and it is successfully changing it but, the confirm button is not changing to confirmed.
Any leads on how to solve this issue would be really appreciated.
Homepage cards layout
class HomepageCards extends StatefulWidget {
final FirebaseUser user;
final Map cardDetails;
HomepageCards({#required this.user, this.cardDetails});
#override
_HomepageCardsState createState() => _HomepageCardsState();
}
class _HomepageCardsState extends State<HomepageCards> {
#override
Widget build(BuildContext context) {
// Confirmation status from firebase about the captain
bool isConfirmed = widget.cardDetails['c'];
final screenHeight = MediaQuery.of(context).size.height;
final screenWidth = MediaQuery.of(context).size.width;
return SingleChildScrollView(
padding: EdgeInsets.fromLTRB(screenHeight / 60, screenHeight / 90,
// UI Code here......
Container(
height: screenHeight / 80,
),
// Confirm Button checking condition and building UI accordingly
isConfirmed == true
? captainConfirmed(context, isConfirmed) // if confirmed then different button style widget
: confirmAndCancelButton(context, isConfirmed), //if not confirmed then show confirm and cancel button in the card
],
),
// Some UI
);
}
}
Once clicking on cancel, the bottom sheet:
Widget confirmCaptainBookingBottomSheet(
BuildContext context, bool isConfirmed) {
final screenHeight = MediaQuery.of(context).size.height;
final screenWidth = MediaQuery.of(context).size.width;
showModalBottomSheet(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Container(
// UI Code
Text(
'Do you want to confirm?',
style: TextStyle(
color: black.color,
fontSize: headSize.fontSize,
),
),
child: FlatButton(
child: Text(
'YES',
style: TextStyle(
color: cyan.color,
fontSize: headSize.fontSize),
),
onPressed: () {
print(isConfirmed);
setState(() {
// change the value of is confirmed which is used to build different buttons in the UI as shown in the above code
isConfirmed = true;
});
print(isConfirmed);
Navigator.pop(context);
}),
),
child: FlatButton(
child: Text(
'NO',
style: TextStyle(
color: cyan.color,
fontSize: headSize.fontSize),
),
onPressed: () {
Navigator.pop(context);
}),
),
});
}
You can create a function in _HomepageCardsState class which change state of isConfirmed and pass that function to widget where you want change the state. Then on onPressed of yes just give that function. it will change state of isConfirmed in _HomepageCardsState widget so you can see captainConfirmed widget.
I am leaving small demo which simulates how you can do that in your case.
I hope following code clear your idea.
class DeleteWidget extends StatefulWidget {
#override
_DeleteWidgetState createState() => _DeleteWidgetState();
}
class _DeleteWidgetState extends State<DeleteWidget> {
bool isConfirmed = false;
changeconfirmed() {
setState(() {
isConfirmed = !isConfirmed;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: Center(
child: isConfirmed
? Home1()
: Home2(
function: changeconfirmed,
),
)),
);
}
}
class Home1 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
child: Text("confirmed Widget"),
);
}
}
class Home2 extends StatelessWidget {
final Function function;
Home2({this.function});
#override
Widget build(BuildContext context) {
return RaisedButton(
child: Text("press"),
onPressed: function,
);
}
}

How change Future.delayed() duration, or circumvent having to do this

I have list of Dismissible widgets that I'd like to swipe left to delete. These have a an argument confirmDismis which takes a Future<bool>. I'd like to show a SnackBar once you've swiped an item allowing you to undo the delete for a few seconds. For this I have this method:
Future<bool> _confirmDismiss() {
var remove = true;
var duration = Duration(seconds: 5);
final snackbar = SnackBar(
duration: duration,
content: Text('This item was deleted'),
action: SnackBarAction(
label: 'Undo',
onPressed: () {
remove = false; //how to return false immediately?
},
),
);
_scaffoldKey.currentState.showSnackBar(snackbar);
return Future.delayed(duration,(){
return remove;
});
}
The snackbar is shown for 5 seconds, after that it disappears and the callback is triggered.
Now I'd like to return the item as soon as you hit the undo button. Right now you have to wait for the 5 seconds to pass for it to return.
I have tried timeout and Time() and many other things but I can't seem to make it work... Any ideas?
#Janneman, I don't think you need to use Future.delayed, that could be the reason why you now have to wait 5 secs. Just use the default SnackBar functions. Here is a quick example that shows SnackBar for 5 secs and quickly changes the text on Undo,
class MyApp extends StatefulWidget {
#override
MyAppState createState() => MyAppState();
}
class MyAppState extends State<MyApp> {
String text = 'Yay! A Snackbar';
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('SnackBar'),
),
body: Builder(builder: (BuildContext context) {
return Padding(
padding: EdgeInsets.all(20.0),
child: Center(
child: Column(children: <Widget>[
Text(text),
RaisedButton(
onPressed: () {
text = 'Snackbar changed';
setState(() {
});
final snackBar = SnackBar(
duration: Duration(seconds: 5),
content: Text(text),
action: SnackBarAction(
label: 'Undo',
onPressed: () {
text = 'Yay! A Snackbar';
setState(() {});
},
),
);
// Find the Scaffold in the widget tree and use
// it to show a SnackBar.
Scaffold.of(context).showSnackBar(snackBar);
},
child: Text('Show SnackBar'),
),
])));
})));
}
}
Hope this helps. Good luck!

How to play the same Flare animation on every tap?

I'm only able to run the animation when the object is tapped in this way?
class _MyHomePageState extends State<MyHomePage> {
bool isOpen = false;
#override
Widget build(BuildContext context) {
return Center(
child: GestureDetector(
onTap: () {
setState(() {
isOpen = !isOpen;
});
},
child: FlareActor('assets/heart.flr',
animation: isOpen ? 'run' : 'x'),
),
);
}
}
but what I need is to run the same animation on every tap, this doesn't work:
GestureDetector(
onTap: () {
setState(() {
});
},
child: FlareActor('assets/heart.flr',
animation: 'run'),
)
I also tried this:
GestureDetector(
onTap: () {
setState(() {
isOpen = !isOpen;
});
},
child: FlareActor('assets/heart.flr',
animation: isOpen ? 'run' : 'run'),
)
I recently had the same problem when I was implementing some things. And I got my question answered, You can use the FlareControls and just call play instead of using the keys to play the animation. Original post.
class _MyHomePageState extends State<MyHomePage> {
// Store a reference to some controls for the Flare widget
final FlareControls controls = FlareControls();
void _playSuccessAnimation() {
// Use the controls to trigger an animation.
controls.play("run");
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: FlareActor("assets/heart.flr",
animation: "run",
fit: BoxFit.contain,
alignment: Alignment.center,
// Make sure you use the controls with the Flare Actor widget.
controller: controls),
floatingActionButton: FloatingActionButton(
onPressed: _playSuccessAnimation,
tooltip: 'Play',
child: Icon(Icons.play_arrow),
),
);
}
}
I also have a video explaining how to integrate these animations and replay the same one.