skip to Main Content

The following code is the custom Bottom Nav Bar that I have created, and I want it to hide when the user scrolls down and pop back up only when the user scrolls back up.

Quite similar to the behaviour of the twitter/X bottom nav bar.


class CustomBottomNavbar extends StatefulWidget {
  final int currentIndex;
  final void Function(int index) onTap;

  const CustomBottomNavbar({
    Key? key,
    required this.currentIndex,
    required this.onTap,
  }) : super(key: key);

  @override
  _CustomBottomNavbarState createState() => _CustomBottomNavbarState();
}

class _CustomBottomNavbarState extends State<CustomBottomNavbar> {
  bool _isVisible = false;
  int _tappedIndex = -1;

  @override
  void initState() {
    super.initState();

    Future.delayed(const Duration(milliseconds: 600), () {
      setState(() {
        _isVisible = true;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedPositioned(
      duration: const Duration(milliseconds: 800),
      curve: Curves.easeInOut,
      bottom: _isVisible ? 0 : -800,
      left: 0,
      right: 0,
      child: Padding(
        padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 48.0),
        child: SlideTransition(
          position: Tween<Offset>(
            begin: const Offset(0, 1),
            end: const Offset(0, 0),
          ).animate(
            CurvedAnimation(
              parent: ModalRoute.of(context)!.animation!,
              curve: Curves.easeOut,
            ),
          ),
          child: Container(
            padding: const EdgeInsets.symmetric(horizontal: 8.0),
            width: double.infinity,
            decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(50),
              color: Colors.grey[200]!.withOpacity(0.95),
            ),
            height: 60.0,
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceAround,
              children: [
                buildNavItem(Icons.home, 0),
                buildNavItem(Icons.explore, 1),
                buildNavItem(Icons.notifications, 2),
                buildNavItem(Icons.account_circle, 3),
              ],
            ),
          ),
        ),
      ),
    );
  }

  Widget buildNavItem(IconData icon, int index) {
    final isSelected = index == widget.currentIndex;
    double iconSize = _tappedIndex == index ? 40.0 : 32.0;

    Color iconColor = isSelected
        ? Colors.grey[900]!
        : (_tappedIndex == index ? Colors.black : Colors.grey[500]!);

    return GestureDetector(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          SizedBox(
            width: 60.0,
            height: 60.0,
            child: AnimatedSwitcher(
              duration: const Duration(milliseconds: 1200),
              child: Icon(
                icon,
                key: ValueKey<IconData>(icon),
                color: iconColor,
                size: iconSize,
              ),
            ),
          ),
        ],
      ),
      onTapDown: (_) {
        setState(() {
          _tappedIndex = index;
        });
      },
      onTapUp: (_) {
        setState(() {
          _tappedIndex = -1;
        });
        widget.onTap(index);
      },
      onTapCancel: () {
        setState(() {
          _tappedIndex = -1;
        });
      },
    );
  }
}

Currently what this code does is:

  1. Initially shows an animation of the bottom nav bar popping up when the user opens the app.
  2. Hides when the user scrolls either up or down.
  3. After the user scrolls, the bottom nav bar stays hidden for about 3 seconds and pops back up.

What I want to achieve is, that the bottom nav bar stays hidden until the user scrolls up.
And shows up ONLY when the user scrolls up (with an animation).

Hope this isn’t too much to ask, but I have been stuck on this problem for quite a few weeks and can’t find a good solution.

Any help would be appreciated!

2

Answers


  1. Try this

    import 'package:flutter/material.dart';
    
    class CustomBottomNavbar extends StatefulWidget {
      final int currentIndex;
      final void Function(int index) onTap;
    
      const CustomBottomNavbar({
        Key? key,
        required this.currentIndex,
        required this.onTap,
      }) : super(key: key);
    
      @override
      _CustomBottomNavbarState createState() => _CustomBottomNavbarState();
    }
    
    class _CustomBottomNavbarState extends State<CustomBottomNavbar> {
      bool _isVisible = true;
      int _tappedIndex = -1;
      ScrollController _scrollController = ScrollController();
      double _scrollThreshold = 10.0;
    
      @override
      void initState() {
        super.initState();
    
        _scrollController.addListener(() {
          if (_scrollController.position.userScrollDirection == ScrollDirection.forward) {
            // User is scrolling up
            if (!_isVisible) {
              setState(() {
                _isVisible = true;
              });
            }
          } else if (_scrollController.position.userScrollDirection == ScrollDirection.reverse) {
            // User is scrolling down
            if (_isVisible) {
              setState(() {
                _isVisible = false;
              });
            }
          }
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return AnimatedPositioned(
          duration: const Duration(milliseconds: 800),
          curve: Curves.easeInOut,
          bottom: _isVisible ? 0 : -800,
          left: 0,
          right: 0,
          child: Padding(
            padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 48.0),
            child: SlideTransition(
              position: Tween<Offset>(
                begin: const Offset(0, 1),
                end: const Offset(0, 0),
              ).animate(
                CurvedAnimation(
                  parent: ModalRoute.of(context)!.animation!,
                  curve: Curves.easeOut,
                ),
              ),
              child: Container(
                padding: const EdgeInsets.symmetric(horizontal: 8.0),
                width: double.infinity,
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(50),
                  color: Colors.grey[200]!.withOpacity(0.95),
                ),
                height: 60.0,
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.spaceAround,
                  children: [
                    buildNavItem(Icons.home, 0),
                    buildNavItem(Icons.explore, 1),
                    buildNavItem(Icons.notifications, 2),
                    buildNavItem(Icons.account_circle, 3),
                  ],
                ),
              ),
            ),
          ),
        );
      }
    
      Widget buildNavItem(IconData icon, int index) {
        final isSelected = index == widget.currentIndex;
        double iconSize = _tappedIndex == index ? 40.0 : 32.0;
    
        Color iconColor = isSelected
            ? Colors.grey[900]!
            : (_tappedIndex == index ? Colors.black : Colors.grey[500]!);
    
        return GestureDetector(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              SizedBox(
                width: 60.0,
                height: 60.0,
                child: AnimatedSwitcher(
                  duration: const Duration(milliseconds: 1200),
                  child: Icon(
                    icon,
                    key: ValueKey<IconData>(icon),
                    color: iconColor,
                    size: iconSize,
                  ),
                ),
              ),
            ],
          ),
          onTapDown: (_) {
            setState(() {
              _tappedIndex = index;
            });
          },
          onTapUp: (_) {
            setState(() {
              _tappedIndex = -1;
            });
            widget.onTap(index);
          },
          onTapCancel: () {
            setState(() {
              _tappedIndex = -1;
            });
          },
        );
      }
    }
    

    The _scrollController is used to detect the user’s scroll direction, and _isVisible is updated accordingly, adjust the _scrollThreshold variable to control how much the user needs to scroll before the bottom navbar reacts.

    Login or Signup to reply.
  2. Try this creative way.

    • Create your own bottomNavBar as a Widget without any transition animation.
    • Use Scaffold in that page you want place bottomNavBar. In body use any type of scrolling widget that you want and assign a scrollController to that.
    • place your custom navBar in Scaffold as floatingActionButton field.
    • wrap your customBottomNavBar with Visibility widget. and define a isVisible variable that you can change it in scrollController.listener().
    • you can add slide animation to that.
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search