bottom_tab_bar.dart 7.46 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
  }) : assert(items != null),
       assert(items.length >= 2),
49
       assert(currentIndex != null),
50 51 52
       assert(0 <= currentIndex && currentIndex < items.length),
       assert(iconSize != null),
       super(key: key);
xster's avatar
xster committed
53 54

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

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

xster's avatar
xster committed
96
  @override
97
  Size get preferredSize => const Size.fromHeight(_kTabBarHeight);
xster's avatar
xster committed
98

99 100
  @override
  Widget build(BuildContext context) {
101
    final double bottomPadding = MediaQuery.of(context).padding.bottom;
102 103
    Widget result = DecoratedBox(
      decoration: BoxDecoration(
xster's avatar
xster committed
104
        border: const Border(
105
          top: BorderSide(
106
            color: _kDefaultTabBarBorderColor,
xster's avatar
xster committed
107 108 109 110 111 112 113
            width: 0.0, // One physical pixel.
            style: BorderStyle.solid,
          ),
        ),
        color: backgroundColor,
      ),
      // TODO(xster): allow icons-only versions of the tab bar too.
114
      child: SizedBox(
115
        height: _kTabBarHeight + bottomPadding,
xster's avatar
xster committed
116
        child: IconTheme.merge( // Default with the inactive state.
117
          data: IconThemeData(
xster's avatar
xster committed
118 119 120
            color: inactiveColor,
            size: iconSize,
          ),
121 122
          child: DefaultTextStyle( // Default with the inactive state.
            style: TextStyle(
123
              fontFamily: '.SF UI Text',
xster's avatar
xster committed
124
              fontSize: 10.0,
125 126
              letterSpacing: 0.1,
              fontWeight: FontWeight.w400,
xster's avatar
xster committed
127 128
              color: inactiveColor,
            ),
129 130 131
            child: Padding(
              padding: EdgeInsets.only(bottom: bottomPadding),
              child: Row(
132 133 134 135
                // Align bottom since we want the labels to be aligned.
                crossAxisAlignment: CrossAxisAlignment.end,
                children: _buildTabItems(),
              ),
xster's avatar
xster committed
136 137 138 139 140 141
            ),
          ),
        ),
      ),
    );

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

    return result;
  }

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

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

    return result;
  }

  /// Change the active tab item's icon and title colors to active.
197
  Widget _wrapActiveItem(Widget item, { @required bool active }) {
xster's avatar
xster committed
198 199 200 201
    if (!active)
      return item;

    return IconTheme.merge(
202
      data: IconThemeData(color: activeColor),
xster's avatar
xster committed
203
      child: DefaultTextStyle.merge(
204
        style: TextStyle(color: activeColor),
xster's avatar
xster committed
205 206 207 208
        child: item,
      ),
    );
  }
209 210

  /// Create a clone of the current [CupertinoTabBar] but with provided
211
  /// parameters overridden.
212 213 214 215 216 217 218 219 220 221
  CupertinoTabBar copyWith({
    Key key,
    List<BottomNavigationBarItem> items,
    Color backgroundColor,
    Color activeColor,
    Color inactiveColor,
    Size iconSize,
    int currentIndex,
    ValueChanged<int> onTap,
  }) {
222
    return CupertinoTabBar(
223 224 225 226 227 228 229 230 231 232
       key: key ?? this.key,
       items: items ?? this.items,
       backgroundColor: backgroundColor ?? this.backgroundColor,
       activeColor: activeColor ?? this.activeColor,
       inactiveColor: inactiveColor ?? this.inactiveColor,
       iconSize: iconSize ?? this.iconSize,
       currentIndex: currentIndex ?? this.currentIndex,
       onTap: onTap ?? this.onTap,
    );
  }
xster's avatar
xster committed
233
}