// 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> { @override void initState() { super.initState(); _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; } 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: widget.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, ); }, ), ); } }