page_scaffold.dart 9.06 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/widgets.dart';

7
import 'colors.dart';
xster's avatar
xster committed
8
import 'theme.dart';
9 10 11 12 13

/// 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.
14
///
15 16 17 18 19 20
/// When tapping a status bar at the top of the CupertinoPageScaffold, 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].
///
21 22 23 24 25 26 27 28
/// {@tool dartpad --template=stateful_widget_cupertino}
/// This example shows a [CupertinoPageScaffold] with a [ListView] as a [child].
/// The [CupertinoButton] is connected to a callback that increments a counter.
/// The [backgroundColor] can be changed.
///
/// ```dart
/// int _count = 0;
///
29
/// @override
30 31 32 33
/// Widget build(BuildContext context) {
///   return CupertinoPageScaffold(
///     // Uncomment to change the background color
///     // backgroundColor: CupertinoColors.systemPink,
34
///     navigationBar: const CupertinoNavigationBar(
35
///       middle: Text('Sample Code'),
36 37
///     ),
///     child: ListView(
38
///       children: <Widget>[
39 40 41 42 43 44 45 46 47 48 49 50 51 52
///         CupertinoButton(
///           onPressed: () => setState(() => _count++),
///           child: const Icon(CupertinoIcons.add),
///         ),
///         Center(
///           child: Text('You have pressed the button $_count times.'),
///         ),
///       ],
///     ),
///   );
/// }
/// ```
/// {@end-tool}
///
53 54
/// See also:
///
55 56 57
///  * [CupertinoTabScaffold], a similar widget for tabbed applications.
///  * [CupertinoPageRoute], a modal page route that typically hosts a
///    [CupertinoPageScaffold] with support for iOS-style page transitions.
58
class CupertinoPageScaffold extends StatefulWidget {
59
  /// Creates a layout for pages with a navigation bar at the top.
60
  const CupertinoPageScaffold({
61
    Key? key,
62
    this.navigationBar,
xster's avatar
xster committed
63
    this.backgroundColor,
64
    this.resizeToAvoidBottomInset = true,
65
    required this.child,
66
  }) : assert(child != null),
67
       assert(resizeToAvoidBottomInset != null),
68 69 70 71 72 73 74
       super(key: key);

  /// 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.
75
  ///
76 77 78 79 80 81 82 83 84 85
  /// The scaffold assumes the navigation bar will account for the [MediaQuery]
  /// top padding, also consume it if the navigation bar is opaque.
  ///
  /// By default `navigationBar` has its text scale factor set to 1.0 and does
  /// not respond to text scale factor changes from the operating system, to match
  /// the native iOS behavior. To override such behavior, wrap each of the `navigationBar`'s
  /// components inside a [MediaQuery] with the desired [MediaQueryData.textScaleFactor]
  /// value. The text scale factor value from the operating system can be retrieved
  /// in many ways, such as querying [MediaQuery.textScaleFactorOf] against
  /// [CupertinoApp]'s [BuildContext].
86
  // TODO(xster): document its page transition animation when ready
87
  final ObstructingPreferredSizeWidget? navigationBar;
88 89 90

  /// Widget to show in the main content area.
  ///
91
  /// Content can slide under the [navigationBar] when they're translucent.
92 93 94
  /// In that case, the child's [BuildContext]'s [MediaQuery] will have a
  /// top padding indicating the area of obstructing overlap from the
  /// [navigationBar].
95 96
  final Widget child;

97 98
  /// The color of the widget that underlies the entire scaffold.
  ///
xster's avatar
xster committed
99
  /// By default uses [CupertinoTheme]'s `scaffoldBackgroundColor` when null.
100
  final Color? backgroundColor;
101

102 103 104 105 106 107
  /// 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.
  ///
108
  /// Defaults to true and cannot be null.
109 110
  final bool resizeToAvoidBottomInset;

111
  @override
112
  State<CupertinoPageScaffold> createState() => _CupertinoPageScaffoldState();
113 114 115 116 117
}

class _CupertinoPageScaffoldState extends State<CupertinoPageScaffold> {

  void _handleStatusBarTap() {
118
    final ScrollController? _primaryScrollController = PrimaryScrollController.of(context);
119
    // Only act on the scroll controller if it has any attached scroll positions.
120
    if (_primaryScrollController != null && _primaryScrollController.hasClients) {
121 122 123 124 125 126 127 128 129
      _primaryScrollController.animateTo(
        0.0,
        // Eyeballed from iOS.
        duration: const Duration(milliseconds: 500),
        curve: Curves.linearToEaseOut,
      );
    }
  }

130 131
  @override
  Widget build(BuildContext context) {
132
    Widget paddedContent = widget.child;
133

134
    final MediaQueryData existingMediaQuery = MediaQuery.of(context);
135
    if (widget.navigationBar != null) {
136 137
      // TODO(xster): Use real size after partial layout instead of preferred size.
      // https://github.com/flutter/flutter/issues/12912
138
      final double topPadding =
139
          widget.navigationBar!.preferredSize.height + existingMediaQuery.padding.top;
140 141

      // Propagate bottom padding and include viewInsets if appropriate
142
      final double bottomPadding = widget.resizeToAvoidBottomInset
143 144
          ? existingMediaQuery.viewInsets.bottom
          : 0.0;
145

146
      final EdgeInsets newViewInsets = widget.resizeToAvoidBottomInset
147 148 149 150 151
          // The insets are consumed by the scaffolds and no longer exposed to
          // the descendant subtree.
          ? existingMediaQuery.viewInsets.copyWith(bottom: 0.0)
          : existingMediaQuery.viewInsets;

152
      final bool fullObstruction = widget.navigationBar!.shouldFullyObstruct(context);
xster's avatar
xster committed
153

154 155
      // If navigation bar is opaquely obstructing, directly shift the main content
      // down. If translucent, let main content draw behind navigation bar but hint the
156
      // obstructed area.
xster's avatar
xster committed
157
      if (fullObstruction) {
158
        paddedContent = MediaQuery(
159 160 161 162
          data: existingMediaQuery
          // If the navigation bar is opaque, the top media query padding is fully consumed by the navigation bar.
          .removePadding(removeTop: true)
          .copyWith(
163 164 165 166
            viewInsets: newViewInsets,
          ),
          child: Padding(
            padding: EdgeInsets.only(top: topPadding, bottom: bottomPadding),
167
            child: paddedContent,
168
          ),
169 170
        );
      } else {
171
        paddedContent = MediaQuery(
172 173 174 175
          data: existingMediaQuery.copyWith(
            padding: existingMediaQuery.padding.copyWith(
              top: topPadding,
            ),
176
            viewInsets: newViewInsets,
177
          ),
178 179
          child: Padding(
            padding: EdgeInsets.only(bottom: bottomPadding),
180
            child: paddedContent,
181
          ),
182 183
        );
      }
184 185 186 187 188 189 190 191 192 193
    } else {
      // If there is no navigation bar, still may need to add padding in order
      // to support resizeToAvoidBottomInset.
      final double bottomPadding = widget.resizeToAvoidBottomInset
          ? existingMediaQuery.viewInsets.bottom
          : 0.0;
      paddedContent = Padding(
        padding: EdgeInsets.only(bottom: bottomPadding),
        child: paddedContent,
      );
194 195
    }

196
    return DecoratedBox(
xster's avatar
xster committed
197
      decoration: BoxDecoration(
198
        color: CupertinoDynamicColor.maybeResolve(widget.backgroundColor, context)
199
            ?? CupertinoTheme.of(context).scaffoldBackgroundColor,
xster's avatar
xster committed
200
      ),
201
      child: Stack(
202 203
        children: <Widget>[
          // The main content being at the bottom is added to the stack first.
204
          paddedContent,
205 206 207 208 209 210 211
          if (widget.navigationBar != null)
            Positioned(
              top: 0.0,
              left: 0.0,
              right: 0.0,
              child: MediaQuery(
                data: existingMediaQuery.copyWith(textScaleFactor: 1),
212
                child: widget.navigationBar!,
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227
              ),
            ),
          // 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,
            ),
          ),
        ],
228 229 230
      ),
    );
  }
231 232 233 234 235 236 237
}

/// Widget that has a preferred size and reports whether it fully obstructs
/// widgets behind it.
///
/// Used by [CupertinoPageScaffold] to either shift away fully obstructed content
/// or provide a padding guide to partially obstructed content.
238
abstract class ObstructingPreferredSizeWidget implements PreferredSizeWidget {
239 240 241 242
  /// If true, this widget fully obstructs widgets behind it by the specified
  /// size.
  ///
  /// If false, this widget partially obstructs.
243
  bool shouldFullyObstruct(BuildContext context);
244
}