I have a UI layout in my head, essentially the same as the new menu/account chooser as in the Google Maps app. It is a modal dialog that pops up on the press of the profile button and is scrollable. When scrolled, the dialog animates into a full screen dialog, and vice-versa.
I am aiming to use a Material Design compatible way of doing this, and it currently only needs to work on Android.
Some minor changes would be made, but my question is: Is that possible in Flutter? Thanks.
Author here, I have created this menu, and the code snippet is below:
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import - ANOTHER PACKAGE -
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';
import 'package:theme_provider/theme_provider.dart';
import '../../services/authManager.dart';
import '../../services/models.dart';
import '../home.dart';
class MainMenu extends StatefulWidget {
const MainMenu({
Key key,
}) : super(key: key);
@override
_MainMenuState createState() => _MainMenuState();
}
class _MainMenuState extends State<MainMenu>
with SingleTickerProviderStateMixin {
@override
Widget build(BuildContext context) {
final mainProps = Provider.of<MainProps>(context);
final authVals = Provider.of<AuthVals>(context);
final userData = Provider.of<CustomUser>(context);
final userDataPrivate = Provider.of<CustomUserPrivate>(context);
final userDataReadOnly = Provider.of<CustomUserReadOnly>(context);
return IgnorePointer(
ignoring: !mainProps.menuOpen,
child: AnimatedOpacity(
opacity: mainProps.menuOpen ? 1 : 0,
duration: Duration(milliseconds: 150),
child: Container(
color: Colors.black.withOpacity(0.75),
child: Stack(
children: [
SafeArea(
child: Container(
width: MediaQuery.of(context).size.width,
margin: EdgeInsets.only(
top: mainProps.menuPadTop + 10,
left: mainProps.menuPadLeft,
right: mainProps.menuPadRight,
),
child: Container(
width: 100.0,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(
Radius.circular(mainProps.menuCorners)),
color: Theme.of(context).backgroundColor,
),
padding: EdgeInsets.only(
top: 10,
left: 10,
right: 10,
),
child: NotificationListener<ScrollNotification>(
onNotification: (scrollNotification) {
if (scrollNotification is ScrollEndNotification) {
if (((mainProps.menuScrollCtrl.position.pixels > 100
? 100
: mainProps
.menuScrollCtrl.position.pixels) -
100)
.abs() >=
25) {
WidgetsBinding.instance.addPostFrameCallback((_) {
mainProps.menuScrollCtrl.animateTo(0,
duration: Duration(milliseconds: 150),
curve: Curves.easeInOut);
});
} else if (mainProps.menuScrollCtrl.position.pixels
.abs() >
75 &&
mainProps.menuScrollCtrl.position.pixels.abs() <
100) {
WidgetsBinding.instance.addPostFrameCallback((_) {
mainProps.menuScrollCtrl.animateTo(100,
duration: Duration(milliseconds: 150),
curve: Curves.easeInOut);
});
}
}
return true;
},
child: Container(
height: 155,
child: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Column(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
(userData.public != 'Local Account'
? CircleAvatar(
radius: 20,
backgroundImage: NetworkImage(
authVals.authUser.photoURL,
),
)
: CircleAvatar(
radius: 20,
child: SvgPicture.network(
userData.photoURL,
color: Theme.of(context)
.primaryColor ==
Color(0xffff9800)
? Colors.black
: Colors.white),
backgroundColor:
Theme.of(context).backgroundColor,
)),
Column(
children: [
Text(
userData.publicExt,
style: TextStyle(
fontWeight: FontWeight.bold),
),
Text(userDataPrivate?.realName ??
'Please Wait...'),
Text(authVals.authUser.email == ''
? 'Anonymous'
: authVals.authUser.email),
Text(userDataReadOnly != null
? userDataReadOnly.joined
.toDate()
.toLocal()
.toString()
: 'Please Wait...'),
],
),
],
),
Spacer(),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
Visibility(
visible: !mainProps.signingOut,
child: OutlineButton(
onPressed: null,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.account_circle),
SizedBox(width: 15),
Text('View Profile'),
],
),
),
),
OutlineButton(
onPressed: () async {
if (!mainProps.signingOut) {
mainProps.signingOut = true;
} else {
await AuthService().signOut();
Navigator.of(context).popAndPushNamed(
-SCREEN-);
}
},
child: AnimatedContainer(
duration: Duration(milliseconds: 250),
constraints: mainProps.signingOut
? BoxConstraints(
maxWidth: MediaQuery.of(context)
.size
.width -
82)
: BoxConstraints(maxWidth: 93),
child: Row(
mainAxisSize: !mainProps.signingOut
? MainAxisSize.min
: MainAxisSize.max,
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Icon(Icons.logout,
color: mainProps.signingOut
? Colors.red
: null),
SizedBox(width: 15),
LimitedBox(
child: Text(
'Sign Out',
style: TextStyle(
color: mainProps.signingOut
? Colors.red
: null),
),
),
],
),
),
),
],
),
Spacer(),
],
),
),
),
),
),
),
),
SafeArea(
child: Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
margin: EdgeInsets.only(
top: mainProps.menuPadTop +
(mainProps.menuScrollCtrl.hasClients
? (((mainProps.compassExpanded ? 195 : 195) / 100) *
(100 -
(mainProps.menuScrollCtrl.position.pixels >
100
? 100
: mainProps
.menuScrollCtrl.position.pixels)))
: -mainProps.menuPadTop),
left: mainProps.menuPadLeft,
right: mainProps.menuPadRight,
),
child: Container(
width: 100.0,
height: 100.0,
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(mainProps.menuCorners),
topRight: Radius.circular(mainProps.menuCorners),
),
color: Theme.of(context).backgroundColor,
),
padding: EdgeInsets.only(
top: 10,
left: 10,
right: 10,
),
child: NotificationListener<ScrollNotification>(
onNotification: (scrollNotification) {
if (scrollNotification is ScrollEndNotification) {
if (((mainProps.menuScrollCtrl.position.pixels > 100
? 100
: mainProps
.menuScrollCtrl.position.pixels) -
100)
.abs() >=
25) {
WidgetsBinding.instance.addPostFrameCallback((_) {
mainProps.menuScrollCtrl.animateTo(0,
duration: Duration(milliseconds: 150),
curve: Curves.easeInOut);
});
} else if (mainProps.menuScrollCtrl.position.pixels
.abs() >
75 &&
mainProps.menuScrollCtrl.position.pixels.abs() <
100) {
WidgetsBinding.instance.addPostFrameCallback((_) {
mainProps.menuScrollCtrl.animateTo(100,
duration: Duration(milliseconds: 150),
curve: Curves.easeInOut);
});
}
}
},
child: SingleChildScrollView(
child: const Text(
'hello\n\n\n\na\n\n\n\no\n\n\n\na\n\n\n\no\n\n\n\na\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\n'),
controller: mainProps.menuScrollCtrl,
),
),
),
),
),
SafeArea(
child: Container(
width: MediaQuery.of(context).size.width,
height: mainProps.compassExpanded ? 60 : 52,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(
Radius.circular(mainProps.menuCorners * 4),
),
),
margin: EdgeInsets.only(
top: mainProps.topMenuPadTop,
left: mainProps.menuPadLeft,
right: mainProps.menuPadRight,
),
child: Material(
borderRadius: BorderRadius.all(
Radius.circular(mainProps.menuCorners * 4),
),
elevation: 4,
color: Theme.of(context).backgroundColor,
child: Row(
children: [
Padding(
padding: const EdgeInsets.only(left: 10.0),
child: IconButton(
icon: Icon(Icons.close),
onPressed: () {
mainProps.menuOpen = false;
mainProps.signingOut = false;
mainProps.menuScrollCtrl.jumpTo(0);
},
),
),
Expanded(
child: Text(
-TEXT-,
style: GoogleFonts.ubuntu(
textStyle: TextStyle(fontSize: 17),
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
),
Padding(
padding: const EdgeInsets.only(right: 10.0),
child: Stack(
children: [
Opacity(
opacity: mainProps.menuScrollCtrl.hasClients
? ((mainProps.menuScrollCtrl.position
.pixels >
100
? 0
: 100 -
mainProps.menuScrollCtrl
.position.pixels) /
100)
: 1,
child: IgnorePointer(
ignoring: mainProps.menuScrollCtrl.hasClients
? (mainProps.menuScrollCtrl.position
.pixels >=
100
? -1
: 100 -
mainProps.menuScrollCtrl
.position.pixels) <
0
: false,
child: IconButton(
icon: Icon(Icons.palette),
onPressed: () => showDialog(
context: context,
builder: (_) => ThemeConsumer(
child: ThemeDialog(
title: Row(
children: [
Icon(Icons.palette),
SizedBox(width: 15),
Text('Choose Theme'),
],
),
hasDescription: false,
),
),
),
),
),
),
Opacity(
opacity: 1.0 -
(mainProps.menuScrollCtrl.hasClients
? ((mainProps.menuScrollCtrl.position
.pixels >
100
? 0
: 100 -
mainProps.menuScrollCtrl
.position.pixels) /
100)
: 1),
child: IgnorePointer(
ignoring:
!(mainProps.menuScrollCtrl.hasClients
? (mainProps.menuScrollCtrl.position
.pixels >=
100
? -1
: 100 -
mainProps.menuScrollCtrl
.position.pixels) <
0
: false),
child: IconButton(
icon: Icon(Icons.keyboard_arrow_down),
onPressed: () =>
mainProps.menuScrollCtrl.animateTo(
0,
duration: Duration(milliseconds: 250),
curve: Curves.easeInOut,
),
),
),
),
],
),
),
],
),
),
),
),
],
),
),
),
);
}
}
mainProps is just my state management solution, using Provider. Doing this using setState would be a nightmare and would probably bulk the code considerably. I think the rest of the code is self-explanatory. It has nice animations and some cool features.
You can see it working here: https://photos.app.goo.gl/aH6otb6CkbYbwpsr7
I'm considering creating a package with similar code to the code above, and sharing it on pub.dev. If you think that would help you, please let me know in the comments of this answer.