create button that pops up on hover - flutter

I've spent a lot of time trying to implement one thing, which is
a button that pops up in the center of a picture when the user's mouse enters the picture block, and when the mouse exits, button should disappear.
I've tried using many different widgets, such as InkWell and MouseArea but sadly didn't get expected behavior
Please share your thoughts if you know how to implement such behavior in flutter
Any help appreciated!

Instead of using AnimatedContainer, you could use AnimatedSwitcher which switches between a Button and a non-visible SizedBox whenever the onEnter or onExit event is called.
Just change the container to the picture you want to show.
Code example with DartPad:
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatefulWidget {
const MyApp({super.key});
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
bool showButton = false;
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Material App',
home: Scaffold(
appBar: AppBar(
title: const Text('Material App Bar'),
),
body: Center(
child: MouseRegion(
onEnter: (_) => setState(() {
showButton = true;
}),
onExit: (_) => setState(() {
showButton = false;
}),
child: Stack(
alignment: Alignment.center,
children: [
Container(
color: Colors.red[200],
width: 300,
height: 300,
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 250),
child: showButton ? ElevatedButton(
onPressed: () => print("Hi!"),
child: const Text("Hello")
) : const SizedBox.shrink(),
)
],
),
),
),
),
);
}
}

Related

How to get showMenu to create a PopUpMenu that fills the screen width?

I want to use the showMenu method to show a PopUpMenu that fills the screen, but it seems to be constrained by some maximum width that's like 4/5 of the screen.
import 'package:flutter/material.dart';
void main(List<String> args) async {
runApp(const MaterialApp(
home: Home(),
));
}
class Home extends StatelessWidget {
const Home({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Center(
child: TextButton(
onPressed: () {
showMenu(
context: context,
position: const RelativeRect.fromLTRB(0, 0, 0, 0),
items: [
const PopupMenuItem(
enabled: false,
child: SizedBox(
// width: popupButtonObject.size.width,
width: 400,
child: Placeholder(),
),
)
],
);
},
child: const Text('show menu'),
),
);
}
}
This GitHub issue seems to be exactly what I am looking for, and is a closed and resolved issue, but rookie me here can't seem to figure out what the fix ultimately was.
I did try to follow the "Fixed by #97798" text at the top of the GitHub issue page, which led to this page, but I'm not sure what to do with it.

How to show next page (Stateless widget) on click only in specific Container in SplitView, not all over the page

I have TestApp, where I have SplitView with 2 horizontal Containers. By clicking button in the first container on the left(blue) I want to show new page (DetailPage widget) but not all over the page, but only in the first Container. Now it shows on the whole screen. What is a best approach to do it?
import 'package:flutter/material.dart';
import 'package:split_view/split_view.dart';
void main() {
runApp(MaterialApp(
title: 'Test',
home: TestApp(),
));
}
class TestApp extends StatelessWidget {
const TestApp({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
home: SplitView(
children: [
Container(
color: Colors.blue,
child: ElevatedButton(
onPressed: () {
Navigator.push(context,
MaterialPageRoute(builder: (context) => DetailPage()));
},
child: const Text('CLICK')),
),
Container(color: Colors.yellow),
],
viewMode: SplitViewMode.Horizontal,
indicator: SplitIndicator(viewMode: SplitViewMode.Horizontal),
activeIndicator: SplitIndicator(
viewMode: SplitViewMode.Horizontal,
isActive: true,
),
controller: SplitViewController(limits: [null, WeightLimit(max: 1)]),
),
);
}
}
class DetailPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('')), body: Container(color: Colors.red));
}
}
When pushing a new page you will be overriding the old one, meaning the new page will not have a spiltView, the best way to do this is by changing the widget displayed inside of the splitView like this :
import 'package:flutter/material.dart';
import 'package:split_view/split_view.dart';
void main() {
runApp(MaterialApp(
title: 'Test',
home: TestApp(),
));
}
class TestApp extends StatefulWidget { // I have already changed the widgte to stateful here
const TestApp({Key? key}) : super(key: key);
#override
_TestAppState createState() => _TestAppState();
}
class _TestAppState extends State<TestApp> {
#override
Widget build(BuildContext context) {
bool Bool;
return MaterialApp(
home: SplitView(
children: [
if (Bool == false){
Container(
color: Colors.blue,
child: ElevatedButton(
onPressed: () {
setState(() {
Bool = !Bool; // this the method for inverting the boolean, it just gives it the opposite value
});
},
child: const Text('CLICK')),
),
}
else{
DetailPage()
},
Container(color: Colors.yellow),
],
viewMode: SplitViewMode.Horizontal,
indicator: SplitIndicator(viewMode: SplitViewMode.Horizontal),
activeIndicator: SplitIndicator(
viewMode: SplitViewMode.Horizontal,
isActive: true,
),
controller: SplitViewController(limits: [null, WeightLimit(max: 1)]),
),
);
}
}
class DetailPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('')), body: Container(color: Colors.red));
}
}
Above I defined a bool called Bool, when rendering the page it checks if Bool is false, in that case it returns the blue widget, if it is true then it returns the red one, and when you click on the button it inverts the bool and updates the page.
Please note that for updating the page you have to use setState which rebuilds the widget, and to use it you have to use a stateful widget since stateless widget is static and cannot be changed.
Also I haven't tested the code because I don't have split_view package, but you should be able to copy and paste it just fine, if you get any errors please let me know.
When you use Navigator.push your routing to a new page and creating a new state. I think you should use showGeneralDialog instead.
showGeneralDialog(
context: context,
pageBuilder: (BuildContext context,
Animation<double> animation, Animation<double> pagebuilder) {
return Align(
alignment: Alignment.centerLeft,
child: Card(
child: Container(
alignment: Alignment.topLeft,
color: Colors.amber,
//show half the screen width
width: MediaQuery.of(context).size.width / 2,
child: IconButton(
icon: const Icon(Icons.cancel),
onPressed: () {
Navigator.pop(context);
}))),
);
});
try to create new Navigator within Container:
GlobalKey<NavigatorState> _navKey = GlobalKey();
home: SplitView(
children: [
Container(
child: Navigator(
key: _navKey,
onGenerateRoute: (_) => MaterialPageRoute<dynamic>(
builder: (_) {
return Container(
color: Colors.blue,
child: ElevatedButton(
onPressed: () {
Navigator.push(context,
MaterialPageRoute(builder: (context) => DetailPage()));
},
child: const Text('CLICK')),
);
},
),
),),

Flutter: AnimatedSwitcher in Stack not animating in desired position

I am trying to use an AnimatedSwitcher within Stack. This leads to very strange behaviour of the animation. It animates the respective child widget (a red box in my case) in the center of my Stack and upon completion it snaps to the top left corner of my screen(which is where I would also like the animation to to take place). When I switch back, the same odd behaviour occurs.
My code looks as follows:
import 'package:flutter/material.dart';
void main() => runApp(MaterialApp(home: Home()));
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
bool _showMenu = false;
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Stack(
children: [
GestureDetector(
onTap: () => setState(() => _showMenu = !_showMenu),
child: SizedBox.expand(
child: Container(
color: Colors.yellow,
),
),
),
AnimatedSwitcher(
duration: Duration(milliseconds: 500),
child: _showMenu
? Container(
key: UniqueKey(),
height: 200,
width: 200,
color: Colors.red,
)
: Container())
],
),
),
);
}
}
Which produces the following behaviour on the tap-event somewhere on the screen:
Any ideas why the red box is not animated in the top left corner but only goes there once the animation has finished?
The problem lies, as #Marino Zorilla pointed out, in the unique key I specified for my animating widget. Once I removed this key and also changed the "empty" Container (for the false-condition of my ternary operation) to a SizedBox it works as desired.
Apparently, this has to do with how flutter works internally (when the element tree and the widget tree are compared to determine which widgets need to be rebuild). If the widget changes to a different type (like in my case from Container to SizedBox) no key is needed for flutter to know that this widget needs to be rebuild.
The correct code looks as follows:
import 'package:flutter/material.dart';
void main() => runApp(MaterialApp(home: Home()));
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
bool _showBox = false;
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Stack(
children: [
GestureDetector(
onTap: () => setState(() => _showBox = !_showBox),
child: SizedBox.expand(
child: Container(
color: Colors.yellow,
),
),
),
AnimatedSwitcher(
duration: Duration(milliseconds: 500),
child: _showBox
? Container(
height: 200.0,
width: 200.0,
color: Colors.red,
)
: SizedBox(),
)
],
)),
);
}
}

How to close a specific Flutter AlertDialog?

Steps to reproduce:
Copy paste the below code in DartPad.dev/flutter
Hit run
Click the Do Api Call button
you should see two popups, one below and one above
After 5 seconds, the one below is desired to close not the one above, instead, the one above closes
How to close the one below and leave the one above open ?
import 'package:flutter/material.dart';
final Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: CloseSpecificDialog(),
),
),
);
}
}
class CloseSpecificDialog extends StatefulWidget {
#override
_CloseSpecificDialogState createState() => _CloseSpecificDialogState();
}
class _CloseSpecificDialogState extends State<CloseSpecificDialog> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: RaisedButton(
child: Text('Do API call'),
onPressed: () async {
showDialogBelow();
showDialogAbove();
await Future.delayed(Duration(seconds: 5));
closeDialogBelowNotAbove();
},
)),
);
}
void showDialogBelow() {
showDialog(
context: context,
builder: (BuildContext contextPopup) {
return AlertDialog(
content: Container(
width: 350.0,
height: 150.0,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
CircularProgressIndicator(),
Text('I am below (you should not see this after 5 sec)'),
],
),
),
),
);
});
}
void showDialogAbove() {
showDialog(
context: context,
builder: (BuildContext contextPopup) {
return AlertDialog(
content: Container(
height: 100.0,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
CircularProgressIndicator(),
Text('I am above (this should not close)'),
],
),
),
),
);
});
}
/// This should close the dialog below not the one above
void closeDialogBelowNotAbove() {
Navigator.of(context).pop();
}
}
I had a similar requirement for my applications and had to spend quite some time to figure out the approach.
First I will tell you what advice I've got/read online which did not work for me:
Store BuildContext of each dialog from builder function when calling showDialog
Using Navigator.pop(context, rootNavigator: true)
removeRoute method on Navigator
None of these worked. #1 and #2 are a no-go because pop method can only remove the latest route/dialog on the navigation stack, so you can't really remove dialog that is placed below other dialog.
#3 was something I was hoping would work but ultimately it did not work for me. I tried creating enclosing Navigator for specific widget where I'm displaying the dialogs but pushing dialog as new route caused dialog being treated as page.
Solution: using Overlay widget
This is not a perfect solution but Overlay widget is actually used internally by other Flutter widgets, including Navigator. It allows you to control what gets placed in which order so it also means you can decide which element on overlay to remove!
My approach was to create a StatefulWidget which would contain a Stack. This stack would render whatever else passed to it and also Overlay widget. This widget would also hold references to OverlayEntry which are basically identifiers for dialogs themselves.
I'd use GlobalKey to reference the Overlay's state and then insert and remove dialogs (OverlayEntry) as I wished.
There is a disadvantage to this though:
No back button support on Android, so pressing back won't close the dialog.¹
Dialog positioning - you have to manage centering of your dialog yourself, as well as setting up the backdrop.²
Animations - you will have to implement these yourself as well. (You might want to fade in/ fade out backdrop, change position of dialog when opening and closing).
You can find interactive example on this dartpad or you can see the code here:
import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final GlobalKey<OverlayState> _overlay = GlobalKey<OverlayState>();
OverlayEntry? _dialog1;
OverlayEntry? _dialog2;
#override
void initState() {
super.initState();
Timer(const Duration(seconds: 3), () {
_openDialog1();
debugPrint('Opened dialog 1. Dialog should read: "Dialog 1"');
Timer(const Duration(seconds: 2), () {
_openDialog2();
debugPrint('Opened dialog 2. Dialog should read: "Dialog 2"');
Timer(const Duration(seconds: 3), () {
_closeDialog1();
debugPrint('Closed dialog 1. Dialog should read: "Dialog 2"');
Timer(const Duration(seconds: 5), () {
_closeDialog2();
debugPrint('Closed dialog 2. You should not see any dialog at all.');
});
});
});
});
}
#override
void dispose() {
_closeDialog1();
_closeDialog2();
super.dispose();
}
Future<void> _openDialog1() async {
_dialog1 = OverlayEntry(
opaque: false,
builder: (dialogContext) => CustomDialog(
title: 'Dialog 1', timeout: false, onClose: _closeDialog1));
setState(() {
_overlay.currentState?.insert(_dialog1!);
});
}
Future<void> _openDialog2() async {
_dialog2 = OverlayEntry(
opaque: false,
builder: (dialogContext) => CustomDialog(
title: 'Dialog 2', timeout: false, onClose: _closeDialog2));
setState(() {
_overlay.currentState?.insert(_dialog2!);
});
}
Future<void> _closeDialog1() async {
setState(() {
_dialog1?.remove();
_dialog1 = null;
});
}
Future<void> _closeDialog2() async {
setState(() {
_dialog2?.remove();
_dialog2 = null;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Stack(
children: <Widget>[
Align(
child:
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
TextButton(onPressed: _openDialog1, child: const Text('Open 1')),
TextButton(onPressed: _openDialog2, child: const Text('Open 2')),
])),
Align(
alignment: Alignment.bottomCenter,
child: Text(
'Opened 1? ${_dialog1 != null}\nOpened 2? ${_dialog2 != null}'),
),
Overlay(key: _overlay),
],
),
);
}
}
class CustomDialog extends StatefulWidget {
const CustomDialog({
Key? key,
required this.timeout,
required this.title,
required this.onClose,
}) : super(key: key);
final String id;
final bool timeout;
final String title;
final void Function() onClose;
#override
createState() => _CustomDialogState();
}
class _CustomDialogState extends State<CustomDialog>
with SingleTickerProviderStateMixin {
late final Ticker _ticker;
Duration? _elapsed;
final Duration _closeIn = const Duration(seconds: 5);
late final Timer? _timer;
#override
void initState() {
super.initState();
_timer = widget.timeout ? Timer(_closeIn, widget.onClose) : null;
_ticker = createTicker((elapsed) {
setState(() {
_elapsed = elapsed;
});
});
_ticker.start();
}
#override
void dispose() {
_ticker.dispose();
_timer?.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Positioned(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: Stack(children: [
GestureDetector(
onTap: widget.onClose,
child: Container(
color: Colors.transparent,
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height)),
BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: AlertDialog(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
title: Text(widget.title),
content: SizedBox(
height: MediaQuery.of(context).size.height / 3,
child: Center(
child: Text([
'${_elapsed?.inMilliseconds ?? 0.0}',
if (widget.timeout) ' / ${_closeIn.inMilliseconds}',
].join('')))),
actions: [
TextButton(
onPressed: widget.onClose, child: const Text('Close'))
],
)),
]));
}
}
In my example you can see that when the app runs, it will start up Timer which will fire other timers. This only demonstrates that you are able to close/open specific dialogs programatically. Feel free to comment out initState method if you don't want this.
1: Since this solution does not use Navigator at all, you can't use WillPopScope to detect back button press. It's a shame, it'd be great if Flutter had a way to attach listener to back button press.
2: showDialog method does lot for you and you basically have to re-implement what it does within your own code.
Popping will remove route which is added the latest, and showDialog just pushes a new route with dialogue you can directly use the Dialog widgets in a Stack and manage the state using a boolean variable To Achieve same the effect,
class _MyHomePageState extends State<MyHomePage> {
bool showBelow = true;
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
await Future.delayed(Duration(seconds: 5));
setState(() {
showBelow = false;
});
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: <Widget>[
if(showBelow) AlertDialog(
title: Text('Below..'),
content: Text('Beyond'),
),
AlertDialog(
title: Text('Above..'),
),
],
),
);
}
}
Remove
await Future.delayed(Duration(seconds: 5));
closeDialogBelowNotAbove();
Add Future.delayed
void showDialogAbove() {
showDialog(
context: context,
builder: (BuildContext contextPopup) {
Future.delayed(Duration(seconds: 5), () {
closeDialogBelowNotAbove();
});
return AlertDialog(
content: Container(
height: 100.0,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
CircularProgressIndicator(),
Text('I am above (this should not close)'),
],
),
),
),
);
});
}
Note: Navigator.pop() method always pop above alert/widget available on the screen, as it works with BuildContext which widget currently has.

How to intercept flutter back-button when keyboard is shown

I want to intercept the back-button of the soft keyboard in flutter. So when I want to close the keyboard by pressing the back-button I want an additional function to be called.
How can I do that?
Keyboard Back button
you can use the keyboard_visibility package to achieve this.
Working Example
the following code displays a SnackBar once the keyboard is dismissed.
import 'package:flutter/material.dart';
import 'package:keyboard_visibility/keyboard_visibility.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
GlobalKey<ScaffoldState> _key;
#override
void initState() {
super.initState();
_key = GlobalKey<ScaffoldState>();
KeyboardVisibilityNotification().addNewListener(
onHide: () {
_key.currentState.showSnackBar(
SnackBar(
content: Text("Keyboard closed"),
),
);
},
);
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
key: _key,
body: Center(
child: TextField(),
),
),
);
}
}
you can use the https://pub.dev/packages/flutter_keyboard_visibility package to achieve this.
import 'package:flutter/material.dart';
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
import 'package:flutter_keyboard_visibility_example/keyboard_dismiss_demo.dart';
import 'package:flutter_keyboard_visibility_example/provider_demo.dart';
void main() {
runApp(App());
}
class App extends StatelessWidget {
const App({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Demo(),
);
}
}
class Demo extends StatelessWidget {
#override
Widget build(BuildContext context) {
return KeyboardDismissOnTap(
child: Scaffold(
appBar: AppBar(
title: Text('Keyboard Visibility Example'),
),
body: Center(
child: Padding(
padding: EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => ProviderDemo()),
);
},
child: Text('Provider Demo'),
),
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => KeyboardDismissDemo()),
);
},
child: Text('KeyboardDismiss Demo'),
),
Spacer(),
TextField(
keyboardType: TextInputType.text,
decoration: InputDecoration(
labelText: 'Input box for keyboard test',
),
),
Container(height: 60.0),
KeyboardVisibilityBuilder(builder: (context, visible) {
return Text(
'The keyboard is: ${visible ? 'VISIBLE' : 'NOT VISIBLE'}',
);
}),
Spacer(),
],
),
),
),
),
);
}
}