Commit 69c25424 authored by xster's avatar xster Committed by GitHub

CupertinoTabBar (#10264)

Move some Cupertino colors to a common file. 

Create a CupertinoTabBar widget to mimic iOS looks
parent f53d0fec
......@@ -8,7 +8,9 @@
library cupertino;
export 'src/cupertino/activity_indicator.dart';
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/page.dart';
export 'src/cupertino/slider.dart';
......
......@@ -6,6 +6,8 @@ import 'dart:math' as math;
import 'package:flutter/widgets.dart';
import 'colors.dart';
/// An iOS-style activity indicator.
///
/// See also:
......@@ -80,7 +82,7 @@ class _CupertinoActivityIndicatorState extends State<CupertinoActivityIndicator>
const double _kTwoPI = math.PI * 2.0;
const int _kTickCount = 12;
const int _kHalfTickCount = _kTickCount ~/ 2;
const Color _kTickColor = const Color(0xFFE0E0E0);
const Color _kTickColor = CupertinoColors.lightBackgroundGray;
const Color _kActiveTickColor = const Color(0xFF9D9D9D);
final RRect _kTickFundamentalRRect = new RRect.fromLTRBXY(-10.0, 1.0, -5.0, -1.0, 1.0, 1.0);
......
// 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 tab bar height.
const double _kTabBarHeight = 50.0;
const Color _kDefaultTabBarBackgroundColor = const Color(0xCCF8F8F8);
class CupertinoTabBar extends StatelessWidget {
CupertinoTabBar({
Key key,
@required this.items,
this.onTap,
this.currentIndex: 0,
this.backgroundColor: _kDefaultTabBarBackgroundColor,
this.activeColor: CupertinoColors.activeBlue,
this.inactiveColor: CupertinoColors.inactiveGray,
this.iconSize: 24.0,
}) : super(key: key) {
assert(items != null);
assert(items.length >= 2);
assert(0 <= currentIndex && currentIndex < items.length);
assert(iconSize != null);
}
/// The interactive items laid out within the bottom navigation bar.
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.
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.
final double iconSize;
@override
Widget build(BuildContext context) {
final bool addBlur = backgroundColor.alpha != 0xFF;
Widget result = new DecoratedBox(
decoration: new BoxDecoration(
border: const Border(
top: const BorderSide(
color: const Color(0x4C000000),
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(
height: _kTabBarHeight,
child: IconTheme.merge( // Default with the inactive state.
data: new IconThemeData(
color: inactiveColor,
size: iconSize,
),
child: DefaultTextStyle.merge( // Default with the inactive state.
style: new TextStyle(
fontSize: 10.0,
letterSpacing: 0.12,
color: inactiveColor,
),
child: new Row(
// Align bottom since we want the labels to be aligned.
crossAxisAlignment: CrossAxisAlignment.end,
children: _buildTabItems(),
),
),
),
),
);
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;
}
List<Widget> _buildTabItems() {
final List<Widget> result = <Widget>[];
for (int index = 0; index < items.length; ++index) {
result.add(
_wrapActiveItem(
new Expanded(
child: new GestureDetector(
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,
],
),
),
),
),
active: index == currentIndex,
),
);
}
return result;
}
/// Change the active tab item's icon and title colors to active.
Widget _wrapActiveItem(Widget item, { bool active }) {
if (!active)
return item;
return IconTheme.merge(
data: new IconThemeData(color: activeColor),
child: DefaultTextStyle.merge(
style: new TextStyle(color: activeColor),
child: item,
),
);
}
}
......@@ -5,9 +5,8 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
// TODO(xster): move this to a common Cupertino color palette with the next yak.
const Color _kBlue = const Color(0xFF007AFF);
const Color _kWhite = const Color(0xFFFFFFFF);
import 'colors.dart';
const Color _kDisabledBackground = const Color(0xFFA9A9A9);
const Color _kDisabledForeground = const Color(0xFFC4C4C4);
......@@ -16,7 +15,7 @@ const TextStyle _kButtonTextStyle = const TextStyle(
inherit: false,
fontSize: 15.0,
fontWeight: FontWeight.normal,
color: _kBlue,
color: CupertinoColors.activeBlue,
textBaseline: TextBaseline.alphabetic,
);
......@@ -25,7 +24,7 @@ final TextStyle _kDisabledButtonTextStyle = _kButtonTextStyle.copyWith(
);
final TextStyle _kBackgroundButtonTextStyle = _kButtonTextStyle.copyWith(
color: _kWhite,
color: CupertinoColors.white,
);
const EdgeInsets _kButtonPadding = const EdgeInsets.all(16.0);
......
// 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 Color;
class CupertinoColors {
CupertinoColors._();
/// iOS 10's default blue color. Used to indicate active elements such as
/// buttons, selected tabs and your own chat bubbles.
static const Color activeBlue = const Color(0xFF007AFF);
/// iOS 10's default green color. Used to indicate active accents such as
/// the switch in its on state and some accent buttons such as the call button
/// and Apple Map's 'Go' button.
static const Color activeGreen = const Color(0xFF4CD964);
/// Opaque white color. Used for backgrounds and fonts against dark backgrounds.
static const Color white = const Color(0xFFFFFFFF);
/// Opaque black color. Used for texts against light backgrounds.
static const Color black = const Color(0xFF000000);
/// Used in iOS 10 for light background fills such as the chat bubble background.
static const Color lightBackgroundGray = const Color(0xFFE5E5EA);
/// Used in iOS 10 for unselected selectables such as tab bar items in their
/// inactive state.
///
/// Not the same gray as disabled buttons etc.
static const Color inactiveGray = const Color(0xFF929292);
/// Used for iOS 10 for destructive actions such as the delete actions in
/// table view cells and dialogs.
///
/// Not the same red as the camera shutter or springboard icon notifications
/// or the foreground red theme in various native apps such as HealthKit.
static const Color destructiveRed = const Color(0xFFFF3B30);
}
......@@ -6,6 +6,8 @@ import 'dart:ui' show ImageFilter;
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'colors.dart';
// TODO(abarth): These constants probably belong somewhere more general.
const TextStyle _kCupertinoDialogTitleStyle = const TextStyle(
......@@ -13,7 +15,7 @@ const TextStyle _kCupertinoDialogTitleStyle = const TextStyle(
inherit: false,
fontSize: 17.5,
fontWeight: FontWeight.w600,
color: const Color(0xFF000000),
color: CupertinoColors.black,
height: 1.25,
textBaseline: TextBaseline.alphabetic,
);
......@@ -23,7 +25,7 @@ const TextStyle _kCupertinoDialogContentStyle = const TextStyle(
inherit: false,
fontSize: 12.4,
fontWeight: FontWeight.w500,
color: const Color(0xFF000000),
color: CupertinoColors.black,
height: 1.35,
textBaseline: TextBaseline.alphabetic,
);
......@@ -33,7 +35,7 @@ const TextStyle _kCupertinoDialogActionStyle = const TextStyle(
inherit: false,
fontSize: 16.8,
fontWeight: FontWeight.w400,
color: const Color(0xFF027AFF),
color: CupertinoColors.activeBlue,
textBaseline: TextBaseline.alphabetic,
);
......@@ -180,7 +182,6 @@ class CupertinoAlertDialog extends StatelessWidget {
}
}
const Color _kDestructiveActionColor = const Color(0xFFFF3B30);
/// A button typically used in a [CupertinoAlertDialog].
///
......@@ -228,7 +229,7 @@ class CupertinoDialogAction extends StatelessWidget {
style = style.copyWith(fontWeight: FontWeight.w600);
if (isDestructiveAction)
style = style.copyWith(color: _kDestructiveActionColor);
style = style.copyWith(color: CupertinoColors.destructiveRed);
if (!enabled)
style = style.copyWith(color: style.color.withOpacity(0.5));
......
......@@ -10,6 +10,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'thumb_painter.dart';
/// An iOS-style slider.
......@@ -48,7 +49,7 @@ class CupertinoSlider extends StatefulWidget {
this.min: 0.0,
this.max: 1.0,
this.divisions,
this.activeColor: const Color(0xFF027AFF),
this.activeColor: CupertinoColors.activeBlue,
}) : assert(value != null),
assert(min != null),
assert(max != null),
......
......@@ -10,6 +10,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'thumb_painter.dart';
/// An iOS-style switch.
......@@ -30,7 +31,7 @@ class CupertinoSwitch extends StatefulWidget {
Key key,
@required this.value,
@required this.onChanged,
this.activeColor: const Color(0xFF4CD964),
this.activeColor: CupertinoColors.activeGreen,
}) : super(key: key);
/// Whether this switch is on or off.
......@@ -130,7 +131,7 @@ const double _kTrackInnerLength = _kTrackInnerEnd - _kTrackInnerStart;
const double _kSwitchWidth = 59.0;
const double _kSwitchHeight = 39.0;
const Color _kTrackColor = const Color(0xFFE5E5E5);
const Color _kTrackColor = CupertinoColors.lightBackgroundGray;
const Duration _kReactionDuration = const Duration(milliseconds: 300);
const Duration _kToggleDuration = const Duration(milliseconds: 200);
......
......@@ -4,6 +4,8 @@
import 'package:flutter/painting.dart';
import 'colors.dart';
final MaskFilter _kShadowMaskFilter = new MaskFilter.blur(BlurStyle.normal, BoxShadow.convertRadiusToSigma(1.0));
/// Paints an iOS-style slider thumb.
......@@ -12,7 +14,7 @@ final MaskFilter _kShadowMaskFilter = new MaskFilter.blur(BlurStyle.normal, BoxS
class CupertinoThumbPainter {
/// Creates an object that paints an iOS-style slider thumb.
CupertinoThumbPainter({
this.color: const Color(0xFFFFFFFF),
this.color: CupertinoColors.white,
this.shadowColor: const Color(0x2C000000),
});
......
......@@ -35,44 +35,6 @@ enum BottomNavigationBarType {
shifting,
}
/// An interactive destination label within [BottomNavigationBar] with an icon
/// and title.
///
/// See also:
///
/// * [BottomNavigationBar]
/// * <https://material.google.com/components/bottom-navigation.html>
class BottomNavigationBarItem {
/// Creates an item that is used with [BottomNavigationBar.items].
///
/// The arguments [icon] and [title] should not be null.
BottomNavigationBarItem({
@required this.icon,
@required this.title,
this.backgroundColor
}) {
assert(icon != null);
assert(title != null);
}
/// The icon of the item.
///
/// Typically the icon is an [Icon] or an [ImageIcon] widget. If another type
/// of widget is provided then it should configure itself to match the current
/// [IconTheme] size and color.
final Widget icon;
/// The title of the item.
final Widget title;
/// The color of the background radial animation.
///
/// If the navigation bar's type is [BottomNavigationBarType.shifting], then
/// the entire bar is flooded with the [backgroundColor] when this item is
/// tapped.
final Color backgroundColor;
}
/// A material widget displayed at the bottom of an app for selecting among a
/// small number of views.
///
......
// 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 Color;
import 'package:flutter/foundation.dart';
import 'framework.dart';
/// An interactive button within either material's [BottomNavigationBar]
/// or the iOS themed [CupertinoTabBar] with an icon and title.
///
/// This calss is rarely used in isolation. Commonly embedded in one of the
/// bottom navigation widgets above.
///
/// See also:
///
/// * [BottomNavigationBar]
/// * <https://material.google.com/components/bottom-navigation.html>
/// * [CupertinoTabBar]
/// * <https://developer.apple.com/ios/human-interface-guidelines/ui-bars/tab-bars>
class BottomNavigationBarItem {
/// Creates an item that is used with [BottomNavigationBar.items].
///
/// The arguments [icon] and [title] should not be null.
const BottomNavigationBarItem({
@required this.icon,
@required this.title,
this.backgroundColor,
}) : assert(icon != null),
assert(title != null);
/// The icon of the item.
///
/// Typically the icon is an [Icon] or an [ImageIcon] widget. If another type
/// of widget is provided then it should configure itself to match the current
/// [IconTheme] size and color.
final Widget icon;
/// The title of the item.
final Widget title;
/// The color of the background radial animation for material [BottomNavigationBar].
///
/// If the navigation bar's type is [BottomNavigationBarType.shifting], then
/// the entire bar is flooded with the [backgroundColor] when this item is
/// tapped.
///
/// Not used for [CupertinoTabBar]. Control the invariant bar color directly
/// via [CupertinoTabBar.backgroundColor].
///
/// See also:
///
/// * [Icon.color] and [ImageIcon.color] to control the foreground color of
/// the icons themselves.
final Color backgroundColor;
}
......@@ -22,6 +22,7 @@ export 'src/widgets/async.dart';
export 'src/widgets/banner.dart';
export 'src/widgets/basic.dart';
export 'src/widgets/binding.dart';
export 'src/widgets/bottom_navigation_bar_item.dart';
export 'src/widgets/container.dart';
export 'src/widgets/debug.dart';
export 'src/widgets/dismissible.dart';
......
// 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/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import '../services/mocks_for_image_cache.dart';
void main() {
testWidgets('Need at least 2 tabs', (WidgetTester tester) async {
try {
await tester.pumpWidget(new CupertinoTabBar(
items: <BottomNavigationBarItem>[
const BottomNavigationBarItem(
icon: const ImageIcon(const TestImageProvider(24, 24)),
title: const Text('Tab 1'),
),
],
));
fail('Should not be possible to create a tab bar with just one item');
} on AssertionError {
// Exception expected.
}
});
testWidgets('Active and inactive colors', (WidgetTester tester) async {
await tester.pumpWidget(new CupertinoTabBar(
items: <BottomNavigationBarItem>[
const BottomNavigationBarItem(
icon: const ImageIcon(const TestImageProvider(24, 24)),
title: const Text('Tab 1'),
),
const BottomNavigationBarItem(
icon: const ImageIcon(const TestImageProvider(24, 24)),
title: const Text('Tab 2'),
),
],
currentIndex: 1,
activeColor: const Color(0xFF123456),
inactiveColor: const Color(0xFF654321),
));
final RichText actualInactive = tester.widget(find.descendant(
of: find.text('Tab 1'),
matching: find.byType(RichText),
));
expect(actualInactive.text.style.color, const Color(0xFF654321));
final RichText actualActive = tester.widget(find.descendant(
of: find.text('Tab 2'),
matching: find.byType(RichText),
));
expect(actualActive.text.style.color, const Color(0xFF123456));
});
testWidgets('Opaque background does not add blur effects', (WidgetTester tester) async {
await tester.pumpWidget(new CupertinoTabBar(
items: <BottomNavigationBarItem>[
const BottomNavigationBarItem(
icon: const ImageIcon(const TestImageProvider(24, 24)),
title: const Text('Tab 1'),
),
const BottomNavigationBarItem(
icon: const ImageIcon(const TestImageProvider(24, 24)),
title: const Text('Tab 2'),
),
],
));
expect(find.byType(BackdropFilter), findsOneWidget);
await tester.pumpWidget(new CupertinoTabBar(
items: <BottomNavigationBarItem>[
const BottomNavigationBarItem(
icon: const ImageIcon(const TestImageProvider(24, 24)),
title: const Text('Tab 1'),
),
const BottomNavigationBarItem(
icon: const ImageIcon(const TestImageProvider(24, 24)),
title: const Text('Tab 2'),
),
],
backgroundColor: const Color(0xFFFFFFFF), // Opaque white.
));
expect(find.byType(BackdropFilter), findsNothing);
});
testWidgets('Tap callback', (WidgetTester tester) async {
int callbackTab;
await tester.pumpWidget(new CupertinoTabBar(
items: <BottomNavigationBarItem>[
const BottomNavigationBarItem(
icon: const ImageIcon(const TestImageProvider(24, 24)),
title: const Text('Tab 1'),
),
const BottomNavigationBarItem(
icon: const ImageIcon(const TestImageProvider(24, 24)),
title: const Text('Tab 2'),
),
],
currentIndex: 1,
onTap: (int tab) { callbackTab = tab; },
));
await tester.tap(find.text('Tab 1'));
expect(callbackTab, 0);
});
}
\ No newline at end of file
......@@ -14,11 +14,11 @@ void main() {
home: new Scaffold(
bottomNavigationBar: new BottomNavigationBar(
items: <BottomNavigationBarItem>[
new BottomNavigationBarItem(
const BottomNavigationBarItem(
icon: const Icon(Icons.ac_unit),
title: const Text('AC')
),
new BottomNavigationBarItem(
const BottomNavigationBarItem(
icon: const Icon(Icons.access_alarm),
title: const Text('Alarm')
)
......@@ -42,11 +42,11 @@ void main() {
home: new Scaffold(
bottomNavigationBar: new BottomNavigationBar(
items: <BottomNavigationBarItem>[
new BottomNavigationBarItem(
const BottomNavigationBarItem(
icon: const Icon(Icons.ac_unit),
title: const Text('AC')
),
new BottomNavigationBarItem(
const BottomNavigationBarItem(
icon: const Icon(Icons.access_alarm),
title: const Text('Alarm')
)
......@@ -69,11 +69,11 @@ void main() {
bottomNavigationBar: new BottomNavigationBar(
type: BottomNavigationBarType.shifting,
items: <BottomNavigationBarItem>[
new BottomNavigationBarItem(
const BottomNavigationBarItem(
icon: const Icon(Icons.ac_unit),
title: const Text('AC')
),
new BottomNavigationBarItem(
const BottomNavigationBarItem(
icon: const Icon(Icons.access_alarm),
title: const Text('Alarm')
)
......@@ -95,11 +95,11 @@ void main() {
currentIndex: 1,
type: BottomNavigationBarType.shifting,
items: <BottomNavigationBarItem>[
new BottomNavigationBarItem(
const BottomNavigationBarItem(
icon: const Icon(Icons.ac_unit),
title: const Text('AC')
),
new BottomNavigationBarItem(
const BottomNavigationBarItem(
icon: const Icon(Icons.access_alarm),
title: const Text('Alarm')
)
......@@ -124,19 +124,19 @@ void main() {
bottomNavigationBar: new BottomNavigationBar(
type: BottomNavigationBarType.shifting,
items: <BottomNavigationBarItem>[
new BottomNavigationBarItem(
const BottomNavigationBarItem(
icon: const Icon(Icons.ac_unit),
title: const Text('AC')
),
new BottomNavigationBarItem(
const BottomNavigationBarItem(
icon: const Icon(Icons.access_alarm),
title: const Text('Alarm')
),
new BottomNavigationBarItem(
const BottomNavigationBarItem(
icon: const Icon(Icons.access_time),
title: const Text('Time')
),
new BottomNavigationBarItem(
const BottomNavigationBarItem(
icon: const Icon(Icons.add),
title: const Text('Add')
)
......@@ -185,19 +185,19 @@ void main() {
bottomNavigationBar: new BottomNavigationBar(
type: BottomNavigationBarType.shifting,
items: <BottomNavigationBarItem>[
new BottomNavigationBarItem(
const BottomNavigationBarItem(
icon: const Icon(Icons.ac_unit),
title: const Text('AC')
),
new BottomNavigationBarItem(
const BottomNavigationBarItem(
icon: const Icon(Icons.access_alarm),
title: const Text('Alarm')
),
new BottomNavigationBarItem(
const BottomNavigationBarItem(
icon: const Icon(Icons.access_time),
title: const Text('Time')
),
new BottomNavigationBarItem(
const BottomNavigationBarItem(
icon: const Icon(Icons.add),
title: const Text('Add')
)
......@@ -223,19 +223,19 @@ void main() {
bottomNavigationBar: new BottomNavigationBar(
type: BottomNavigationBarType.fixed,
items: <BottomNavigationBarItem>[
new BottomNavigationBarItem(
const BottomNavigationBarItem(
icon: const Icon(Icons.ac_unit),
title: const Text('AC')
),
new BottomNavigationBarItem(
const BottomNavigationBarItem(
icon: const Icon(Icons.access_alarm),
title: const Text('Alarm')
),
new BottomNavigationBarItem(
const BottomNavigationBarItem(
icon: const Icon(Icons.access_time),
title: const Text('Time')
),
new BottomNavigationBarItem(
const BottomNavigationBarItem(
icon: const Icon(Icons.add),
title: const Text('Add')
)
......@@ -259,7 +259,7 @@ void main() {
bottomNavigationBar: new BottomNavigationBar(
iconSize: 12.0,
items: <BottomNavigationBarItem>[
new BottomNavigationBarItem(
const BottomNavigationBarItem(
title: const Text('A'),
icon: const Icon(Icons.ac_unit),
),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment