// Copyright 2018 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/rendering.dart';
import 'package:flutter/widgets.dart';
import 'button.dart';
import 'colors.dart';
import 'icons.dart';
import 'tab_view.dart';
// Based on specs from https://developer.apple.com/design/resources/ for
// iOS 12.
const TextStyle _kDefaultTextStyle = TextStyle(
fontFamily: '.SF Pro Text',
fontSize: 17.0,
letterSpacing: -0.38,
color: CupertinoColors.black,
decoration: TextDecoration.none,
);
/// An application that uses Cupertino design.
///
/// A convenience widget that wraps a number of widgets that are commonly
/// required for an iOS-design targeting application. It builds upon a
/// [WidgetsApp] by iOS specific defaulting such as fonts and scrolling
/// physics.
///
/// The [CupertinoApp] configures the top-level [Navigator] to search for routes
/// in the following order:
///
/// 1. For the `/` route, the [home] property, if non-null, is used.
///
/// 2. Otherwise, the [routes] table is used, if it has an entry for the route.
///
/// 3. Otherwise, [onGenerateRoute] is called, if provided. It should return a
/// non-null value for any _valid_ route not handled by [home] and [routes].
///
/// 4. Finally if all else fails [onUnknownRoute] is called.
///
/// If [home], [routes], [onGenerateRoute], and [onUnknownRoute] are all null,
/// and [builder] is not null, then no [Navigator] is created.
///
/// This widget also configures the observer of the top-level [Navigator] (if
/// any) to perform [Hero] animations.
///
/// Use this widget with caution on Android since it may produce behaviors
/// Android users are not expecting such as:
///
/// * Pages will be dismissible via a back swipe.
/// * Scrolling past extremities will trigger iOS-style spring overscrolls.
/// * The San Francisco font family is unavailable on Android and can result
/// in undefined font behavior.
///
/// See also:
///
/// * [CupertinoPageScaffold], which provides a standard page layout default
/// with nav bars.
/// * [Navigator], which is used to manage the app's stack of pages.
/// * [CupertinoPageRoute], which defines an app page that transitions in an
/// iOS-specific way.
/// * [WidgetsApp], which defines the basic app elements but does not depend
/// on the Cupertino library.
class CupertinoApp extends StatefulWidget {
/// Creates a CupertinoApp.
///
/// At least one of [home], [routes], [onGenerateRoute], or [builder] must be
/// non-null. If only [routes] is given, it must include an entry for the
/// [Navigator.defaultRouteName] (`/`), since that is the route used when the
/// application is launched with an intent that specifies an otherwise
/// unsupported route.
///
/// This class creates an instance of [WidgetsApp].
///
/// The boolean arguments, [routes], and [navigatorObservers], must not be null.
CupertinoApp({ // can't be const because the asserts use methods on Map :-(
Key key,
this.home,
this.routes = const <String, WidgetBuilder>{},
this.initialRoute,
this.onGenerateRoute,
this.onUnknownRoute,
this.navigatorObservers = const <NavigatorObserver>[],
this.builder,
this.title = '',
this.onGenerateTitle,
this.color,
this.locale,
this.localizationsDelegates,
this.localeResolutionCallback,
this.supportedLocales = const <Locale>[Locale('en', 'US')],
this.showPerformanceOverlay = false,
this.checkerboardRasterCacheImages = false,
this.checkerboardOffscreenLayers = false,
this.showSemanticsDebugger = false,
this.debugShowCheckedModeBanner = true,
}) : assert(routes != null),
assert(navigatorObservers != null),
assert(
home == null ||
!routes.containsKey(Navigator.defaultRouteName),
'If the home property is specified, the routes table '
'cannot include an entry for "/", since it would be redundant.'
),
assert(
builder != null ||
home != null ||
routes.containsKey(Navigator.defaultRouteName) ||
onGenerateRoute != null ||
onUnknownRoute != null,
'Either the home property must be specified, '
'or the routes table must include an entry for "/", '
'or there must be on onGenerateRoute callback specified, '
'or there must be an onUnknownRoute callback specified, '
'or the builder property must be specified, '
'because otherwise there is nothing to fall back on if the '
'app is started with an intent that specifies an unknown route.'
),
assert(
(home != null ||
routes.isNotEmpty ||
onGenerateRoute != null ||
onUnknownRoute != null)
||
(builder != null &&
initialRoute == null &&
navigatorObservers.isEmpty),
'If no route is provided using '
'home, routes, onGenerateRoute, or onUnknownRoute, '
'a non-null callback for the builder property must be provided, '
'and the other navigator-related properties, '
'navigatorKey, initialRoute, and navigatorObservers, '
'must have their initial values '
'(null, null, and the empty list, respectively).'
),
assert(title != null),
assert(showPerformanceOverlay != null),
assert(checkerboardRasterCacheImages != null),
assert(checkerboardOffscreenLayers != null),
assert(showSemanticsDebugger != null),
assert(debugShowCheckedModeBanner != null),
super(key: key);
/// The widget for the default route of the app ([Navigator.defaultRouteName],
/// which is `/`).
///
/// This is the route that is displayed first when the application is started
/// normally, unless [initialRoute] is specified. It's also the route that's
/// displayed if the [initialRoute] can't be displayed.
///
/// To be able to directly call [MediaQuery.of], [Navigator.of], etc, in the
/// code that sets the [home] argument in the constructor, you can use a
/// [Builder] widget to get a [BuildContext].
///
/// If [home] is specified, then [routes] must not include an entry for `/`,
/// as [home] takes its place.
///
/// The [Navigator] is only built if routes are provided (either via [home],
/// [routes], [onGenerateRoute], or [onUnknownRoute]); if they are not,
/// [builder] must not be null.
///
/// The difference between using [home] and using [builder] is that the [home]
/// subtree is inserted into the application below a [Navigator] (and thus
/// below an [Overlay], which [Navigator] uses). With [home], therefore,
/// dialog boxes will work automatically, the [routes] table will be used, and
/// APIs such as [Navigator.push] and [Navigator.pop] will work as expected.
/// In contrast, the widget returned from [builder] is inserted _above_ the
/// [CupertinoApp]'s [Navigator] (if any).
final Widget home;
/// The application's top-level routing table.
///
/// When a named route is pushed with [Navigator.pushNamed], the route name is
/// looked up in this map. If the name is present, the associated
/// [WidgetBuilder] is used to construct a [CupertinoPageRoute] that performs
/// an appropriate transition, including [Hero] animations, to the new route.
///
/// If the app only has one page, then you can specify it using [home] instead.
///
/// If [home] is specified, then it implies an entry in this table for the
/// [Navigator.defaultRouteName] route (`/`), and it is an error to
/// redundantly provide such a route in the [routes] table.
///
/// If a route is requested that is not specified in this table (or by
/// [home]), then the [onGenerateRoute] callback is called to build the page
/// instead.
///
/// The [Navigator] is only built if routes are provided (either via [home],
/// [routes], [onGenerateRoute], or [onUnknownRoute]); if they are not,
/// [builder] must not be null.
final Map<String, WidgetBuilder> routes;
/// {@macro flutter.widgets.widgetsApp.initialRoute}
///
/// The [Navigator] is only built if routes are provided (either via [home],
/// [routes], [onGenerateRoute], or [onUnknownRoute]); if they are not,
/// [initialRoute] must be null and [builder] must not be null.
///
/// See also:
///
/// * [Navigator.initialRoute], which is used to implement this property.
/// * [Navigator.push], for pushing additional routes.
/// * [Navigator.pop], for removing a route from the stack.
final String initialRoute;
/// {@macro flutter.widgets.widgetsApp.onGenerateRoute}
///
/// This is used if [routes] does not contain the requested route.
///
/// The [Navigator] is only built if routes are provided (either via [home],
/// [routes], [onGenerateRoute], or [onUnknownRoute]); if they are not,
/// [builder] must not be null.
final RouteFactory onGenerateRoute;
/// Called when [onGenerateRoute] fails to generate a route, except for the
/// [initialRoute].
///
/// {@macro flutter.widgets.widgetsApp.onUnknownRoute}
///
/// The [Navigator] is only built if routes are provided (either via [home],
/// [routes], [onGenerateRoute], or [onUnknownRoute]); if they are not,
/// [builder] must not be null.
final RouteFactory onUnknownRoute;
/// {@macro flutter.widgets.widgetsApp.navigatorObservers}
///
/// The [Navigator] is only built if routes are provided (either via [home],
/// [routes], [onGenerateRoute], or [onUnknownRoute]); if they are not,
/// [navigatorObservers] must be the empty list and [builder] must not be null.
final List<NavigatorObserver> navigatorObservers;
/// {@macro flutter.widgets.widgetsApp.builder}
///
/// If no routes are provided using [home], [routes], [onGenerateRoute], or
/// [onUnknownRoute], the `child` will be null, and it is the responsibility
/// of the [builder] to provide the application's routing machinery.
///
/// If routes _are_ provided using one or more of those properties, then
/// `child` is not null, and the returned value should include the `child` in
/// the widget subtree; if it does not, then the application will have no
/// navigator and the [navigatorKey], [home], [routes], [onGenerateRoute],
/// [onUnknownRoute], [initialRoute], and [navigatorObservers] properties will
/// have no effect.
///
/// If [builder] is null, it is as if a builder was specified that returned
/// the `child` directly. If it is null, routes must be provided using one of
/// the other properties listed above.
///
/// Unless a [Navigator] is provided, either implicitly from [builder] being
/// null, or by a [builder] including its `child` argument, or by a [builder]
/// explicitly providing a [Navigator] of its own, widgets and APIs such as
/// [Hero], [Navigator.push] and [Navigator.pop], will not function.
final TransitionBuilder builder;
/// {@macro flutter.widgets.widgetsApp.title}
///
/// This value is passed unmodified to [WidgetsApp.title].
final String title;
/// {@macro flutter.widgets.widgetsApp.onGenerateTitle}
///
/// This value is passed unmodified to [WidgetsApp.onGenerateTitle].
final GenerateAppTitle onGenerateTitle;
/// {@macro flutter.widgets.widgetsApp.color}
final Color color;
/// {@macro flutter.widgets.widgetsApp.locale}
final Locale locale;
/// {@macro flutter.widgets.widgetsApp.localizationsDelegates}
final Iterable<LocalizationsDelegate<dynamic>> localizationsDelegates;
/// {@macro flutter.widgets.widgetsApp.localeResolutionCallback}
///
/// This callback is passed along to the [WidgetsApp] built by this widget.
final LocaleResolutionCallback localeResolutionCallback;
/// {@macro flutter.widgets.widgetsApp.supportedLocales}
///
/// It is passed along unmodified to the [WidgetsApp] built by this widget.
final Iterable<Locale> supportedLocales;
/// Turns on a performance overlay.
///
/// See also:
///
/// * <https://flutter.io/debugging/#performanceoverlay>
final bool showPerformanceOverlay;
/// Turns on checkerboarding of raster cache images.
final bool checkerboardRasterCacheImages;
/// Turns on checkerboarding of layers rendered to offscreen bitmaps.
final bool checkerboardOffscreenLayers;
/// Turns on an overlay that shows the accessibility information
/// reported by the framework.
final bool showSemanticsDebugger;
/// {@macro flutter.widgets.widgetsApp.debugShowCheckedModeBanner}
final bool debugShowCheckedModeBanner;
@override
_CupertinoAppState createState() => new _CupertinoAppState();
}
class _AlwaysCupertinoScrollBehavior extends ScrollBehavior {
@override
Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) {
// Never build any overscroll glow indicators.
return child;
}
@override
ScrollPhysics getScrollPhysics(BuildContext context) {
return const BouncingScrollPhysics();
}
}
class _CupertinoAppState extends State<CupertinoApp> {
HeroController _heroController;
List<NavigatorObserver> _navigatorObservers;
@override
void initState() {
super.initState();
_heroController = new HeroController(); // Linear tweening.
_updateNavigator();
}
@override
void didUpdateWidget(CupertinoApp oldWidget) {
super.didUpdateWidget(oldWidget);
_updateNavigator();
}
bool _haveNavigator;
void _updateNavigator() {
_haveNavigator = widget.home != null ||
widget.routes.isNotEmpty ||
widget.onGenerateRoute != null ||
widget.onUnknownRoute != null;
_navigatorObservers =
new List<NavigatorObserver>.from(widget.navigatorObservers)
..add(_heroController);
}
Widget defaultBuilder(BuildContext context, Widget child) {
// The `child` coming back out from WidgetsApp will always be null since
// we never passed in anything for it to create a Navigator inside
// WidgetsApp.
assert(child == null);
if (_haveNavigator) {
// Reuse CupertinoTabView which creates a routing Navigator for us.
final Widget navigator = new CupertinoTabView(
builder: widget.home != null
? (BuildContext context) => widget.home
: null,
routes: widget.routes,
onGenerateRoute: widget.onGenerateRoute,
onUnknownRoute: widget.onUnknownRoute,
navigatorObservers: _navigatorObservers,
);
if (widget.builder != null) {
return widget.builder(context, navigator);
} else {
return navigator;
}
} else {
// We asserted that child is null above.
return widget.builder(context, null);
}
}
@override
Widget build(BuildContext context) {
return new ScrollConfiguration(
behavior: new _AlwaysCupertinoScrollBehavior(),
child: new WidgetsApp(
key: new GlobalObjectKey(this),
// We're passing in a builder and nothing else that the WidgetsApp uses
// to build its own Navigator because we're building a Navigator with
// routes in this class.
builder: defaultBuilder,
title: widget.title,
onGenerateTitle: widget.onGenerateTitle,
textStyle: _kDefaultTextStyle,
color: widget.color ?? CupertinoColors.activeBlue,
locale: widget.locale,
localizationsDelegates: widget.localizationsDelegates,
localeResolutionCallback: widget.localeResolutionCallback,
supportedLocales: widget.supportedLocales,
showPerformanceOverlay: widget.showPerformanceOverlay,
checkerboardRasterCacheImages: widget.checkerboardRasterCacheImages,
checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers,
showSemanticsDebugger: widget.showSemanticsDebugger,
debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner,
inspectorSelectButtonBuilder: (BuildContext context, VoidCallback onPressed) {
return new CupertinoButton(
child: const Icon(
CupertinoIcons.search,
size: 28.0,
color: CupertinoColors.white,
),
color: CupertinoColors.activeBlue,
padding: EdgeInsets.zero,
onPressed: onPressed,
);
},
),
);
}
}