diff --git a/packages/flutter/lib/cupertino.dart b/packages/flutter/lib/cupertino.dart index 684c2fe4db..2726576eb2 100644 --- a/packages/flutter/lib/cupertino.dart +++ b/packages/flutter/lib/cupertino.dart @@ -12,6 +12,7 @@ export 'src/cupertino/bottom_tab_bar.dart'; export 'src/cupertino/button.dart'; export 'src/cupertino/colors.dart'; export 'src/cupertino/dialog.dart'; +export 'src/cupertino/nav_bar.dart'; export 'src/cupertino/page.dart'; export 'src/cupertino/slider.dart'; export 'src/cupertino/switch.dart'; diff --git a/packages/flutter/lib/src/cupertino/bottom_tab_bar.dart b/packages/flutter/lib/src/cupertino/bottom_tab_bar.dart index 8a0ddded09..cd4cc55f98 100644 --- a/packages/flutter/lib/src/cupertino/bottom_tab_bar.dart +++ b/packages/flutter/lib/src/cupertino/bottom_tab_bar.dart @@ -13,7 +13,24 @@ import 'colors.dart'; const double _kTabBarHeight = 50.0; const Color _kDefaultTabBarBackgroundColor = const Color(0xCCF8F8F8); +const Color _kDefaultTabBarBorderColor = const Color(0x4C000000); +/// 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. +/// +/// 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. +/// +// TODO(xster): document using with a CupertinoScaffold. class CupertinoTabBar extends StatelessWidget { CupertinoTabBar({ Key key, @@ -72,7 +89,7 @@ class CupertinoTabBar extends StatelessWidget { decoration: new BoxDecoration( border: const Border( top: const BorderSide( - color: const Color(0x4C000000), + color: _kDefaultTabBarBorderColor, width: 0.0, // One physical pixel. style: BorderStyle.solid, ), diff --git a/packages/flutter/lib/src/cupertino/nav_bar.dart b/packages/flutter/lib/src/cupertino/nav_bar.dart new file mode 100644 index 0000000000..0ee64fe912 --- /dev/null +++ b/packages/flutter/lib/src/cupertino/nav_bar.dart @@ -0,0 +1,146 @@ +// 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/foundation.dart'; +import 'package:flutter/widgets.dart'; + +import 'colors.dart'; + +// Standard iOS 10 nav bar height without the status bar. +const double _kNavBarHeight = 44.0; + +const Color _kDefaultNavBarBackgroundColor = const Color(0xCCF8F8F8); +const Color _kDefaultNavBarBorderColor = const Color(0x4C000000); + +/// An iOS styled navigation bar. +/// +/// The navigation bar is a toolbar that minimally consists of a widget, normally +/// a page title, in the [middle] of the toolbar. +/// +/// It also supports a [leading] and [trailing] widget before and after the +/// [middle] widget while keeping the [middle] widget centered. +/// +/// It should be placed at top of the screen and automatically accounts for +/// the OS's status bar. +/// +/// 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. +/// +// TODO(xster): document automatic addition of a CupertinoBackButton. +// TODO(xster): add sample code using icons. +// TODO(xster): document integration into a CupertinoScaffold. +class CupertinoNavigationBar extends StatelessWidget implements PreferredSizeWidget { + const CupertinoNavigationBar({ + Key key, + this.leading, + @required this.middle, + this.trailing, + this.backgroundColor: _kDefaultNavBarBackgroundColor, + this.actionsForegroundColor: CupertinoColors.activeBlue, + }) : assert(middle != null, 'There must be a middle widget, usually a title'), + super(key: key); + + /// Widget to place at the start of the nav bar. Normally a back button + /// for a normal page or a cancel button for full page dialogs. + final Widget leading; + + /// Widget to place in the middle of the nav bar. Normally a title or + /// a segmented control. + final Widget middle; + + /// Widget to place at the end of the nav bar. Normally additional actions + /// taken on the page such as a search or edit function. + final Widget trailing; + + // TODO(xster): implement support for double row nav bars. + + /// The background color of the nav bar. If it contains transparency, the + /// tab bar will automatically produce a blurring effect to the content + /// behind it. + final Color backgroundColor; + + /// Default color used for text and icons of the [leading] and [trailing] + /// widgets in the nav bar. + /// + /// The [title] remains black if it's a text as per iOS standard design. + final Color actionsForegroundColor; + + @override + Size get preferredSize => const Size.fromHeight(_kNavBarHeight); + + @override + Widget build(BuildContext context) { + final bool addBlur = backgroundColor.alpha != 0xFF; + + Widget styledMiddle = middle; + if (styledMiddle.runtimeType == Text || styledMiddle.runtimeType == DefaultTextStyle) { + // Let the middle be black rather than `actionsForegroundColor` in case + // it's a plain text title. + styledMiddle = DefaultTextStyle.merge( + style: const TextStyle(color: CupertinoColors.black), + child: middle, + ); + } + + // TODO(xster): automatically build a CupertinoBackButton. + + Widget result = new DecoratedBox( + decoration: new BoxDecoration( + border: const Border( + bottom: const BorderSide( + color: _kDefaultNavBarBorderColor, + width: 0.0, // One physical pixel. + style: BorderStyle.solid, + ), + ), + color: backgroundColor, + ), + child: new SizedBox( + height: _kNavBarHeight + MediaQuery.of(context).padding.top, + child: IconTheme.merge( + data: new IconThemeData( + color: actionsForegroundColor, + size: 22.0, + ), + child: DefaultTextStyle.merge( + style: new TextStyle( + fontSize: 17.0, + letterSpacing: -0.24, + color: actionsForegroundColor, + ), + child: new Padding( + padding: new EdgeInsets.only( + top: MediaQuery.of(context).padding.top, + // TODO(xster): dynamically reduce padding when an automatic + // CupertinoBackButton is present. + left: 16.0, + right: 16.0, + ), + child: new NavigationToolbar( + leading: leading, + middle: styledMiddle, + trailing: trailing, + centerMiddle: true, + ), + ), + ), + ), + ), + ); + + if (addBlur) { + // 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; + } +} diff --git a/packages/flutter/test/cupertino/nav_bar_test.dart b/packages/flutter/test/cupertino/nav_bar_test.dart new file mode 100644 index 0000000000..d143ac59a1 --- /dev/null +++ b/packages/flutter/test/cupertino/nav_bar_test.dart @@ -0,0 +1,71 @@ +// 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 'package:flutter/cupertino.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Middle still in center with asymmetrical actions', (WidgetTester tester) async { + await tester.pumpWidget( + new WidgetsApp( + color: const Color(0xFFFFFFFF), + onGenerateRoute: (RouteSettings settings) { + return new PageRouteBuilder( + settings: settings, + pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) { + return const CupertinoNavigationBar( + leading: const CupertinoButton(child: const Text('Something'), onPressed: null,), + middle: const Text('Title'), + ); + }, + ); + }, + ), + ); + + // Expect the middle of the title to be exactly in the middle of the screen. + expect(tester.getCenter(find.text('Title')).dx, 400.0); + }); + + testWidgets('Opaque background does not add blur effects', (WidgetTester tester) async { + await tester.pumpWidget( + new WidgetsApp( + color: const Color(0xFFFFFFFF), + onGenerateRoute: (RouteSettings settings) { + return new PageRouteBuilder( + settings: settings, + pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) { + return const CupertinoNavigationBar( + middle: const Text('Title'), + backgroundColor: const Color(0xFFE5E5E5), + ); + }, + ); + }, + ), + ); + expect(find.byType(BackdropFilter), findsNothing); + }); + + testWidgets('Non-opaque background adds blur effects', (WidgetTester tester) async { + await tester.pumpWidget( + new WidgetsApp( + color: const Color(0xFFFFFFFF), + onGenerateRoute: (RouteSettings settings) { + return new PageRouteBuilder( + settings: settings, + pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) { + return const CupertinoNavigationBar( + middle: const Text('Title'), + ); + }, + ); + }, + ), + ); + expect(find.byType(BackdropFilter), findsOneWidget); + }); +} \ No newline at end of file