Commit b96fa4de authored by xster's avatar xster Committed by GitHub

Let CupertinoNavBar automatically have a back or close button (#12575)

* create auto back button behaviour

* cosmetic fidelity

* tests

* review

* document new icon
parent 4c1150dd
......@@ -190,6 +190,7 @@ class _CupertinoButtonState extends State<CupertinoButton> with SingleTickerProv
final Color backgroundColor = widget.color;
return new GestureDetector(
behavior: HitTestBehavior.opaque,
onTapDown: enabled ? _handleTapDown : null,
onTapUp: enabled ? _handleTapUp : null,
onTapCancel: enabled ? _handleTapCancel : null,
......
......@@ -77,4 +77,7 @@ class CupertinoIcons {
/// A checkmark in a circle.
static const IconData check_mark_circled = const IconData(0xf41f, fontFamily: iconFont, fontPackage: iconFontPackage);
}
\ No newline at end of file
/// A thicker left chevron used in iOS for the nav bar back button.
static const IconData back = const IconData(0xf3f0, fontFamily: iconFont, fontPackage: iconFontPackage);
}
......@@ -7,7 +7,9 @@ import 'dart:ui' show ImageFilter;
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'button.dart';
import 'colors.dart';
import 'icons.dart';
/// Standard iOS navigation bar height without the status bar.
const double _kNavBarPersistentHeight = 44.0;
......@@ -22,6 +24,11 @@ const double _kNavBarShowLargeTitleThreshold = 10.0;
const double _kNavBarEdgePadding = 16.0;
// The back chevron has a special padding in iOS.
const double _kNavBarBackButtonPadding = 0.0;
const double _kNavBarBackButtonTapWidth = 50.0;
/// Title text transfer fade.
const Duration _kNavBarTitleFadeDuration = const Duration(milliseconds: 150);
......@@ -44,6 +51,10 @@ const TextStyle _kLargeTitleTextStyle = const TextStyle(
/// It also supports a [leading] and [trailing] widget before and after the
/// [middle] widget while keeping the [middle] widget centered.
///
/// The [leading] widget will automatically be a back chevron icon button (or a
/// close button in case of a fullscreen dialog) to pop the current route if none
/// is provided and [automaticallyImplyLeading] is true (true by default).
///
/// It should be placed at top of the screen and automatically accounts for
/// the OS's status bar.
///
......@@ -56,25 +67,31 @@ const TextStyle _kLargeTitleTextStyle = const TextStyle(
/// [CupertinoNavigationBar].
/// * [CupertinoSliverNavigationBar] for a navigation bar to be placed in a
/// scrolling list and that supports iOS-11-style large titles.
//
// TODO(xster): document automatic addition of a CupertinoBackButton.
// TODO(xster): add sample code using icons.
class CupertinoNavigationBar extends StatelessWidget implements PreferredSizeWidget {
/// Creates a navigation bar in the iOS style.
const CupertinoNavigationBar({
Key key,
this.leading,
@required this.middle,
this.automaticallyImplyLeading: true,
this.middle,
this.trailing,
this.backgroundColor: _kDefaultNavBarBackgroundColor,
this.actionsForegroundColor: CupertinoColors.activeBlue,
}) : assert(middle != null, 'There must be a middle widget, usually a title.'),
}) : assert(automaticallyImplyLeading != null),
super(key: key);
/// Widget to place at the start of the navigation bar. Normally a back button
/// for a normal page or a cancel button for full page dialogs.
final Widget leading;
/// Controls whether we should try to imply the leading widget if null.
///
/// If true and [leading] is null, automatically try to deduce what the [leading]
/// widget should be. If [leading] widget is not null, this parameter has no effect.
///
/// This value cannot be null.
final bool automaticallyImplyLeading;
/// Widget to place in the middle of the navigation bar. Normally a title or
/// a segmented control.
final Widget middle;
......@@ -111,6 +128,7 @@ class CupertinoNavigationBar extends StatelessWidget implements PreferredSizeWid
backgroundColor: backgroundColor,
child: new _CupertinoPersistentNavigationBar(
leading: leading,
automaticallyImplyLeading: automaticallyImplyLeading,
middle: middle,
trailing: trailing,
actionsForegroundColor: actionsForegroundColor,
......@@ -137,6 +155,13 @@ class CupertinoNavigationBar extends StatelessWidget implements PreferredSizeWid
/// For advanced uses, an optional [middle] widget can be supplied to show a
/// different widget in the middle of the navigation bar when the sliver is collapsed.
///
/// Like [CupertinoNavigationBar], it also supports a [leading] and [trailing]
/// widget on the static section on top that remains while scrolling.
///
/// The [leading] widget will automatically be a back chevron icon button (or a
/// close button in case of a fullscreen dialog) to pop the current route if none
/// is provided and [automaticallyImplyLeading] is true (true by default).
///
/// See also:
///
/// * [CupertinoNavigationBar], an iOS navigation bar for use on non-scrolling
......@@ -149,11 +174,13 @@ class CupertinoSliverNavigationBar extends StatelessWidget {
Key key,
@required this.largeTitle,
this.leading,
this.automaticallyImplyLeading: true,
this.middle,
this.trailing,
this.backgroundColor: _kDefaultNavBarBackgroundColor,
this.actionsForegroundColor: CupertinoColors.activeBlue,
}) : assert(largeTitle != null),
assert(automaticallyImplyLeading != null),
super(key: key);
/// The navigation bar's title.
......@@ -178,6 +205,14 @@ class CupertinoSliverNavigationBar extends StatelessWidget {
/// This widget is visible in both collapsed and expanded states.
final Widget leading;
/// Controls whether we should try to imply the leading widget if null.
///
/// If true and [leading] is null, automatically try to deduce what the [leading]
/// widget should be. If [leading] widget is not null, this parameter has no effect.
///
/// This value cannot be null.
final bool automaticallyImplyLeading;
/// A widget to place in the middle of the static navigation bar instead of
/// the [largeTitle].
///
......@@ -215,6 +250,7 @@ class CupertinoSliverNavigationBar extends StatelessWidget {
persistentHeight: _kNavBarPersistentHeight + MediaQuery.of(context).padding.top,
title: largeTitle,
leading: leading,
automaticallyImplyLeading: automaticallyImplyLeading,
middle: middle,
trailing: trailing,
backgroundColor: backgroundColor,
......@@ -261,7 +297,8 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe
const _CupertinoPersistentNavigationBar({
Key key,
this.leading,
@required this.middle,
this.automaticallyImplyLeading,
this.middle,
this.trailing,
this.actionsForegroundColor,
this.middleVisible,
......@@ -269,6 +306,8 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe
final Widget leading;
final bool automaticallyImplyLeading;
final Widget middle;
final Widget trailing;
......@@ -320,7 +359,27 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe
child: styledMiddle,
);
// TODO(xster): automatically build a CupertinoBackButton.
// Auto add back button if leading not provided.
Widget backOrCloseButton;
bool useBackButton = false;
if (styledLeading == null && automaticallyImplyLeading) {
final ModalRoute<dynamic> currentRoute = ModalRoute.of(context);
if (currentRoute?.canPop == true) {
useBackButton = !(currentRoute is PageRoute && currentRoute?.fullscreenDialog == true);
backOrCloseButton = new CupertinoButton(
child: useBackButton
? new Container(
height: _kNavBarPersistentHeight,
width: _kNavBarBackButtonTapWidth,
alignment: Alignment.centerLeft,
child: const Icon(CupertinoIcons.back, size: 34.0,)
)
: const Text('Close'),
padding: EdgeInsets.zero,
onPressed: () { Navigator.of(context).maybePop(); },
);
}
}
return new SizedBox(
height: _kNavBarPersistentHeight + MediaQuery.of(context).padding.top,
......@@ -332,16 +391,14 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe
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: _kNavBarEdgePadding,
left: useBackButton ? _kNavBarBackButtonPadding : _kNavBarEdgePadding,
right: _kNavBarEdgePadding,
),
child: new MediaQuery.removePadding(
context: context,
removeTop: true,
child: new NavigationToolbar(
leading: styledLeading,
leading: styledLeading ?? backOrCloseButton,
middle: animatedStyledMiddle,
trailing: styledTrailing,
centerMiddle: true,
......@@ -358,6 +415,7 @@ class _CupertinoLargeTitleNavigationBarSliverDelegate extends SliverPersistentHe
@required this.persistentHeight,
@required this.title,
this.leading,
this.automaticallyImplyLeading,
this.middle,
this.trailing,
this.backgroundColor,
......@@ -370,6 +428,8 @@ class _CupertinoLargeTitleNavigationBarSliverDelegate extends SliverPersistentHe
final Widget leading;
final bool automaticallyImplyLeading;
final Widget middle;
final Widget trailing;
......@@ -391,6 +451,7 @@ class _CupertinoLargeTitleNavigationBarSliverDelegate extends SliverPersistentHe
final _CupertinoPersistentNavigationBar persistentNavigationBar =
new _CupertinoPersistentNavigationBar(
leading: leading,
automaticallyImplyLeading: automaticallyImplyLeading,
middle: middle ?? title,
trailing: trailing,
// If middle widget exists, always show it. Otherwise, show title
......
......@@ -275,6 +275,70 @@ void main() {
expect(tester.getBottomLeft(find.text('Title')).dy, 44.0 - 8.0); // Extension gone, (static part - padding) left.
});
testWidgets('Auto back/close button', (WidgetTester tester) async {
await tester.pumpWidget(
new WidgetsApp(
color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) {
return new CupertinoPageRoute<Null>(
settings: settings,
builder: (BuildContext context) {
return const CupertinoNavigationBar(
middle: const Text('Home page'),
);
},
);
},
),
);
expect(find.byType(CupertinoButton), findsNothing);
tester.state<NavigatorState>(find.byType(Navigator)).push(new CupertinoPageRoute<Null>(
builder: (BuildContext context) {
return const CupertinoNavigationBar(
middle: const Text('Page 2'),
);
},
));
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
expect(find.byType(CupertinoButton), findsOneWidget);
expect(find.byType(Icon), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).push(new CupertinoPageRoute<Null>(
fullscreenDialog: true,
builder: (BuildContext context) {
return const CupertinoNavigationBar(
middle: const Text('Dialog page'),
);
},
));
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
expect(find.byType(CupertinoButton), findsNWidgets(2));
expect(find.text('Close'), findsOneWidget);
// Test popping goes back correctly.
await tester.tap(find.text('Close'));
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
expect(find.text('Page 2'), findsOneWidget);
await tester.tap(find.byType(Icon));
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
expect(find.text('Home page'), findsOneWidget);
});
}
class _ExpectStyles extends StatelessWidget {
......
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