bottom_tab_bar.dart 8.97 KB
Newer Older
xster's avatar
xster committed
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:ui' show ImageFilter;

import 'package:flutter/widgets.dart';

import 'colors.dart';
xster's avatar
xster committed
10
import 'theme.dart';
xster's avatar
xster committed
11 12 13 14

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

15
const Color _kDefaultTabBarBorderColor = Color(0x4C000000);
xster's avatar
xster committed
16

17
/// An iOS-styled bottom navigation tab bar.
18 19 20 21 22 23
///
/// 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
24 25
/// for the new selection to reflect. This can also be done automatically
/// by wrapping this with a [CupertinoTabScaffold].
26 27
///
/// Tab changes typically trigger a switch between [Navigator]s, each with its
xster's avatar
xster committed
28 29
/// own navigation stack, per standard iOS design. This can be done by using
/// [CupertinoTabView]s inside each tab builder in [CupertinoTabScaffold].
30 31 32
///
/// 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.
33
///
34 35 36 37 38 39 40 41
/// 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].
///
42 43
/// See also:
///
44 45
///  * [CupertinoTabScaffold], which hosts the [CupertinoTabBar] at the bottom.
///  * [BottomNavigationBarItem], an item in a [CupertinoTabBar].
46
class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget {
47
  /// Creates a tab bar in the iOS style.
48
  const CupertinoTabBar({
xster's avatar
xster committed
49 50 51
    Key key,
    @required this.items,
    this.onTap,
52
    this.currentIndex = 0,
xster's avatar
xster committed
53 54
    this.backgroundColor,
    this.activeColor,
55 56
    this.inactiveColor = CupertinoColors.inactiveGray,
    this.iconSize = 30.0,
57 58 59 60 61 62 63
    this.border = const Border(
      top: BorderSide(
        color: _kDefaultTabBarBorderColor,
        width: 0.0, // One physical pixel.
        style: BorderStyle.solid,
      ),
    ),
64
  }) : assert(items != null),
65 66 67 68
       assert(
         items.length >= 2,
         "Tabs need at least 2 items to conform to Apple's HIG",
       ),
69
       assert(currentIndex != null),
70 71
       assert(0 <= currentIndex && currentIndex < items.length),
       assert(iconSize != null),
xster's avatar
xster committed
72
       assert(inactiveColor != null),
73
       super(key: key);
xster's avatar
xster committed
74 75

  /// The interactive items laid out within the bottom navigation bar.
76 77
  ///
  /// Must not be null.
xster's avatar
xster committed
78 79 80 81 82 83 84 85 86 87
  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.
  final ValueChanged<int> onTap;

  /// The index into [items] of the current active item.
88
  ///
89 90
  /// Must not be null and must inclusively be between 0 and the number of tabs
  /// minus 1.
xster's avatar
xster committed
91 92 93 94 95
  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
96 97
  ///
  /// Defaults to [CupertinoTheme]'s `barBackgroundColor` when null.
xster's avatar
xster committed
98 99 100 101
  final Color backgroundColor;

  /// The foreground color of the icon and title for the [BottomNavigationBarItem]
  /// of the selected tab.
xster's avatar
xster committed
102 103
  ///
  /// Defaults to [CupertinoTheme]'s `primaryColor` if null.
xster's avatar
xster committed
104 105 106 107
  final Color activeColor;

  /// The foreground color of the icon and title for the [BottomNavigationBarItem]s
  /// in the unselected state.
xster's avatar
xster committed
108 109
  ///
  /// Defaults to [CupertinoColors.inactiveGray] and cannot be null.
xster's avatar
xster committed
110 111 112 113
  final Color inactiveColor;

  /// The size of all of the [BottomNavigationBarItem] icons.
  ///
114 115
  /// 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
116
  /// should configure itself to match the icon theme's size and color.
117 118
  ///
  /// Must not be null.
xster's avatar
xster committed
119 120
  final double iconSize;

121 122 123 124 125
  /// The border of the [CupertinoTabBar].
  ///
  /// The default value is a one physical pixel top border with grey color.
  final Border border;

xster's avatar
xster committed
126
  @override
127
  Size get preferredSize => const Size.fromHeight(_kTabBarHeight);
xster's avatar
xster committed
128

xster's avatar
xster committed
129 130 131 132 133 134 135 136
  /// 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;
    return backgroundColor.alpha == 0xFF;
  }

137 138
  @override
  Widget build(BuildContext context) {
139
    final double bottomPadding = MediaQuery.of(context).padding.bottom;
xster's avatar
xster committed
140

141 142
    Widget result = DecoratedBox(
      decoration: BoxDecoration(
143
        border: border,
xster's avatar
xster committed
144
        color: backgroundColor ?? CupertinoTheme.of(context).barBackgroundColor,
xster's avatar
xster committed
145
      ),
146
      child: SizedBox(
147
        height: _kTabBarHeight + bottomPadding,
xster's avatar
xster committed
148
        child: IconTheme.merge( // Default with the inactive state.
149
          data: IconThemeData(
xster's avatar
xster committed
150 151 152
            color: inactiveColor,
            size: iconSize,
          ),
153
          child: DefaultTextStyle( // Default with the inactive state.
xster's avatar
xster committed
154
            style: CupertinoTheme.of(context).textTheme.tabLabelTextStyle.copyWith(color: inactiveColor),
155 156 157
            child: Padding(
              padding: EdgeInsets.only(bottom: bottomPadding),
              child: Row(
158 159
                // Align bottom since we want the labels to be aligned.
                crossAxisAlignment: CrossAxisAlignment.end,
xster's avatar
xster committed
160
                children: _buildTabItems(context),
161
              ),
xster's avatar
xster committed
162 163 164 165 166 167
            ),
          ),
        ),
      ),
    );

xster's avatar
xster committed
168
    if (!opaque(context)) {
xster's avatar
xster committed
169
      // For non-opaque backgrounds, apply a blur effect.
170 171 172
      result = ClipRect(
        child: BackdropFilter(
          filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
xster's avatar
xster committed
173 174 175 176 177 178 179 180
          child: result,
        ),
      );
    }

    return result;
  }

xster's avatar
xster committed
181
  List<Widget> _buildTabItems(BuildContext context) {
xster's avatar
xster committed
182 183
    final List<Widget> result = <Widget>[];

184
    for (int index = 0; index < items.length; index += 1) {
185
      final bool active = index == currentIndex;
xster's avatar
xster committed
186 187
      result.add(
        _wrapActiveItem(
xster's avatar
xster committed
188
          context,
189 190
          Expanded(
            child: Semantics(
191
              selected: active,
192
              // TODO(xster): This needs localization support. https://github.com/flutter/flutter/issues/13452
193
              hint: 'tab, ${index + 1} of ${items.length}',
194
              child: GestureDetector(
195 196
                behavior: HitTestBehavior.opaque,
                onTap: onTap == null ? null : () { onTap(index); },
197
                child: Padding(
198
                  padding: const EdgeInsets.only(bottom: 4.0),
199
                  child: Column(
200
                    mainAxisAlignment: MainAxisAlignment.end,
201
                    children: _buildSingleTabItem(items[index], active),
202
                  ),
xster's avatar
xster committed
203 204 205 206
                ),
              ),
            ),
          ),
207
          active: active,
xster's avatar
xster committed
208 209 210 211 212 213 214
        ),
      );
    }

    return result;
  }

215 216 217 218
  List<Widget> _buildSingleTabItem(BottomNavigationBarItem item, bool active) {
    final List<Widget> components = <Widget>[
      Expanded(
        child: Center(child: active ? item.activeIcon : item.icon),
219
      ),
220 221 222 223 224 225 226 227 228
    ];

    if (item.title != null) {
      components.add(item.title);
    }

    return components;
  }

xster's avatar
xster committed
229
  /// Change the active tab item's icon and title colors to active.
xster's avatar
xster committed
230
  Widget _wrapActiveItem(BuildContext context, Widget item, { @required bool active }) {
xster's avatar
xster committed
231 232 233
    if (!active)
      return item;

xster's avatar
xster committed
234
    final Color activeColor = this.activeColor ?? CupertinoTheme.of(context).primaryColor;
xster's avatar
xster committed
235
    return IconTheme.merge(
236
      data: IconThemeData(color: activeColor),
xster's avatar
xster committed
237
      child: DefaultTextStyle.merge(
238
        style: TextStyle(color: activeColor),
xster's avatar
xster committed
239 240 241 242
        child: item,
      ),
    );
  }
243 244

  /// Create a clone of the current [CupertinoTabBar] but with provided
245
  /// parameters overridden.
246 247 248 249 250 251 252
  CupertinoTabBar copyWith({
    Key key,
    List<BottomNavigationBarItem> items,
    Color backgroundColor,
    Color activeColor,
    Color inactiveColor,
    Size iconSize,
253
    Border border,
254 255 256
    int currentIndex,
    ValueChanged<int> onTap,
  }) {
257
    return CupertinoTabBar(
258 259 260 261 262 263 264 265 266
      key: key ?? this.key,
      items: items ?? this.items,
      backgroundColor: backgroundColor ?? this.backgroundColor,
      activeColor: activeColor ?? this.activeColor,
      inactiveColor: inactiveColor ?? this.inactiveColor,
      iconSize: iconSize ?? this.iconSize,
      border: border ?? this.border,
      currentIndex: currentIndex ?? this.currentIndex,
      onTap: onTap ?? this.onTap,
267 268
    );
  }
xster's avatar
xster committed
269
}