navigation_toolbar.dart 6.33 KB
Newer Older
1 2 3 4 5 6 7 8 9
// Copyright 2017 The Chromium Authors. All rights reserved.
// 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 'package:flutter/rendering.dart';

import 'basic.dart';
10
import 'debug.dart';
11 12 13 14 15 16
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.
///
17
/// The [leading] and [trailing] widgets occupy the edges of the widget with
18 19 20 21 22 23 24
/// 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 {
25

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

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

42 43 44 45 46 47 48 49 50 51 52 53 54 55
  /// Widget to place at the start of the horizontal toolbar.
  final Widget leading;

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

  /// Widget to place at the end of the horizontal toolbar.
  final Widget trailing;

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

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

61 62
  @override
  Widget build(BuildContext context) {
63
    assert(debugCheckHasDirectionality(context));
64
    final TextDirection textDirection = Directionality.of(context);
65 66
    return CustomMultiChildLayout(
      delegate: _ToolbarLayout(
67
        centerMiddle: centerMiddle,
68
        middleSpacing: middleSpacing,
69
        textDirection: textDirection,
70
      ),
71 72 73 74 75
      children: <Widget>[
        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),
      ],
76 77 78 79 80 81 82 83 84 85 86
    );
  }
}

enum _ToolbarSlot {
  leading,
  middle,
  trailing,
}

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

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

100 101 102
  /// The spacing around middle widget on horizontal axis.
  final double middleSpacing;

103 104
  final TextDirection textDirection;

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

    if (hasChild(_ToolbarSlot.leading)) {
111
      final BoxConstraints constraints = BoxConstraints(
112 113 114 115 116 117
        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;
118 119 120 121 122 123 124 125 126
      double leadingX;
      switch (textDirection) {
        case TextDirection.rtl:
          leadingX = size.width - leadingWidth;
          break;
        case TextDirection.ltr:
          leadingX = 0.0;
          break;
      }
127
      positionChild(_ToolbarSlot.leading, Offset(leadingX, 0.0));
128 129 130
    }

    if (hasChild(_ToolbarSlot.trailing)) {
131
      final BoxConstraints constraints = BoxConstraints.loose(size);
132
      final Size trailingSize = layoutChild(_ToolbarSlot.trailing, constraints);
133 134 135 136 137 138 139 140 141 142
      double trailingX;
      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;
143
      trailingWidth = trailingSize.width;
144
      positionChild(_ToolbarSlot.trailing, Offset(trailingX, trailingY));
145 146 147
    }

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

152
      final double middleStartMargin = leadingWidth + middleSpacing;
153
      double middleStart = middleStartMargin;
154 155 156 157
      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) {
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172
        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;
      }

      double middleX;
      switch (textDirection) {
        case TextDirection.rtl:
          middleX = size.width - middleSize.width - middleStart;
          break;
        case TextDirection.ltr:
          middleX = middleStart;
          break;
173 174
      }

175
      positionChild(_ToolbarSlot.middle, Offset(middleX, middleY));
176 177 178 179
    }
  }

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