Search code examples
iosflutterdartpopup

Custom non-blocking pop-up in Flutter with scrollable content


I'm trying to build a custom pop-up in Flutter/Dart using the following features:

  1. Non-blocking. The popup should be triggered from a scrollable view and should not block the view from being scrolled after the popup is shown.
  2. The popup should contain custom content that supports pushing and popping pages inside the popup, including scrollable views.
  3. The popup should support an arrow pointing to a specific location on the screen where it was triggered from.

Please refer to the attached images for an example of this implementation in IOS. I desire to achieve the same thing using Flutter/Dart.

The popups included in Flutter framework are rudimentary. Any ideas on how this is done?

enter image description here

enter image description here


Solution

  • I'm not a flutter pro at all but just out of curiosity I tried to implement as much as I can of your targets in my free time.

    You can show a popup without blocking the scroll. And you can push and pop the content of popup.

    I'm not claiming that it's an efficient or a healthy way to do things. It's not complete but I think you can go on and build on it. If you decide to use it I think it would be best to;

    • Edit the PopupBase showPopup method to avoid misplacement of popup as it can overflow in certain situations.
    • The arrow which will point to the clicked location can also be added after the improvement of placement.
    
    class MyScaffold extends StatefulWidget {
      const MyScaffold({Key? key}) : super(key: key);
    
      @override
      State<MyScaffold> createState() => _MyScaffoldState();
    }
    
    class _MyScaffoldState extends State<MyScaffold> {
      CustomPopupController customPopupController = CustomPopupController();
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: Text("NonBlocking PopUp")),
          body: Column(
            children: [
              Expanded(
                child: Padding(
                  padding: const EdgeInsets.all(20.0),
                  child: SingleChildScrollView(
                      scrollDirection: Axis.vertical,
                      child: PopupBase(
                        child: RichTextContent(),
                      )),
                ),
              ),
            ],
          ),
        );
      }
    }
    
    class PopupBase extends StatefulWidget {
      const PopupBase({Key? key, required this.child}) : super(key: key);
    
      final Widget child;
      @override
      State<PopupBase> createState() => _PopupBaseState();
      static _PopupBaseState of(BuildContext context) =>
          context.findAncestorStateOfType<_PopupBaseState>()!;
    }
    
    class _PopupBaseState extends State<PopupBase> {
      List<Widget> popupStack = List.empty(growable: true);
      @override
      void initState() {
        super.initState();
        popupStack.add(widget.child);
      }
    
      void showPopup(CustomPopup popup, Offset position) {
        setState(() {
          popupStack.add(Positioned(
            child: popup,
            left: position.dx,
            top: position.dy,
          ));
        });
      }
    
      void removePopup(CustomPopup popup) {
        print("popupbase remove");
        setState(() {
          popupStack.removeLast();
        });
        print(popupStack.length);
      }
    
      @override
      Widget build(BuildContext context) {
        return Stack(
          children: popupStack,
        );
      }
    }
    
    class CustomPopupController {
      late bool Function(Widget) push;
      late Widget Function()? pop;
    }
    
    class CustomPopup extends StatefulWidget {
      CustomPopup({Key? key, required this.content, required this.controller})
          : super(key: key);
      Widget content;
      final CustomPopupController controller;
      @override
      State<CustomPopup> createState() => _CustomPopupState();
      static _CustomPopupState of(BuildContext context) =>
          context.findAncestorStateOfType<_CustomPopupState>()!;
    }
    
    class _CustomPopupState extends State<CustomPopup> {
      @override
      void initState() {
        super.initState();
        widget.controller.push = push;
        widget.controller.pop = pop;
      }
    
      final List<Widget> _contentStack = List.empty(growable: true);
      bool push(Widget widgetToPush) {
        _contentStack.add(widget.content);
        setState(() {
          widget.content = widgetToPush;
        });
        return false;
      }
    
      Widget pop() {
        if (_contentStack.isNotEmpty) {
          Widget temp = widget.content;
          setState(() {
            widget.content = _contentStack.last;
            _contentStack.removeLast();
          });
          return temp;
        } else {
          PopupBase.of(context).removePopup(this.widget);
          return widget.content;
        }
      }
    
      @override
      Widget build(BuildContext context) {
        return Card(
          child: SizedBox(
              height: 300,
              width: 200,
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  Container(
                    color: Colors.red,
                    height: 25,
                    child: Row(
                      mainAxisSize: MainAxisSize.max,
                      children: [
                        (_contentStack.isNotEmpty)
                            ? Padding(
                                padding: EdgeInsets.only(left: 10),
                                child: InkWell(
                                    onTap: () {
                                      pop();
                                    },
                                    child: Icon(Icons.arrow_back_ios)),
                              )
                            : Container(),
                        Expanded(child: Container()),
                        Padding(
                          padding: EdgeInsets.only(left: 10),
                          child: InkWell(
                              onTap: () {
                                pop();
                              },
                              child: Icon(Icons.close)),
                        )
                      ],
                    ),
                  ),
                  Expanded(
                      child: SingleChildScrollView(
                    child: widget.content,
                  ))
                ],
              )),
        );
      }
    }
    
    class RichTextContent extends StatelessWidget {
      RichTextContent({Key? key}) : super(key: key);
    
      TextStyle text = TextStyle(color: Colors.black);
      TextStyle clickable = TextStyle(color: Colors.blue);
      @override
      Widget build(BuildContext context) {
        CustomPopupController controller = CustomPopupController();
        return RichText(
            text: TextSpan(children: [
          TextSpan(style: text, text: """Lorem ipsum dolor sit """),
          TextSpan(
              text: 'amet',
              style: clickable,
              recognizer: TapGestureRecognizer()
                ..onTapUp = (TapUpDetails details) {
                  PopupBase.of(context).showPopup(
                      CustomPopup(content: Text("Popup"), controller: controller),
                      details.localPosition);
                }),
          TextSpan(
              style: text,
              text:
                  """, consectetur adipiscing elit. Nulla bibendum at massa et euismod. Praesent ex ipsum, ultrices ut rhoncus et, efficitur vehicula mi. Duis sollicitudin dolor sed tristique molestie. Ut eu elit velit. Cras et lorem quis risus mattis porta vitae ac velit. Nunc laoreet malesuada lectus at laoreet. Etiam ut tristique nulla. Pellentesque eros est, pretium sit amet convallis ut, convallis eu justo. Nulla suscipit blandit massa. Vestibulum vitae magna eu urna faucibus hendrerit. Etiam eros nibh, venenatis ac ullamcorper vitae, venenatis ac neque. Aenean vitae erat massa. Aliquam vulputate facilisis volutpat. Sed tincidunt dolor a enim dictum, eget tristique nisi laoreet. Quisque suscipit, odio et mattis mattis, lectus justo lacinia enim, nec finibus mi ante gravida sem.
    
    Nunc ac nunc nec sapien porttitor volutpat. In et accumsan est. Duis non dui porta, pharetra dolor consequat, bibendum lorem. Quisque suscipit sit amet mi ac placerat. Integer cursus, est nec aliquet consequat, felis erat sodales sem, eget maximus ante libero in nibh. Cras et orci magna. Suspendisse viverra nibh eget nulla mattis laoreet. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Morbi tortor diam, pulvinar quis erat a, posuere vehicula ante. Sed aliquet et magna nec finibus. Morbi in dapibus risus. Nullam ac imperdiet enim.
    
    Nam enim mauris, volutpat et metus ut, euismod porttitor odio. Duis at orci hendrerit, posuere tortor sed, convallis nibh. Nullam lobortis est eget magna finibus porttitor. Nulla facilisi. Donec bibendum ac lorem eget consectetur. Phasellus at lacinia augue. Integer a mi quam. Morbi malesuada maximus diam. In orci nisi, mollis sed urna eget, fermentum efficitur libero. Cras auctor est sit amet ex aliquet, eu rutrum turpis gravida. Donec mattis, erat eget ultricies accumsan, odio urna egestas lectus, at mattis lorem sem scelerisque dolor. Curabitur aliquet venenatis bibendum.
    
                                """),
        ]));
      }
    }