bottom_tab_bar.dart 7.4 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;
xster's avatar
xster committed
102 103 104
    Widget result = new DecoratedBox(
      decoration: new BoxDecoration(
        border: const Border(
105
          top: BorderSide(
106
            color: _kDefaultTabBarBorderColor,
xster's avatar
xster committed
107 108 109 110 111 112 113 114
            width: 0.0, // One physical pixel.
            style: BorderStyle.solid,
          ),
        ),
        color: backgroundColor,
      ),
      // TODO(xster): allow icons-only versions of the tab bar too.
      child: new SizedBox(
115
        height: _kTabBarHeight + bottomPadding,
xster's avatar
xster committed
116 117 118 119 120
        child: IconTheme.merge( // Default with the inactive state.
          data: new IconThemeData(
            color: inactiveColor,
            size: iconSize,
          ),
121
          child: new DefaultTextStyle( // Default with the inactive state.
xster's avatar
xster committed
122
            style: new 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 132 133 134 135
            child: new Padding(
              padding: new EdgeInsets.only(bottom: bottomPadding),
              child: new Row(
                // 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 144 145 146 147 148 149 150 151 152 153 154 155 156 157
      // For non-opaque backgrounds, apply a blur effect.
      result = new ClipRect(
        child: new BackdropFilter(
          filter: new ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
          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 162
      result.add(
        _wrapActiveItem(
          new Expanded(
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179
            child: new Semantics(
              selected: active,
              // TODO(https://github.com/flutter/flutter/issues/13452):
              // This needs localization support.
              hint: 'tab, ${index + 1} of ${items.length}',
              child: new GestureDetector(
                behavior: HitTestBehavior.opaque,
                onTap: onTap == null ? null : () { onTap(index); },
                child: new Padding(
                  padding: const EdgeInsets.only(bottom: 4.0),
                  child: new Column(
                    mainAxisAlignment: MainAxisAlignment.end,
                    children: <Widget> [
                      new Expanded(child: new Center(child: items[index].icon)),
                      items[index].title,
                    ],
                  ),
xster's avatar
xster committed
180 181 182 183
                ),
              ),
            ),
          ),
184
          active: active,
xster's avatar
xster committed
185 186 187 188 189 190 191 192
        ),
      );
    }

    return result;
  }

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

    return IconTheme.merge(
      data: new IconThemeData(color: activeColor),
      child: DefaultTextStyle.merge(
        style: new TextStyle(color: activeColor),
        child: item,
      ),
    );
  }
205 206

  /// Create a clone of the current [CupertinoTabBar] but with provided
207
  /// parameters overridden.
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228
  CupertinoTabBar copyWith({
    Key key,
    List<BottomNavigationBarItem> items,
    Color backgroundColor,
    Color activeColor,
    Color inactiveColor,
    Size iconSize,
    int currentIndex,
    ValueChanged<int> onTap,
  }) {
    return new CupertinoTabBar(
       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
229
}