| 1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | /// @docImport 'package:flutter/cupertino.dart'; |
| 6 | /// @docImport 'package:flutter/material.dart'; |
| 7 | library; |
| 8 | |
| 9 | import 'dart:math' as math; |
| 10 | |
| 11 | import 'basic.dart'; |
| 12 | import 'debug.dart'; |
| 13 | import 'framework.dart'; |
| 14 | |
| 15 | /// [NavigationToolbar] is a layout helper to position 3 widgets or groups of |
| 16 | /// widgets along a horizontal axis that's sensible for an application's |
| 17 | /// navigation bar such as in Material Design and in iOS. |
| 18 | /// |
| 19 | /// The [leading] and [trailing] widgets occupy the edges of the widget with |
| 20 | /// reasonable size constraints while the [middle] widget occupies the remaining |
| 21 | /// space in either a center aligned or start aligned fashion. |
| 22 | /// |
| 23 | /// Either directly use the themed app bars such as the Material [AppBar] or |
| 24 | /// the iOS [CupertinoNavigationBar] or wrap this widget with more theming |
| 25 | /// specifications for your own custom app bar. |
| 26 | class NavigationToolbar extends StatelessWidget { |
| 27 | /// Creates a widget that lays out its children in a manner suitable for a |
| 28 | /// toolbar. |
| 29 | const NavigationToolbar({ |
| 30 | super.key, |
| 31 | this.leading, |
| 32 | this.middle, |
| 33 | this.trailing, |
| 34 | this.centerMiddle = true, |
| 35 | this.middleSpacing = kMiddleSpacing, |
| 36 | }); |
| 37 | |
| 38 | /// The default spacing around the [middle] widget in dp. |
| 39 | static const double kMiddleSpacing = 16.0; |
| 40 | |
| 41 | /// Widget to place at the start of the horizontal toolbar. |
| 42 | final Widget? leading; |
| 43 | |
| 44 | /// Widget to place in the middle of the horizontal toolbar, occupying |
| 45 | /// as much remaining space as possible. |
| 46 | final Widget? middle; |
| 47 | |
| 48 | /// Widget to place at the end of the horizontal toolbar. |
| 49 | final Widget? trailing; |
| 50 | |
| 51 | /// Whether to align the [middle] widget to the center of this widget or |
| 52 | /// next to the [leading] widget when false. |
| 53 | final bool centerMiddle; |
| 54 | |
| 55 | /// The spacing around the [middle] widget on horizontal axis. |
| 56 | /// |
| 57 | /// Defaults to [kMiddleSpacing]. |
| 58 | final double middleSpacing; |
| 59 | |
| 60 | @override |
| 61 | Widget build(BuildContext context) { |
| 62 | assert(debugCheckHasDirectionality(context)); |
| 63 | final TextDirection textDirection = Directionality.of(context); |
| 64 | return CustomMultiChildLayout( |
| 65 | delegate: _ToolbarLayout( |
| 66 | centerMiddle: centerMiddle, |
| 67 | middleSpacing: middleSpacing, |
| 68 | textDirection: textDirection, |
| 69 | ), |
| 70 | children: <Widget>[ |
| 71 | if (leading != null) LayoutId(id: _ToolbarSlot.leading, child: leading!), |
| 72 | if (middle != null) LayoutId(id: _ToolbarSlot.middle, child: middle!), |
| 73 | if (trailing != null) LayoutId(id: _ToolbarSlot.trailing, child: trailing!), |
| 74 | ], |
| 75 | ); |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | enum _ToolbarSlot { leading, middle, trailing } |
| 80 | |
| 81 | class _ToolbarLayout extends MultiChildLayoutDelegate { |
| 82 | _ToolbarLayout({ |
| 83 | required this.centerMiddle, |
| 84 | required this.middleSpacing, |
| 85 | required this.textDirection, |
| 86 | }); |
| 87 | |
| 88 | // If false the middle widget should be start-justified within the space |
| 89 | // between the leading and trailing widgets. |
| 90 | // If true the middle widget is centered within the toolbar (not within the horizontal |
| 91 | // space between the leading and trailing widgets). |
| 92 | final bool centerMiddle; |
| 93 | |
| 94 | /// The spacing around middle widget on horizontal axis. |
| 95 | final double middleSpacing; |
| 96 | |
| 97 | final TextDirection textDirection; |
| 98 | |
| 99 | @override |
| 100 | void performLayout(Size size) { |
| 101 | double leadingWidth = 0.0; |
| 102 | double trailingWidth = 0.0; |
| 103 | |
| 104 | if (hasChild(_ToolbarSlot.leading)) { |
| 105 | final BoxConstraints constraints = BoxConstraints( |
| 106 | maxWidth: size.width, |
| 107 | minHeight: size.height, // The height should be exactly the height of the bar. |
| 108 | maxHeight: size.height, |
| 109 | ); |
| 110 | leadingWidth = layoutChild(_ToolbarSlot.leading, constraints).width; |
| 111 | final double leadingX = switch (textDirection) { |
| 112 | TextDirection.rtl => size.width - leadingWidth, |
| 113 | TextDirection.ltr => 0.0, |
| 114 | }; |
| 115 | positionChild(_ToolbarSlot.leading, Offset(leadingX, 0.0)); |
| 116 | } |
| 117 | |
| 118 | if (hasChild(_ToolbarSlot.trailing)) { |
| 119 | final BoxConstraints constraints = BoxConstraints.loose(size); |
| 120 | final Size trailingSize = layoutChild(_ToolbarSlot.trailing, constraints); |
| 121 | final double trailingX = switch (textDirection) { |
| 122 | TextDirection.rtl => 0.0, |
| 123 | TextDirection.ltr => size.width - trailingSize.width, |
| 124 | }; |
| 125 | final double trailingY = (size.height - trailingSize.height) / 2.0; |
| 126 | trailingWidth = trailingSize.width; |
| 127 | positionChild(_ToolbarSlot.trailing, Offset(trailingX, trailingY)); |
| 128 | } |
| 129 | |
| 130 | if (hasChild(_ToolbarSlot.middle)) { |
| 131 | final double maxWidth = math.max( |
| 132 | size.width - leadingWidth - trailingWidth - middleSpacing * 2.0, |
| 133 | 0.0, |
| 134 | ); |
| 135 | final BoxConstraints constraints = BoxConstraints.loose(size).copyWith(maxWidth: maxWidth); |
| 136 | final Size middleSize = layoutChild(_ToolbarSlot.middle, constraints); |
| 137 | |
| 138 | final double middleStartMargin = leadingWidth + middleSpacing; |
| 139 | double middleStart = middleStartMargin; |
| 140 | final double middleY = (size.height - middleSize.height) / 2.0; |
| 141 | // If the centered middle will not fit between the leading and trailing |
| 142 | // widgets, then align its left or right edge with the adjacent boundary. |
| 143 | if (centerMiddle) { |
| 144 | middleStart = (size.width - middleSize.width) / 2.0; |
| 145 | if (middleStart + middleSize.width > size.width - trailingWidth) { |
| 146 | middleStart = size.width - trailingWidth - middleSize.width - middleSpacing; |
| 147 | } else if (middleStart < middleStartMargin) { |
| 148 | middleStart = middleStartMargin; |
| 149 | } |
| 150 | } |
| 151 | |
| 152 | final double middleX = switch (textDirection) { |
| 153 | TextDirection.rtl => size.width - middleSize.width - middleStart, |
| 154 | TextDirection.ltr => middleStart, |
| 155 | }; |
| 156 | |
| 157 | positionChild(_ToolbarSlot.middle, Offset(middleX, middleY)); |
| 158 | } |
| 159 | } |
| 160 | |
| 161 | @override |
| 162 | bool shouldRelayout(_ToolbarLayout oldDelegate) { |
| 163 | return oldDelegate.centerMiddle != centerMiddle || |
| 164 | oldDelegate.middleSpacing != middleSpacing || |
| 165 | oldDelegate.textDirection != textDirection; |
| 166 | } |
| 167 | } |
| 168 | |