bottom_tab_bar.dart 8.4 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
///
/// See also:
///
36 37
///  * [CupertinoTabScaffold], which hosts the [CupertinoTabBar] at the bottom.
///  * [BottomNavigationBarItem], an item in a [CupertinoTabBar].
38
class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget {
39
  /// Creates a tab bar in the iOS style.
xster's avatar
xster committed
40 41 42 43
  CupertinoTabBar({
    Key key,
    @required this.items,
    this.onTap,
44
    this.currentIndex = 0,
xster's avatar
xster committed
45 46
    this.backgroundColor,
    this.activeColor,
47 48
    this.inactiveColor = CupertinoColors.inactiveGray,
    this.iconSize = 30.0,
49 50 51 52 53 54 55
    this.border = const Border(
      top: BorderSide(
        color: _kDefaultTabBarBorderColor,
        width: 0.0, // One physical pixel.
        style: BorderStyle.solid,
      ),
    ),
56
  }) : assert(items != null),
57 58 59 60
       assert(
         items.length >= 2,
         "Tabs need at least 2 items to conform to Apple's HIG",
       ),
61
       assert(currentIndex != null),
62 63
       assert(0 <= currentIndex && currentIndex < items.length),
       assert(iconSize != null),
xster's avatar
xster committed
64
       assert(inactiveColor != null),
65
       super(key: key);
xster's avatar
xster committed
66 67

  /// The interactive items laid out within the bottom navigation bar.
68 69
  ///
  /// Must not be null.
xster's avatar
xster committed
70 71 72 73 74 75 76 77 78 79
  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.
80
  ///
81 82
  /// Must not be null and must inclusively be between 0 and the number of tabs
  /// minus 1.
xster's avatar
xster committed
83 84 85 86 87
  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
88 89
  ///
  /// Defaults to [CupertinoTheme]'s `barBackgroundColor` when null.
xster's avatar
xster committed
90 91 92 93
  final Color backgroundColor;

  /// The foreground color of the icon and title for the [BottomNavigationBarItem]
  /// of the selected tab.
xster's avatar
xster committed
94 95
  ///
  /// Defaults to [CupertinoTheme]'s `primaryColor` if null.
xster's avatar
xster committed
96 97 98 99
  final Color activeColor;

  /// The foreground color of the icon and title for the [BottomNavigationBarItem]s
  /// in the unselected state.
xster's avatar
xster committed
100 101
  ///
  /// Defaults to [CupertinoColors.inactiveGray] and cannot be null.
xster's avatar
xster committed
102 103 104 105
  final Color inactiveColor;

  /// The size of all of the [BottomNavigationBarItem] icons.
  ///
106 107
  /// 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
108
  /// should configure itself to match the icon theme's size and color.
109 110
  ///
  /// Must not be null.
xster's avatar
xster committed
111 112
  final double iconSize;

113 114 115 116 117
  /// 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
118
  @override
119
  Size get preferredSize => const Size.fromHeight(_kTabBarHeight);
xster's avatar
xster committed
120

xster's avatar
xster committed
121 122 123 124 125 126 127 128
  /// 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;
  }

129 130
  @override
  Widget build(BuildContext context) {
131
    final double bottomPadding = MediaQuery.of(context).padding.bottom;
xster's avatar
xster committed
132

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

xster's avatar
xster committed
160
    if (!opaque(context)) {
xster's avatar
xster committed
161
      // For non-opaque backgrounds, apply a blur effect.
162 163 164
      result = ClipRect(
        child: BackdropFilter(
          filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
xster's avatar
xster committed
165 166 167 168 169 170 171 172
          child: result,
        ),
      );
    }

    return result;
  }

xster's avatar
xster committed
173
  List<Widget> _buildTabItems(BuildContext context) {
xster's avatar
xster committed
174 175
    final List<Widget> result = <Widget>[];

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

    return result;
  }

207 208 209 210
  List<Widget> _buildSingleTabItem(BottomNavigationBarItem item, bool active) {
    final List<Widget> components = <Widget>[
      Expanded(
        child: Center(child: active ? item.activeIcon : item.icon),
211
      ),
212 213 214 215 216 217 218 219 220
    ];

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

    return components;
  }

xster's avatar
xster committed
221
  /// Change the active tab item's icon and title colors to active.
xster's avatar
xster committed
222
  Widget _wrapActiveItem(BuildContext context, Widget item, { @required bool active }) {
xster's avatar
xster committed
223 224 225
    if (!active)
      return item;

xster's avatar
xster committed
226
    final Color activeColor = this.activeColor ?? CupertinoTheme.of(context).primaryColor;
xster's avatar
xster committed
227
    return IconTheme.merge(
228
      data: IconThemeData(color: activeColor),
xster's avatar
xster committed
229
      child: DefaultTextStyle.merge(
230
        style: TextStyle(color: activeColor),
xster's avatar
xster committed
231 232 233 234
        child: item,
      ),
    );
  }
235 236

  /// Create a clone of the current [CupertinoTabBar] but with provided
237
  /// parameters overridden.
238 239 240 241 242 243 244
  CupertinoTabBar copyWith({
    Key key,
    List<BottomNavigationBarItem> items,
    Color backgroundColor,
    Color activeColor,
    Color inactiveColor,
    Size iconSize,
245
    Border border,
246 247 248
    int currentIndex,
    ValueChanged<int> onTap,
  }) {
249
    return CupertinoTabBar(
250 251 252 253 254 255 256 257 258
      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,
259 260
    );
  }
xster's avatar
xster committed
261
}