// Copyright 2014 The Flutter 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'; import 'localizations.dart'; import 'theme.dart'; // Standard iOS 10 tab bar height. const double _kTabBarHeight = 50.0; const Color _kDefaultTabBarBorderColor = CupertinoDynamicColor.withBrightness( color: Color(0x4C000000), darkColor: Color(0x29000000), ); const Color _kDefaultTabBarInactiveColor = CupertinoColors.inactiveGray; /// An iOS-styled bottom navigation tab bar. /// /// 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. This can also be done automatically /// by wrapping this with a [CupertinoTabScaffold]. /// /// Tab changes typically trigger a switch between [Navigator]s, each with its /// own navigation stack, per standard iOS design. This can be done by using /// [CupertinoTabView]s inside each tab builder in [CupertinoTabScaffold]. /// /// 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. /// /// 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]. /// /// See also: /// /// * [CupertinoTabScaffold], which hosts the [CupertinoTabBar] at the bottom. /// * [BottomNavigationBarItem], an item in a [CupertinoTabBar]. class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget { /// Creates a tab bar in the iOS style. const CupertinoTabBar({ Key? key, required this.items, this.onTap, this.currentIndex = 0, this.backgroundColor, this.activeColor, this.inactiveColor = _kDefaultTabBarInactiveColor, this.iconSize = 30.0, this.border = const Border( top: BorderSide( color: _kDefaultTabBarBorderColor, width: 0.0, // One physical pixel. style: BorderStyle.solid, ), ), }) : assert(items != null), assert( items.length >= 2, "Tabs need at least 2 items to conform to Apple's HIG", ), assert(currentIndex != null), assert(0 <= currentIndex && currentIndex < items.length), assert(iconSize != null), assert(inactiveColor != null), super(key: key); /// The interactive items laid out within the bottom navigation bar. /// /// Must not be null. 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. /// /// Must not be null and must inclusively be between 0 and the number of tabs /// minus 1. 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. /// /// Defaults to [CupertinoTheme]'s `barBackgroundColor` when null. final Color? backgroundColor; /// The foreground color of the icon and title for the [BottomNavigationBarItem] /// of the selected tab. /// /// Defaults to [CupertinoTheme]'s `primaryColor` if null. final Color? activeColor; /// The foreground color of the icon and title for the [BottomNavigationBarItem]s /// in the unselected state. /// /// Defaults to a [CupertinoDynamicColor] that matches the disabled foreground /// color of the native `UITabBar` component. Cannot be null. final Color inactiveColor; /// The size of all of the [BottomNavigationBarItem] icons. /// /// This value is used 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. /// /// Must not be null. final double iconSize; /// The border of the [CupertinoTabBar]. /// /// The default value is a one physical pixel top border with grey color. final Border? border; @override Size get preferredSize => const Size.fromHeight(_kTabBarHeight); /// 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 CupertinoDynamicColor.resolve(backgroundColor, context).alpha == 0xFF; } @override Widget build(BuildContext context) { assert(debugCheckHasMediaQuery(context)); final double bottomPadding = MediaQuery.of(context).padding.bottom; final Color backgroundColor = CupertinoDynamicColor.resolve( this.backgroundColor ?? CupertinoTheme.of(context).barBackgroundColor, context, ); BorderSide resolveBorderSide(BorderSide side) { return side == BorderSide.none ? side : side.copyWith(color: CupertinoDynamicColor.resolve(side.color, context)); } // Return the border as is when it's a subclass. final Border? resolvedBorder = border == null || border.runtimeType != Border ? border : Border( top: resolveBorderSide(border!.top), left: resolveBorderSide(border!.left), bottom: resolveBorderSide(border!.bottom), right: resolveBorderSide(border!.right), ); final Color inactive = CupertinoDynamicColor.resolve(inactiveColor, context); Widget result = DecoratedBox( decoration: BoxDecoration( border: resolvedBorder, color: backgroundColor, ), child: SizedBox( height: _kTabBarHeight + bottomPadding, child: IconTheme.merge( // Default with the inactive state. data: IconThemeData(color: inactive, size: iconSize), child: DefaultTextStyle( // Default with the inactive state. style: CupertinoTheme.of(context).textTheme.tabLabelTextStyle.copyWith(color: inactive), child: Padding( padding: EdgeInsets.only(bottom: bottomPadding), child: Semantics( explicitChildNodes: true, child: Row( // Align bottom since we want the labels to be aligned. crossAxisAlignment: CrossAxisAlignment.end, children: _buildTabItems(context), ), ), ), ), ), ), ); if (!opaque(context)) { // For non-opaque backgrounds, apply a blur effect. result = ClipRect( child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0), child: result, ), ); } return result; } List<Widget> _buildTabItems(BuildContext context) { final List<Widget> result = <Widget>[]; final CupertinoLocalizations localizations = CupertinoLocalizations.of(context); for (int index = 0; index < items.length; index += 1) { final bool active = index == currentIndex; result.add( _wrapActiveItem( context, Expanded( child: Semantics( selected: active, hint: localizations.tabSemanticsLabel( tabIndex: index + 1, tabCount: items.length, ), child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: onTap == null ? null : () { onTap!(index); }, child: Padding( padding: const EdgeInsets.only(bottom: 4.0), child: Column( mainAxisAlignment: MainAxisAlignment.end, children: _buildSingleTabItem(items[index], active), ), ), ), ), ), active: active, ), ); } return result; } List<Widget> _buildSingleTabItem(BottomNavigationBarItem item, bool active) { return <Widget>[ Expanded( child: Center(child: active ? item.activeIcon : item.icon), ), if (item.title != null) item.title!, if (item.label != null) Text(item.label!), ]; } /// Change the active tab item's icon and title colors to active. Widget _wrapActiveItem(BuildContext context, Widget item, { required bool active }) { if (!active) return item; final Color activeColor = CupertinoDynamicColor.resolve( this.activeColor ?? CupertinoTheme.of(context).primaryColor, context, ); return IconTheme.merge( data: IconThemeData(color: activeColor), child: DefaultTextStyle.merge( style: TextStyle(color: activeColor), child: item, ), ); } /// Create a clone of the current [CupertinoTabBar] but with provided /// parameters overridden. CupertinoTabBar copyWith({ Key? key, List<BottomNavigationBarItem>? items, Color? backgroundColor, Color? activeColor, Color? inactiveColor, double? iconSize, Border? border, int? currentIndex, ValueChanged<int>? onTap, }) { return 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, border: border ?? this.border, currentIndex: currentIndex ?? this.currentIndex, onTap: onTap ?? this.onTap, ); } }