This figure has nothing to do with the text, just to look good

Writing in the front

These days I have been learning about Flutter. I saw a navigation bar design drawing on Dribble, which is the one below, and I liked it very much, so I thought about how to achieve this effect in Flutter.

Design by Luka š Straň ak

After some research, the results are generally achieved (some areas still need to be improved), as follows:

This article will share with you the implementation process, communication and learning together.

The key to read

This effect is achieved using AnimationController and CustomPaint, which is redrawn when switching navigation.

First, build the skeleton of the entire page:

class FloatNavigator extends StatefulWidget {
  @override
  _FloatNavigatorState createState() => _FloatNavigatorState();
}
class _FloatNavigatorState extends State<FloatNavigator>
    with SingleTickerProviderStateMixin {
    
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Stack(children: [
        Scaffold(
          appBar: AppBar(
            backgroundColor: Colors.transparent,
            elevation: 0.0,
            title: Text('Float Navigator'),
            centerTitle: true,
          ),
          backgroundColor: Color(0xFFFF0035),
        ),
        Positioned(
          bottom: 0.0,
          child: Container(
            width: width,
            child: Stack(
              overflow: Overflow.visible,
              children: <Widget>[
                // Float icon
                // All ICONS[(), () [(), () [(). }}Copy the code

Here, the navigation in the figure is divided into two parts, one is the floating icon, the other is all ICONS, when clicked, the floating icon will move to the corresponding position of all ICONS, and the circular notch on all ICONS will move together.

Next, define some variables in _FloatNavigatorState that you can use:

  int _activeIndex = 0; / / activate items
  double _height = 48.0; // Height of navigation bar
  double _floatRadius; // Hover icon radius
  double _moveTween = 0.0; // Move tween
  double _padding = 10.0; // The gap between the floating icon and the arc
  AnimationController _animationController; // Animation controller
  Animation<double> _moveAnimation; // Move the animation
  List _navs = [
    Icons.search,
    Icons.ondemand_video,
    Icons.music_video,
    Icons.insert_comment,
    Icons.person
  ]; / / navigation items
Copy the code

We then initialize some variables in initState:

  @override
  void initState() {
    _floatRadius = _height * 2 / 3;
    _animationController =
        AnimationController(vsync: this, duration: Duration(milliseconds: 400));
    super.initState();
  }
Copy the code

Here I set the radius of the hover icon to two-thirds of the height of the navigation bar and the animation duration to 400 milliseconds, although these parameters can be changed.

Next, implement the hover icon:

// Hover ICONS
Positioned(
  top: _animationController.value <= 0.5
      ? (_animationController.value * _height * _padding / 2) -
          _floatRadius / 3 * 2
      : (1 - _animationController.value) *
              _height *
              _padding /
              2 -
          _floatRadius / 3 * 2,
  left: _moveTween * singleWidth +
      (singleWidth - _floatRadius) / 2 -
      _padding / 2,
  child: DecoratedBox(
    decoration:
        ShapeDecoration(shape: CircleBorder(), shadows: [
      BoxShadow(    // Shadow effect
          blurRadius: _padding / 2,
          offset: Offset(0, _padding / 2),
          spreadRadius: 0,
          color: Colors.black26),
    ]),
    child: CircleAvatar(
        radius: _floatRadius - _padding, // Set a 10pixel gap between the floating icon and the arc
        backgroundColor: Colors.white,
        child: Icon(_navs[_activeIndex], color: Colors.black)),
  ),
)
Copy the code

_animationController generates values between 0.0 and 1.0, so if less than or equal to 0.5, it moves the icon down. Greater than 0.5, move up (the distance can be modified at will).

Left moves horizontally, using _moveTween, because the distance moved is a multiple of singleWidth (of course, the final distance moved is subtracted by the radius and gap, where the multiple is the length of the navigation item in the path from index 0 to index 3).

Below is the main part, drawing all ICONS:

CustomPaint(
  child: SizedBox(
    height: _height,
    child: Row(
      mainAxisAlignment: MainAxisAlignment.spaceAround,
      crossAxisAlignment: CrossAxisAlignment.center,
      children: _navs
          .asMap()
          .map((i, v) => MapEntry(
              i,
              GestureDetector(
                child: Icon(v,
                    color: _activeIndex == i
                        ? Colors.transparent
                        : Colors.grey),
                onTap: () {
                  _switchNav(i);
                },
              )))
          .values
          .toList(),
    ),
  ),
  painter: ArcPainter(
      navCount: _navs.length,
      moveTween: _moveTween,
      padding: _padding),
)
Copy the code

The index is used to determine which navigation is clicked each time, so we use asMap and MapEntry. ArcPainter is used to draw the background. Take a look at the implementation of drawing the background (don’t panic, the _switchNav method will be explained later) :

// Draw the arc background
class ArcPainter extends CustomPainter {
  final int navCount; // Total navigation
  final double moveTween; // Move tween
  final double padding; / / clearance
  ArcPainter({this.navCount, this.moveTween, this.padding});

  @override
  voidpaint(Canvas canvas, Size size) { Paint paint = Paint() .. color = (Colors.white) .. style = PaintingStyle.stroke;/ / brush
    double width = size.width; // The total width of the navigation bar, namely the canvas width
    double singleWidth = width / navCount; // Width of a single navigation item
    double height = size.height; // Navigation height, canvas height
    double arcRadius = height * 2 / 3; // Arc radius
    double restSpace = (singleWidth - arcRadius * 2) / 2; // Single navigation item after subtracting the arc diameter remaining width

    Path path = Path() / / path
      ..relativeLineTo(moveTween * singleWidth, 0)
      ..relativeCubicTo(restSpace + padding, 0, restSpace + padding / 2,
          arcRadius, singleWidth / 2, arcRadius) // Left half of the arc
      ..relativeCubicTo(arcRadius, 0, arcRadius - padding, -arcRadius,
          restSpace + arcRadius, -arcRadius) // Right half of the arc
      ..relativeLineTo(width - (moveTween + 1) * singleWidth, 0)
      ..relativeLineTo(0, height) .. relativeLineTo(-width,0)
      ..relativeLineTo(0, -height) .. close(); paint.style = PaintingStyle.fill; canvas.drawPath(path, paint); }@override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true; }}Copy the code

Draw the entire background of the navigation bar and fill it with white to get the shape we want with rounded notches. There are two ways to draw a Flutter (not exactly, sometimes only one). Take a relativeLineTo, for example, and its corresponding method is lineTo. The difference is that after a relativeLineTo is drawn, it uses the end point as the origin of the new coordinate system (0,0), whereas lineTo’s origin is always in the upper left corner. I’m using relative* here because I don’t have to worry about where I’m going to start the next stroke after I draw one stroke, which I like very much.

Here the most complex (to me) is the circular arc part drawing, with the help of three times bezier curve (his hand on the draft paper drew on the position of each point, can’t, that is food), it is important to note that in after drawing the circular arc left side, the origin shifted to the arc at the bottom, thus rendering the coordinates of the right half arc with the left side is on the contrary, I’ll just draw the rest of it.

Finally, implement the animation control method in _FloatNavigatorState _switchNav:

// Toggle navigation
_switchNav(int newIndex) {
    double oldPosition = _activeIndex.toDouble();
    double newPosition = newIndex.toDouble();
    if(oldPosition ! = newPosition && _animationController.status ! = AnimationStatus.forward) { _animationController.reset(); _moveAnimation = Tween(begin: oldPosition, end: newPosition).animate( CurvedAnimation( parent: _animationController, curve: Curves.easeInCubic)) .. addListener(() { setState(() { _moveTween = _moveAnimation.value; }); }).. addStatusListener((AnimationStatus status) {if(status == AnimationStatus.completed) { setState(() { _activeIndex = newIndex; }); }}); _animationController.forward(); }}Copy the code

Each time you click on the toggle navigation, reassign begin and end to _moveAnimation to determine the actual distance to move, and update the current active item when the animation completes.

One more thing, which I almost missed, destroy the animation controller:

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }
Copy the code

Now that the code is done, look at the dynamic effect:

It feels better to have fewer navigation items, and the full code can be found here

The last words

It can only be said that this effect is achieved in general, but there are still some shortcomings:

  • The path navigation icon is not hidden when the arc is moving
  • The icon in the hover icon is a new icon that is switched after the animation is finished

These deficiencies still make the final result less than perfect, but they are sufficient. We have any good ideas or suggestions can exchange, speak freely.

A practical tutorial on Flutter has been recorded for those interested