How to hide bottom navigation bar with animation?

Viewed 952

In the following app found on SOF, while scrolling the list causing BottomAppBar to hide FloatingActionButton and CircularNotchedRectangle change size.

enter image description here

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: WorkoutDetailsPage(Workout()),
    );
  }
}

class Exercise {
  String name;

  Exercise({@required name}) {
    this.name = name;
  }
}

class Workout {
  String name = "my name";
}

class WorkoutDetailsPage extends StatefulWidget {
  Workout _workout = Workout();

  WorkoutDetailsPage(this._workout);

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

class _WorkoutDetailsPageState extends State<WorkoutDetailsPage> {
  final List<Exercise> exercises = [
    Exercise(name: "Push Ups"),
    Exercise(name: "Bench press"),
    Exercise(name: "Pull ups"),
    Exercise(name: "Press ups"),
    Exercise(name: "Crunches"),
    Exercise(name: "Sit ups"),
    Exercise(name: "BIceps curl"),
    Exercise(name: "Something else"),
    Exercise(name: "Push Ups"),
    Exercise(name: "Bench press"),
    Exercise(name: "Pull ups"),
    Exercise(name: "Press ups"),
    Exercise(name: "Crunches"),
    Exercise(name: "Sit ups"),
    Exercise(name: "BIceps curl"),
    Exercise(name: "Something else"),
    Exercise(name: "Push Ups"),
    Exercise(name: "Bench press"),
    Exercise(name: "Pull ups"),
  ];

  ScrollController _hideButtonController;

  bool _isVisible = true;

  @override
  void initState() {
    super.initState();
    _isVisible = true;
    _hideButtonController = new ScrollController();
    _hideButtonController.addListener(() {
      print("listener");
      if (_hideButtonController.position.userScrollDirection == ScrollDirection.reverse) {
        setState(() {
          _isVisible = false;
          print("**** $_isVisible up");
        });
      }
      if (_hideButtonController.position.userScrollDirection == ScrollDirection.forward) {
        setState(() {
          _isVisible = true;
          print("**** $_isVisible down");
        });
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButtonAnimator: FloatingActionButtonAnimator.scaling,
      floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
      floatingActionButton: _isVisible
          ? FloatingActionButton(
              backgroundColor: Colors.blue,
              elevation: 12,
              onPressed: () {},
            )
          : null,
      bottomNavigationBar: AnimatedContainer(
        duration: Duration(milliseconds: 200),
        height: _isVisible ? 60 : 0.0,
        child: BottomAppBar(
          elevation: 8,
          shape: CircularNotchedRectangle(),
          color: Colors.blue,
          child: Container(
            height: 60,
            child: Row(
              children: <Widget>[Text("data")],
            ),
          ),
        ),
      ),
      body: CustomScrollView(
        controller: _hideButtonController,
        slivers: <Widget>[
          SliverAppBar(
            expandedHeight: kToolbarHeight,
            pinned: true,
            floating: true,
            snap: true,
            flexibleSpace: FlexibleSpaceBar(
              title: Text(widget._workout.name),
            ),
          ),
          SliverList(
            delegate: SliverChildBuilderDelegate(
                buildSliverListItem, childCount: exercises.length
                ),
          ),
        ],
      ),
    );
  }

  Widget buildSliverListItem(BuildContext context, int index) {
    return Center(
      child: ListTile(
        title: Text(exercises[index].name),
      ),
    );
  }
}

How can I solve it? I tested putting the BottomAppBar in Stack but it didn't work.

2 Answers

If you don't want to resize the FloatingActionButton you just have to create your own FloatingActionButtonAnimator , a made some changes to your code, check this :

@override
  void initState() {
    super.initState();
    _isVisible = true;
    _hideButtonController = new ScrollController();
    _hideButtonController.addListener(() {
      //print("listener");
      if (_hideButtonController.position.userScrollDirection ==
          ScrollDirection.reverse) {
        if (_isVisible)
          setState(() {
            _isVisible = false;
            //print("**** $_isVisible up");
          });
      }
      if (_hideButtonController.position.userScrollDirection ==
          ScrollDirection.forward) {
        if (!_isVisible)
          setState(() {
            _isVisible = true;
            //print("**** $_isVisible down");
          });
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButtonAnimator: MyFabAnimation(),
      floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
      floatingActionButton: FloatingActionButton(
        backgroundColor: Colors.red,
        elevation: 12,
        onPressed: () {},
      ),
      bottomNavigationBar: AnimatedContainer(
        duration: Duration(milliseconds: 200),
        height: _isVisible ? 60 : 0.0,
        child: BottomAppBar(
          elevation: 8,
          shape: CircularNotchedRectangle(),
          color: Colors.blue,
          child: Container(
            height: 60,
            child: Row(
              children: <Widget>[Text("data")],
            ),
          ),
        ),
      ),
      body: CustomScrollView(
        controller: _hideButtonController,
        slivers: <Widget>[
          SliverAppBar(
            expandedHeight: kToolbarHeight,
            pinned: true,
            floating: true,
            snap: true,
            flexibleSpace: FlexibleSpaceBar(
              title: Text(widget._workout.name),
            ),
          ),
          SliverList(
            delegate: SliverChildBuilderDelegate(buildSliverListItem,
                childCount: exercises.length),
          ),
        ],
      ),
    );
  }

And this is the custom animation for your FloatingActionButton :

double minValue = double.maxFinite;

class MyFabAnimation extends FloatingActionButtonAnimator {
  @override
  Offset getOffset({Offset begin, Offset end, double progress}) {
    if (minValue == double.maxFinite) {
      minValue = end.dy;
    } else {
      if (begin.dy < minValue) minValue = begin.dy;
    }
    double difference = end.dy - minValue;
    return Offset(end.dx, end.dy + (difference * 1.8));
  }

  @override
  Animation<double> getRotationAnimation({Animation<double> parent}) {
    return Tween<double>(begin: 1.0, end: 1.0).animate(parent);
  }

  @override
  Animation<double> getScaleAnimation({Animation<double> parent}) {
    const Curve curve = Interval(0.5, 1.0, curve: Curves.ease);
    return _AnimationSwap<double>(
      ReverseAnimation(parent.drive(CurveTween(curve: curve.flipped))),
      parent.drive(CurveTween(curve: curve)),
      parent,
      0.5,
    );
  }
}

class _AnimationSwap<T> extends CompoundAnimation<T> {
  _AnimationSwap(
      Animation<T> first, Animation<T> next, this.parent, this.swapThreshold)
      : super(first: first, next: next);

  final Animation<double> parent;
  final double swapThreshold;

  @override
  T get value => parent.value < swapThreshold ? first.value : next.value;
}

Result

Screenshot (Null safe):

enter image description here


Code:

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  bool _visible = true;
  final Duration _duration = Duration(milliseconds: millis);

  @override
  Widget build(BuildContext context) {
    screenHeight = MediaQuery.of(context).size.height;
    return Scaffold(
      appBar: AppBar(),
      floatingActionButton: FloatingActionButton(onPressed: _animate),
      floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
      floatingActionButtonAnimator: MyAnimator(),
      bottomNavigationBar: AnimatedContainer(
        duration: _duration,
        height: _visible ? height : 0,
        child: BottomAppBar(color: Colors.red, shape: CircularNotchedRectangle()),
      ),
    );
  }

  void _animate() async {
    _visible = !_visible;
    setState(() {});
  }
}

late double screenHeight;
double value = 0, height = 100;
int millis = 5000;

class MyAnimator extends FloatingActionButtonAnimator {
  @override
  Offset getOffset({required Offset begin, required Offset end, required double progress}) {
    Offset offset = Offset(begin.dx, begin.dy);

    if (begin.dy >= (screenHeight - kToolbarHeight).round()) {
      value++;
      double max = (millis + 80) / 60;
      var other = (56 * value * height / 100) / max;
      offset = Offset(begin.dx, begin.dy + other);
    }

    return offset;
  }

  @override
  Animation<double> getRotationAnimation({required Animation<double> parent}) => parent;

  @override
  Animation<double> getScaleAnimation({required Animation<double> parent}) => parent;
}
Related