bottom_tab_bar.dart 7.64 KB
Newer Older
xster's avatar
xster committed
1 2 3 4 5 6 7 8 9 10 11 12 13
// 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';

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

14 15
const Color _kDefaultTabBarBackgroundColor = Color(0xCCF8F8F8);
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 24 25 26 27 28 29 30
///
/// 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]
/// for the new selection to reflect.
///
/// Tab changes typically trigger a switch between [Navigator]s, each with its
/// own navigation stack, per standard iOS design.
///
/// 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.
31 32 33
///
/// See also:
///
34 35
///  * [CupertinoTabScaffold], which hosts the [CupertinoTabBar] at the bottom.
///  * [BottomNavigationBarItem], an item in a [CupertinoTabBar].
36
class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget {
37
  /// Creates a tab bar in the iOS style.
xster's avatar
xster committed
38 39 40 41
  CupertinoTabBar({
    Key key,
    @required this.items,
    this.onTap,
42 43 44 45 46
    this.currentIndex = 0,
    this.backgroundColor = _kDefaultTabBarBackgroundColor,
    this.activeColor = CupertinoColors.activeBlue,
    this.inactiveColor = CupertinoColors.inactiveGray,
    this.iconSize = 30.0,
47 48 49 50 51 52 53
    this.border = const Border(
      top: BorderSide(
        color: _kDefaultTabBarBorderColor,
        width: 0.0, // One physical pixel.
        style: BorderStyle.solid,
      ),
    ),
54 55
  }) : assert(items != null),
       assert(items.length >= 2),
56
       assert(currentIndex != null),
57 58 59
       assert(0 <= currentIndex && currentIndex < items.length),
       assert(iconSize != null),
       super(key: key);
xster's avatar
xster committed
60 61

  /// The interactive items laid out within the bottom navigation bar.
62 63
  ///
  /// Must not be null.
xster's avatar
xster committed
64 65 66 67 68 69 70 71 72 73
  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.
74 75
  ///
  /// Must not be null.
xster's avatar
xster committed
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 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.
  final Color backgroundColor;

  /// The foreground color of the icon and title for the [BottomNavigationBarItem]
  /// of the selected tab.
  final Color activeColor;

  /// The foreground color of the icon and title for the [BottomNavigationBarItem]s
  /// in the unselected state.
  final Color inactiveColor;

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

100 101 102 103 104
  /// The border of the [CupertinoTabBar].
  ///
  /// The default value is a one physical pixel top border with grey color.
  final Border border;

105 106 107
  /// True if the tab bar's background color has no transparency.
  bool get opaque => backgroundColor.alpha == 0xFF;

xster's avatar
xster committed
108
  @override
109
  Size get preferredSize => const Size.fromHeight(_kTabBarHeight);
xster's avatar
xster committed
110

111 112
  @override
  Widget build(BuildContext context) {
113
    final double bottomPadding = MediaQuery.of(context).padding.bottom;
114 115
    Widget result = DecoratedBox(
      decoration: BoxDecoration(
116
        border: border,
xster's avatar
xster committed
117 118
        color: backgroundColor,
      ),
119
      child: SizedBox(
120
        height: _kTabBarHeight + bottomPadding,
xster's avatar
xster committed
121
        child: IconTheme.merge( // Default with the inactive state.
122
          data: IconThemeData(
xster's avatar
xster committed
123 124 125
            color: inactiveColor,
            size: iconSize,
          ),
126 127
          child: DefaultTextStyle( // Default with the inactive state.
            style: TextStyle(
128
              fontFamily: '.SF UI Text',
xster's avatar
xster committed
129
              fontSize: 10.0,
130 131
              letterSpacing: 0.1,
              fontWeight: FontWeight.w400,
xster's avatar
xster committed
132 133
              color: inactiveColor,
            ),
134 135 136
            child: Padding(
              padding: EdgeInsets.only(bottom: bottomPadding),
              child: Row(
137 138 139 140
                // Align bottom since we want the labels to be aligned.
                crossAxisAlignment: CrossAxisAlignment.end,
                children: _buildTabItems(),
              ),
xster's avatar
xster committed
141 142 143 144 145 146
            ),
          ),
        ),
      ),
    );

147
    if (!opaque) {
xster's avatar
xster committed
148
      // For non-opaque backgrounds, apply a blur effect.
149 150 151
      result = ClipRect(
        child: BackdropFilter(
          filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
xster's avatar
xster committed
152 153 154 155 156 157 158 159 160 161 162
          child: result,
        ),
      );
    }

    return result;
  }

  List<Widget> _buildTabItems() {
    final List<Widget> result = <Widget>[];

163
    for (int index = 0; index < items.length; index += 1) {
164
      final bool active = index == currentIndex;
xster's avatar
xster committed
165 166
      result.add(
        _wrapActiveItem(
167 168
          Expanded(
            child: Semantics(
169
              selected: active,
170
              // TODO(xster): This needs localization support. https://github.com/flutter/flutter/issues/13452
171
              hint: 'tab, ${index + 1} of ${items.length}',
172
              child: GestureDetector(
173 174
                behavior: HitTestBehavior.opaque,
                onTap: onTap == null ? null : () { onTap(index); },
175
                child: Padding(
176
                  padding: const EdgeInsets.only(bottom: 4.0),
177
                  child: Column(
178
                    mainAxisAlignment: MainAxisAlignment.end,
179
                    children: _buildSingleTabItem(items[index], active),
180
                  ),
xster's avatar
xster committed
181 182 183 184
                ),
              ),
            ),
          ),
185
          active: active,
xster's avatar
xster committed
186 187 188 189 190 191 192
        ),
      );
    }

    return result;
  }

193 194 195 196 197 198 199 200 201 202 203 204 205 206
  List<Widget> _buildSingleTabItem(BottomNavigationBarItem item, bool active) {
    final List<Widget> components = <Widget>[
      Expanded(
        child: Center(child: active ? item.activeIcon : item.icon),
      )
    ];

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

    return components;
  }

xster's avatar
xster committed
207
  /// Change the active tab item's icon and title colors to active.
208
  Widget _wrapActiveItem(Widget item, { @required bool active }) {
xster's avatar
xster committed
209 210 211 212
    if (!active)
      return item;

    return IconTheme.merge(
213
      data: IconThemeData(color: activeColor),
xster's avatar
xster committed
214
      child: DefaultTextStyle.merge(
215
        style: TextStyle(color: activeColor),
xster's avatar
xster committed
216 217 218 219
        child: item,
      ),
    );
  }
220 221

  /// Create a clone of the current [CupertinoTabBar] but with provided
222
  /// parameters overridden.
223 224 225 226 227 228 229
  CupertinoTabBar copyWith({
    Key key,
    List<BottomNavigationBarItem> items,
    Color backgroundColor,
    Color activeColor,
    Color inactiveColor,
    Size iconSize,
230
    Border border,
231 232 233
    int currentIndex,
    ValueChanged<int> onTap,
  }) {
234
    return CupertinoTabBar(
235 236 237 238 239 240 241 242 243
      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,
244 245
    );
  }
xster's avatar
xster committed
246
}