Search code examples
flutteranimationoverlayflutter-animation

Make an overlay widget in flutter to slide in and out from top of the screen


I created an overlay widget which can be displayed and removed after 3 seconds at the click of a button.

This is my view

  OverlayState? overlayState;
  OverlayEntry? entry;

  bool _isVisible = false;
  bool btnClicked = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'Overlay Example',
            ),
            const SizedBox(height: 12),
            ElevatedButton(
                onPressed: () {
                  setState(() {
                    _isVisible = true;
                  });
                  displayOverlay(_isVisible);
                  Future.delayed(const Duration(seconds: 3), () {
                    setState(() {
                      _isVisible = false;
                      displayOverlay(_isVisible);
                    });
                  });
                },
                child: const Text('Press Me'))
          ],
        ),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }

These are the functions that are used to control the overlay widget

 void displayOverlay(bool isClicked) {
    if (isClicked == true) {
      WidgetsBinding.instance!.addPostFrameCallback((_) => showOverlay());
    } else {
      WidgetsBinding.instance!.addPostFrameCallback((_) => hideOverlay());
    }
  }



  void hideOverlay() {
    entry?.remove();
    entry = null;
  }

This is the widget that is Overlayed on the screen

  void showOverlay() {
    entry = OverlayEntry(
      builder: (context) => AnimatedPositioned(
        right: 0,
        left: 0,
        top: _isVisible ? 0 : -MediaQuery.of(context).size.height,
        duration: const Duration(milliseconds: 500),
        child: Container(
          constraints: const BoxConstraints(maxHeight: 200),
          child: Material(
            color: const Color(0xff2D2B36).withOpacity(0.6),
            shape: ShapeBorder.lerp(
              RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
              RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
              0.5, // Interpolation value between 0.0 and 1.0
            ),
            child: Column(
              children: [],
            ),
          ),
        ),
      ),
    );

    final overlay = Overlay.of(context)!;
    overlay?.insert(entry!);
  }

I would like to know how I can make the overlay widget slide in from top of the screen then slide out after 3 seconds without using any flutter package.


Solution

  • You can handle showing and hiding animation of the banner inside its widget. Can use AnimationController to control showing/hiding the banner and animate it. FractionalTranslation can be used to specify the vertical position from top to bottom based on the height of the banner itself (its values can range from -1 to 1) and Franctionally translates based on widget's size itself.

    The idea is that we add the OverlayBanner widget to the overlay and itself will show with animation and hide.

    To remove the overlay entry when the animation is done can call a callback onBannerDismissed which is passed from the parent and the parent widget that added the entry can remove it.

    class OverlayWidget extends StatefulWidget {
      const OverlayWidget({Key? key}) : super(key: key);
    
      @override
      State<OverlayWidget> createState() => _OverlayWidgetState();
    }
    
    class _OverlayWidgetState extends State<OverlayWidget> {
      OverlayEntry? entry;
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            backgroundColor: Theme.of(context).colorScheme.inversePrimary,
            title: Text(''),
          ),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                const Text(
                  'Overlay Example',
                ),
                const SizedBox(height: 12),
                ElevatedButton(
                  onPressed: () {
                    displayOverlay();
                  },
                  child: const Text('Press Me'),
                )
              ],
            ),
          ), // This trailing comma makes auto-formatting nicer for build methods.
        );
      }
    
      void displayOverlay() {
        WidgetsBinding.instance!.addPostFrameCallback((_) => showOverlay());
      }
    
      void hideOverlay() {
        entry?.remove();
        entry = null;
      }
    
      void showOverlay() {
        entry = OverlayEntry(
          builder: (context) => OverlayBanner(
            onBannerDismissed: () {
              hideOverlay();
            },
          ),
        );
    
        final overlay = Overlay.of(context)!;
        overlay.insert(entry!);
      }
    }
    
    class OverlayBanner extends StatefulWidget {
      const OverlayBanner({Key? key, this.onBannerDismissed}) : super(key: key);
    
      final VoidCallback? onBannerDismissed;
    
      @override
      State<OverlayBanner> createState() => _OverlayBannerState();
    }
    
    class _OverlayBannerState extends State<OverlayBanner>
        with SingleTickerProviderStateMixin {
      late AnimationController _controller;
    
      static const Curve curve = Curves.easeOut;
    
      @override
      void initState() {
        super.initState();
    
        _controller = AnimationController(
          vsync: this,
          duration: const Duration(milliseconds: 600),
        );
    
        _playAnimation();
      }
    
      @override
      Widget build(BuildContext context) {
        return AnimatedBuilder(
          builder: (context, child) {
            final double animationValue = curve.transform(_controller.value);
            return FractionalTranslation(
              translation: Offset(0, -(1 - animationValue)),
              child: child,
            );
          },
          animation: _controller,
          child: SingleChildScrollView(
            child: Container(
              width: double.infinity,
              height: 200,
              color: Colors.purple,
            ),
          ),
        );
      }
    
      @override
      void dispose() {
        _controller.dispose();
        super.dispose();
      }
    
      void _playAnimation() async {
        // fist will show banner with forward.
        await _controller.forward();
        // wait for 3 second and then play reverse animation to hide the banner
        // Duration can be passed as parameter, banner will wait this much and then will dismiss
        await Future<void>.delayed(const Duration(seconds: 3));
        await _controller.reverse(from: 1);
        // call onDismissedCallback so OverlayWidget can remove and clear the OverlayEntry.
        widget.onBannerDismissed?.call();
      }
    }
    
    

    PS: No need to setState as well, can use AnimatedBuilder and pass a child to it which is the banner content, this is a more optimal way because the banner content (purple container) won't rebuild by every animation tick.

    enter image description here