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

import 'dart:ui' show ImageFilter;

7
import 'package:flutter/foundation.dart';
xster's avatar
xster committed
8 9 10
import 'package:flutter/widgets.dart';

import 'colors.dart';
11
import 'localizations.dart';
xster's avatar
xster committed
12
import 'theme.dart';
xster's avatar
xster committed
13 14 15 16

// Standard iOS 10 tab bar height.
const double _kTabBarHeight = 50.0;

17 18 19 20
const Color _kDefaultTabBarBorderColor = CupertinoDynamicColor.withBrightness(
  color: Color(0x4C000000),
  darkColor: Color(0x29000000),
);
21
const Color _kDefaultTabBarInactiveColor = CupertinoColors.inactiveGray;
xster's avatar
xster committed
22

23
/// An iOS-styled bottom navigation tab bar.
24 25 26 27 28 29
///
/// Displays multiple tabs using [BottomNavigationBarItem] with one tab being
/// active, the first tab by default.
///
/// This [StatelessWidget] doesn't store the active tab itself. You must
/// listen to the [onTap] callbacks and call `setState` with a new [currentIndex]
xster's avatar
xster committed
30 31
/// for the new selection to reflect. This can also be done automatically
/// by wrapping this with a [CupertinoTabScaffold].
32 33
///
/// Tab changes typically trigger a switch between [Navigator]s, each with its
xster's avatar
xster committed
34 35
/// own navigation stack, per standard iOS design. This can be done by using
/// [CupertinoTabView]s inside each tab builder in [CupertinoTabScaffold].
36 37 38
///
/// If the given [backgroundColor]'s opacity is not 1.0 (which is the case by
/// default), it will produce a blurring effect to the content behind it.
39
///
40 41 42 43 44 45 46 47
/// When used as [CupertinoTabScaffold.tabBar], by default `CupertinoTabBar` has
/// its text scale factor set to 1.0 and does not respond to text scale factor
/// changes from the operating system, to match the native iOS behavior. To override
/// this behavior, wrap each of the `navigationBar`'s components inside a [MediaQuery]
/// with the desired [MediaQueryData.textScaleFactor] value. The text scale factor
/// value from the operating system can be retrieved in many ways, such as querying
/// [MediaQuery.textScaleFactorOf] against [CupertinoApp]'s [BuildContext].
///
48
/// {@tool dartpad}
49
/// This example shows a [CupertinoTabBar] placed in a [CupertinoTabScaffold].
50 51 52 53
///
/// ** See code in examples/api/lib/cupertino/bottom_tab_bar/bottom_tab_bar.0.dart **
/// {@end-tool}
///
54 55
/// See also:
///
56 57
///  * [CupertinoTabScaffold], which hosts the [CupertinoTabBar] at the bottom.
///  * [BottomNavigationBarItem], an item in a [CupertinoTabBar].
58
class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget {
59
  /// Creates a tab bar in the iOS style.
60
  const CupertinoTabBar({
61
    super.key,
62
    required this.items,
xster's avatar
xster committed
63
    this.onTap,
64
    this.currentIndex = 0,
xster's avatar
xster committed
65 66
    this.backgroundColor,
    this.activeColor,
67
    this.inactiveColor = _kDefaultTabBarInactiveColor,
68
    this.iconSize = 30.0,
69
    this.height = _kTabBarHeight,
70 71 72
    this.border = const Border(
      top: BorderSide(
        color: _kDefaultTabBarBorderColor,
73
        width: 0.0, // 0.0 means one physical pixel
74 75
      ),
    ),
76
  }) : assert(items != null),
77 78 79 80
       assert(
         items.length >= 2,
         "Tabs need at least 2 items to conform to Apple's HIG",
       ),
81
       assert(currentIndex != null),
82 83
       assert(0 <= currentIndex && currentIndex < items.length),
       assert(iconSize != null),
84
       assert(height != null && height >= 0.0),
85
       assert(inactiveColor != null);
xster's avatar
xster committed
86 87

  /// The interactive items laid out within the bottom navigation bar.
88 89
  ///
  /// Must not be null.
xster's avatar
xster committed
90 91 92 93 94 95 96
  final List<BottomNavigationBarItem> items;

  /// The callback that is called when a item is tapped.
  ///
  /// The widget creating the bottom navigation bar needs to keep track of the
  /// current index and call `setState` to rebuild it with the newly provided
  /// index.
97
  final ValueChanged<int>? onTap;
xster's avatar
xster committed
98 99

  /// The index into [items] of the current active item.
100
  ///
101 102
  /// Must not be null and must inclusively be between 0 and the number of tabs
  /// minus 1.
xster's avatar
xster committed
103 104 105 106 107
  final int currentIndex;

  /// The background color of the tab bar. If it contains transparency, the
  /// tab bar will automatically produce a blurring effect to the content
  /// behind it.
xster's avatar
xster committed
108 109
  ///
  /// Defaults to [CupertinoTheme]'s `barBackgroundColor` when null.
110
  final Color? backgroundColor;
xster's avatar
xster committed
111 112 113

  /// The foreground color of the icon and title for the [BottomNavigationBarItem]
  /// of the selected tab.
xster's avatar
xster committed
114 115
  ///
  /// Defaults to [CupertinoTheme]'s `primaryColor` if null.
116
  final Color? activeColor;
xster's avatar
xster committed
117 118 119

  /// The foreground color of the icon and title for the [BottomNavigationBarItem]s
  /// in the unselected state.
xster's avatar
xster committed
120
  ///
121 122
  /// Defaults to a [CupertinoDynamicColor] that matches the disabled foreground
  /// color of the native `UITabBar` component. Cannot be null.
xster's avatar
xster committed
123 124 125 126
  final Color inactiveColor;

  /// The size of all of the [BottomNavigationBarItem] icons.
  ///
127 128
  /// This value is used to configure the [IconTheme] for the navigation bar.
  /// When a [BottomNavigationBarItem.icon] widget is not an [Icon] the widget
xster's avatar
xster committed
129
  /// should configure itself to match the icon theme's size and color.
130 131
  ///
  /// Must not be null.
xster's avatar
xster committed
132 133
  final double iconSize;

134 135 136 137 138
  /// The height of the [CupertinoTabBar].
  ///
  /// Defaults to 50.0. Must not be null.
  final double height;

139 140 141
  /// The border of the [CupertinoTabBar].
  ///
  /// The default value is a one physical pixel top border with grey color.
142
  final Border? border;
143

xster's avatar
xster committed
144
  @override
145
  Size get preferredSize => Size.fromHeight(height);
xster's avatar
xster committed
146

xster's avatar
xster committed
147 148 149 150 151
  /// Indicates whether the tab bar is fully opaque or can have contents behind
  /// it show through it.
  bool opaque(BuildContext context) {
    final Color backgroundColor =
        this.backgroundColor ?? CupertinoTheme.of(context).barBackgroundColor;
152
    return CupertinoDynamicColor.resolve(backgroundColor, context).alpha == 0xFF;
xster's avatar
xster committed
153 154
  }

155 156
  @override
  Widget build(BuildContext context) {
157
    assert(debugCheckHasMediaQuery(context));
158
    final double bottomPadding = MediaQuery.of(context).padding.bottom;
xster's avatar
xster committed
159

160
    final Color backgroundColor = CupertinoDynamicColor.resolve(
161 162 163 164 165 166 167 168 169 170 171
      this.backgroundColor ?? CupertinoTheme.of(context).barBackgroundColor,
      context,
    );

    BorderSide resolveBorderSide(BorderSide side) {
      return side == BorderSide.none
        ? side
        : side.copyWith(color: CupertinoDynamicColor.resolve(side.color, context));
    }

    // Return the border as is when it's a subclass.
172
    final Border? resolvedBorder = border == null || border.runtimeType != Border
173 174
      ? border
      : Border(
175 176 177 178
        top: resolveBorderSide(border!.top),
        left: resolveBorderSide(border!.left),
        bottom: resolveBorderSide(border!.bottom),
        right: resolveBorderSide(border!.right),
179 180
      );

181
    final Color inactive = CupertinoDynamicColor.resolve(inactiveColor, context);
182 183
    Widget result = DecoratedBox(
      decoration: BoxDecoration(
184 185
        border: resolvedBorder,
        color: backgroundColor,
xster's avatar
xster committed
186
      ),
187
      child: SizedBox(
188
        height: height + bottomPadding,
xster's avatar
xster committed
189
        child: IconTheme.merge( // Default with the inactive state.
190
          data: IconThemeData(color: inactive, size: iconSize),
191
          child: DefaultTextStyle( // Default with the inactive state.
192
            style: CupertinoTheme.of(context).textTheme.tabLabelTextStyle.copyWith(color: inactive),
193 194
            child: Padding(
              padding: EdgeInsets.only(bottom: bottomPadding),
195 196 197 198 199 200 201
              child: Semantics(
                explicitChildNodes: true,
                child: Row(
                  // Align bottom since we want the labels to be aligned.
                  crossAxisAlignment: CrossAxisAlignment.end,
                  children: _buildTabItems(context),
                ),
202
              ),
xster's avatar
xster committed
203 204 205 206 207 208
            ),
          ),
        ),
      ),
    );

xster's avatar
xster committed
209
    if (!opaque(context)) {
xster's avatar
xster committed
210
      // For non-opaque backgrounds, apply a blur effect.
211 212 213
      result = ClipRect(
        child: BackdropFilter(
          filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
xster's avatar
xster committed
214 215 216 217 218 219 220 221
          child: result,
        ),
      );
    }

    return result;
  }

xster's avatar
xster committed
222
  List<Widget> _buildTabItems(BuildContext context) {
xster's avatar
xster committed
223
    final List<Widget> result = <Widget>[];
224
    final CupertinoLocalizations localizations = CupertinoLocalizations.of(context);
xster's avatar
xster committed
225

226
    for (int index = 0; index < items.length; index += 1) {
227
      final bool active = index == currentIndex;
xster's avatar
xster committed
228 229
      result.add(
        _wrapActiveItem(
xster's avatar
xster committed
230
          context,
231 232
          Expanded(
            child: Semantics(
233
              selected: active,
234
              hint: localizations.tabSemanticsLabel(
235 236 237
                tabIndex: index + 1,
                tabCount: items.length,
              ),
238 239 240 241 242 243 244 245 246 247 248
              child: MouseRegion(
                cursor:  kIsWeb ? SystemMouseCursors.click : MouseCursor.defer,
                child: GestureDetector(
                  behavior: HitTestBehavior.opaque,
                  onTap: onTap == null ? null : () { onTap!(index); },
                  child: Padding(
                    padding: const EdgeInsets.only(bottom: 4.0),
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.end,
                      children: _buildSingleTabItem(items[index], active),
                    ),
249
                  ),
xster's avatar
xster committed
250 251 252 253
                ),
              ),
            ),
          ),
254
          active: active,
xster's avatar
xster committed
255 256 257 258 259 260 261
        ),
      );
    }

    return result;
  }

262
  List<Widget> _buildSingleTabItem(BottomNavigationBarItem item, bool active) {
263
    return <Widget>[
264 265
      Expanded(
        child: Center(child: active ? item.activeIcon : item.icon),
266
      ),
267
      if (item.label != null) Text(item.label!),
268 269 270
    ];
  }

xster's avatar
xster committed
271
  /// Change the active tab item's icon and title colors to active.
272
  Widget _wrapActiveItem(BuildContext context, Widget item, { required bool active }) {
xster's avatar
xster committed
273 274 275
    if (!active)
      return item;

276
    final Color activeColor = CupertinoDynamicColor.resolve(
277 278 279
      this.activeColor ?? CupertinoTheme.of(context).primaryColor,
      context,
    );
xster's avatar
xster committed
280
    return IconTheme.merge(
281
      data: IconThemeData(color: activeColor),
xster's avatar
xster committed
282
      child: DefaultTextStyle.merge(
283
        style: TextStyle(color: activeColor),
xster's avatar
xster committed
284 285 286 287
        child: item,
      ),
    );
  }
288 289

  /// Create a clone of the current [CupertinoTabBar] but with provided
290
  /// parameters overridden.
291
  CupertinoTabBar copyWith({
292 293 294 295 296 297
    Key? key,
    List<BottomNavigationBarItem>? items,
    Color? backgroundColor,
    Color? activeColor,
    Color? inactiveColor,
    double? iconSize,
298
    double? height,
299 300 301
    Border? border,
    int? currentIndex,
    ValueChanged<int>? onTap,
302
  }) {
303
    return CupertinoTabBar(
304 305 306 307 308 309
      key: key ?? this.key,
      items: items ?? this.items,
      backgroundColor: backgroundColor ?? this.backgroundColor,
      activeColor: activeColor ?? this.activeColor,
      inactiveColor: inactiveColor ?? this.inactiveColor,
      iconSize: iconSize ?? this.iconSize,
310
      height: height ?? this.height,
311 312 313
      border: border ?? this.border,
      currentIndex: currentIndex ?? this.currentIndex,
      onTap: onTap ?? this.onTap,
314 315
    );
  }
xster's avatar
xster committed
316
}