Search code examples
androidflutteruser-interfacemodal-dialogmaterial-design

Flutter: Convert Simple Dialog To Full Screen Dialog On Scroll


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.


Solution

  • 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.