// Copyright 2014 The Flutter 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'; /// Implements a single iOS application page's layout. /// /// The scaffold lays out the navigation bar on top and the content between or /// behind the navigation bar. /// /// When tapping a status bar at the top of the IsekaiPageScaffold, an /// animation will complete for the current primary [ScrollView], scrolling to /// the beginning. This is done using the [PrimaryScrollController] that /// encloses the [ScrollView]. The [ScrollView.primary] flag is used to connect /// a [ScrollView] to the enclosing [PrimaryScrollController]. /// /// {@tool dartpad} /// This example shows a [IsekaiPageScaffold] with a [ListView] as a [child]. /// The [CupertinoButton] is connected to a callback that increments a counter. /// The [backgroundColor] can be changed. /// /// ** See code in examples/api/lib/cupertino/page_scaffold/cupertino_page_scaffold.0.dart ** /// {@end-tool} /// /// See also: /// /// * [CupertinoTabScaffold], a similar widget for tabbed applications. /// * [CupertinoPageRoute], a modal page route that typically hosts a /// [IsekaiPageScaffold] with support for iOS-style page transitions. class IsekaiPageScaffold extends StatefulWidget { /// Creates a layout for pages with a navigation bar at the top. const IsekaiPageScaffold({ super.key, this.navigationBar, this.bottomNavigationBar, this.backgroundColor, this.resizeToAvoidBottomInset = false, required this.child, }) : assert(child != null), assert(resizeToAvoidBottomInset != null); /// The [navigationBar], typically a [CupertinoNavigationBar], is drawn at the /// top of the screen. /// /// If translucent, the main content may slide behind it. /// Otherwise, the main content's top margin will be offset by its height. /// /// The scaffold assumes the navigation bar will account for the [MediaQuery] /// top padding, also consume it if the navigation bar is opaque. // TODO(xster): document its page transition animation when ready final ObstructingPreferredSizeWidget? navigationBar; /// The [bottomNavigationBar], typically a [CupertinoNavigationBar], is drawn at the /// bottom of the screen. /// /// If translucent, the main content may slide behind it. /// Otherwise, the main content's bottom margin will be offset by its height. /// /// The scaffold assumes the navigation bar will account for the [MediaQuery] /// bottom padding, also consume it if the navigation bar is opaque. final ObstructingPreferredSizeWidget? bottomNavigationBar; /// Widget to show in the main content area. /// /// Content can slide under the [navigationBar] when they're translucent. /// In that case, the child's [BuildContext]'s [MediaQuery] will have a /// top padding indicating the area of obstructing overlap from the /// [navigationBar]. final Widget child; /// The color of the widget that underlies the entire scaffold. /// /// By default uses [CupertinoTheme]'s `scaffoldBackgroundColor` when null. final Color? backgroundColor; /// Whether the [child] should size itself to avoid the window's bottom inset. /// /// For example, if there is an onscreen keyboard displayed above the /// scaffold, the body can be resized to avoid overlapping the keyboard, which /// prevents widgets inside the body from being obscured by the keyboard. /// /// Defaults to true and cannot be null. final bool resizeToAvoidBottomInset; @override State createState() => _IsekaiPageScaffoldState(); } class _IsekaiPageScaffoldState extends State { void _handleStatusBarTap() { final ScrollController? primaryScrollController = PrimaryScrollController.of(context); // Only act on the scroll controller if it has any attached scroll positions. if (primaryScrollController != null && primaryScrollController.hasClients) { primaryScrollController.animateTo( 0.0, // Eyeballed from iOS. duration: const Duration(milliseconds: 500), curve: Curves.linearToEaseOut, ); } } @override Widget build(BuildContext context) { Widget paddedContent = widget.child; final MediaQueryData existingMediaQuery = MediaQuery.of(context); final double topPadding; final bool topFullObstruction; if (widget.navigationBar != null) { // Propagate top padding and include viewInsets if appropriate topPadding = widget.navigationBar!.preferredSize.height + existingMediaQuery.padding.top; topFullObstruction = widget.navigationBar!.shouldFullyObstruct(context); } else { // Propagate bottom padding and include viewInsets if appropriate topPadding = existingMediaQuery.padding.top; topFullObstruction = false; } final double bottomPadding; final bool bottomFullObstruction; if (widget.bottomNavigationBar != null) { // Propagate bottom padding and include viewInsets if appropriate bottomPadding = widget.bottomNavigationBar!.preferredSize.height + existingMediaQuery.padding.bottom; bottomFullObstruction = widget.bottomNavigationBar!.shouldFullyObstruct(context); } else { // Propagate bottom padding and include viewInsets if appropriate bottomPadding = existingMediaQuery.padding.bottom; bottomFullObstruction = false; } final EdgeInsets newViewInsets = existingMediaQuery.viewInsets; var fixedExistingMediaQuery = existingMediaQuery.removePadding( removeTop: widget.navigationBar != null && topFullObstruction, removeBottom: widget.bottomNavigationBar != null && bottomFullObstruction, ); paddedContent = MediaQuery( data: fixedExistingMediaQuery.copyWith( padding: fixedExistingMediaQuery.padding.copyWith( top: topFullObstruction ? null : topPadding, bottom: bottomFullObstruction ? null : bottomPadding, ), viewInsets: newViewInsets, ), child: Padding( padding: EdgeInsets.only( top: (widget.navigationBar == null) ? 0 : (topFullObstruction ? topPadding : 0), bottom: (widget.bottomNavigationBar == null) ? 0 : (bottomFullObstruction ? bottomPadding : 0), ), child: paddedContent, ), ); return DecoratedBox( decoration: BoxDecoration( color: CupertinoDynamicColor.maybeResolve( widget.backgroundColor, context) ?? CupertinoTheme.of(context).scaffoldBackgroundColor, ), child: Stack( children: [ // The main content being at the bottom is added to the stack first. paddedContent, if (widget.navigationBar != null) Positioned( top: 0.0, left: 0.0, right: 0.0, child: widget.navigationBar!, ), if (widget.bottomNavigationBar != null) Positioned( bottom: 0.0, left: 0.0, right: 0.0, child: widget.bottomNavigationBar!, ), // Add a touch handler the size of the status bar on top of all contents // to handle scroll to top by status bar taps. Positioned( top: 0.0, left: 0.0, right: 0.0, height: existingMediaQuery.padding.top, child: GestureDetector( excludeFromSemantics: true, onTap: _handleStatusBarTap, ), ), ], ), ); } }