navigation_toolbar.dart 6.32 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:math' as math;

import 'basic.dart';
8
import 'debug.dart';
9 10 11 12 13 14
import 'framework.dart';

/// [NavigationToolbar] is a layout helper to position 3 widgets or groups of
/// widgets along a horizontal axis that's sensible for an application's
/// navigation bar such as in Material Design and in iOS.
///
15
/// The [leading] and [trailing] widgets occupy the edges of the widget with
16 17 18 19 20 21 22
/// reasonable size constraints while the [middle] widget occupies the remaining
/// space in either a center aligned or start aligned fashion.
///
/// Either directly use the themed app bars such as the Material [AppBar] or
/// the iOS [CupertinoNavigationBar] or wrap this widget with more theming
/// specifications for your own custom app bar.
class NavigationToolbar extends StatelessWidget {
23

24 25
  /// Creates a widget that lays out its children in a manner suitable for a
  /// toolbar.
26
  const NavigationToolbar({
27
    Key? key,
28 29 30
    this.leading,
    this.middle,
    this.trailing,
31 32
    this.centerMiddle = true,
    this.middleSpacing = kMiddleSpacing,
33
  }) : assert(centerMiddle != null),
34
       assert(middleSpacing != null),
35 36
       super(key: key);

37 38 39
  /// The default spacing around the [middle] widget in dp.
  static const double kMiddleSpacing = 16.0;

40
  /// Widget to place at the start of the horizontal toolbar.
41
  final Widget? leading;
42 43 44

  /// Widget to place in the middle of the horizontal toolbar, occupying
  /// as much remaining space as possible.
45
  final Widget? middle;
46 47

  /// Widget to place at the end of the horizontal toolbar.
48
  final Widget? trailing;
49 50 51 52 53

  /// Whether to align the [middle] widget to the center of this widget or
  /// next to the [leading] widget when false.
  final bool centerMiddle;

54 55 56 57 58
  /// The spacing around the [middle] widget on horizontal axis.
  ///
  /// Defaults to [kMiddleSpacing].
  final double middleSpacing;

59 60
  @override
  Widget build(BuildContext context) {
61
    assert(debugCheckHasDirectionality(context));
62
    final TextDirection textDirection = Directionality.of(context);
63 64
    return CustomMultiChildLayout(
      delegate: _ToolbarLayout(
65
        centerMiddle: centerMiddle,
66
        middleSpacing: middleSpacing,
67
        textDirection: textDirection,
68
      ),
69
      children: <Widget>[
70 71 72
        if (leading != null) LayoutId(id: _ToolbarSlot.leading, child: leading!),
        if (middle != null) LayoutId(id: _ToolbarSlot.middle, child: middle!),
        if (trailing != null) LayoutId(id: _ToolbarSlot.trailing, child: trailing!),
73
      ],
74 75 76 77 78 79 80 81 82 83 84
    );
  }
}

enum _ToolbarSlot {
  leading,
  middle,
  trailing,
}

class _ToolbarLayout extends MultiChildLayoutDelegate {
85
  _ToolbarLayout({
86 87 88
    required this.centerMiddle,
    required this.middleSpacing,
    required this.textDirection,
89 90
  }) : assert(middleSpacing != null),
       assert(textDirection != null);
91

92
  // If false the middle widget should be start-justified within the space
93 94
  // between the leading and trailing widgets.
  // If true the middle widget is centered within the toolbar (not within the horizontal
95
  // space between the leading and trailing widgets).
96 97
  final bool centerMiddle;

98 99 100
  /// The spacing around middle widget on horizontal axis.
  final double middleSpacing;

101 102
  final TextDirection textDirection;

103 104 105 106 107 108
  @override
  void performLayout(Size size) {
    double leadingWidth = 0.0;
    double trailingWidth = 0.0;

    if (hasChild(_ToolbarSlot.leading)) {
109
      final BoxConstraints constraints = BoxConstraints(
110 111 112 113 114 115
        minWidth: 0.0,
        maxWidth: size.width / 3.0, // The leading widget shouldn't take up more than 1/3 of the space.
        minHeight: size.height, // The height should be exactly the height of the bar.
        maxHeight: size.height,
      );
      leadingWidth = layoutChild(_ToolbarSlot.leading, constraints).width;
116
      final double leadingX;
117 118 119 120 121 122 123 124
      switch (textDirection) {
        case TextDirection.rtl:
          leadingX = size.width - leadingWidth;
          break;
        case TextDirection.ltr:
          leadingX = 0.0;
          break;
      }
125
      positionChild(_ToolbarSlot.leading, Offset(leadingX, 0.0));
126 127 128
    }

    if (hasChild(_ToolbarSlot.trailing)) {
129
      final BoxConstraints constraints = BoxConstraints.loose(size);
130
      final Size trailingSize = layoutChild(_ToolbarSlot.trailing, constraints);
131
      final double trailingX;
132 133 134 135 136 137 138 139 140
      switch (textDirection) {
        case TextDirection.rtl:
          trailingX = 0.0;
          break;
        case TextDirection.ltr:
          trailingX = size.width - trailingSize.width;
          break;
      }
      final double trailingY = (size.height - trailingSize.height) / 2.0;
141
      trailingWidth = trailingSize.width;
142
      positionChild(_ToolbarSlot.trailing, Offset(trailingX, trailingY));
143 144 145
    }

    if (hasChild(_ToolbarSlot.middle)) {
146
      final double maxWidth = math.max(size.width - leadingWidth - trailingWidth - middleSpacing * 2.0, 0.0);
147
      final BoxConstraints constraints = BoxConstraints.loose(size).copyWith(maxWidth: maxWidth);
148 149
      final Size middleSize = layoutChild(_ToolbarSlot.middle, constraints);

150
      final double middleStartMargin = leadingWidth + middleSpacing;
151
      double middleStart = middleStartMargin;
152 153 154 155
      final double middleY = (size.height - middleSize.height) / 2.0;
      // If the centered middle will not fit between the leading and trailing
      // widgets, then align its left or right edge with the adjacent boundary.
      if (centerMiddle) {
156 157 158 159 160 161 162
        middleStart = (size.width - middleSize.width) / 2.0;
        if (middleStart + middleSize.width > size.width - trailingWidth)
          middleStart = size.width - trailingWidth - middleSize.width;
        else if (middleStart < middleStartMargin)
          middleStart = middleStartMargin;
      }

163
      final double middleX;
164 165 166 167 168 169 170
      switch (textDirection) {
        case TextDirection.rtl:
          middleX = size.width - middleSize.width - middleStart;
          break;
        case TextDirection.ltr:
          middleX = middleStart;
          break;
171 172
      }

173
      positionChild(_ToolbarSlot.middle, Offset(middleX, middleY));
174 175 176 177
    }
  }

  @override
178 179
  bool shouldRelayout(_ToolbarLayout oldDelegate) {
    return oldDelegate.centerMiddle != centerMiddle
180
        || oldDelegate.middleSpacing != middleSpacing
181 182
        || oldDelegate.textDirection != textDirection;
  }
183
}