page_scaffold.dart 6.65 KB
Newer Older
1 2 3 4 5 6
// Copyright 2017 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/widgets.dart';

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

/// 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.
13 14 15
///
/// See also:
///
16 17 18
///  * [CupertinoTabScaffold], a similar widget for tabbed applications.
///  * [CupertinoPageRoute], a modal page route that typically hosts a
///    [CupertinoPageScaffold] with support for iOS-style page transitions.
19
class CupertinoPageScaffold extends StatefulWidget {
20
  /// Creates a layout for pages with a navigation bar at the top.
21 22 23
  const CupertinoPageScaffold({
    Key key,
    this.navigationBar,
xster's avatar
xster committed
24
    this.backgroundColor,
25
    this.resizeToAvoidBottomInset = true,
26 27
    @required this.child,
  }) : assert(child != null),
28
       assert(resizeToAvoidBottomInset != null),
29 30 31 32 33 34 35
       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.
36
  ///
37 38
  /// The scaffold assumes the navigation bar will account for the [MediaQuery] top padding,
  /// also consume it if the navigation bar is opaque.
39
  // TODO(xster): document its page transition animation when ready
40
  final ObstructingPreferredSizeWidget navigationBar;
41 42 43

  /// Widget to show in the main content area.
  ///
44
  /// Content can slide under the [navigationBar] when they're translucent.
45 46 47
  /// In that case, the child's [BuildContext]'s [MediaQuery] will have a
  /// top padding indicating the area of obstructing overlap from the
  /// [navigationBar].
48 49
  final Widget child;

50 51
  /// The color of the widget that underlies the entire scaffold.
  ///
xster's avatar
xster committed
52
  /// By default uses [CupertinoTheme]'s `scaffoldBackgroundColor` when null.
53 54
  final Color backgroundColor;

55 56 57 58 59 60
  /// 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.
  ///
61
  /// Defaults to true and cannot be null.
62 63
  final bool resizeToAvoidBottomInset;

64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
  @override
  _CupertinoPageScaffoldState createState() => _CupertinoPageScaffoldState();
}

class _CupertinoPageScaffoldState extends State<CupertinoPageScaffold> {
  final ScrollController _primaryScrollController = ScrollController();

  void _handleStatusBarTap() {
    // Only act on the scroll controller if it has any attached scroll positions.
    if (_primaryScrollController.hasClients) {
      _primaryScrollController.animateTo(
        0.0,
        // Eyeballed from iOS.
        duration: const Duration(milliseconds: 500),
        curve: Curves.linearToEaseOut,
      );
    }
  }

83 84 85
  @override
  Widget build(BuildContext context) {
    final List<Widget> stacked = <Widget>[];
86

87
    Widget paddedContent = widget.child;
88

89 90
    final MediaQueryData existingMediaQuery = MediaQuery.of(context);
    if (widget.navigationBar != null) {
91 92
      // TODO(xster): Use real size after partial layout instead of preferred size.
      // https://github.com/flutter/flutter/issues/12912
93
      final double topPadding =
94
          widget.navigationBar.preferredSize.height + existingMediaQuery.padding.top;
95 96

      // Propagate bottom padding and include viewInsets if appropriate
97
      final double bottomPadding = widget.resizeToAvoidBottomInset
98 99
          ? existingMediaQuery.viewInsets.bottom
          : 0.0;
100

101
      final EdgeInsets newViewInsets = widget.resizeToAvoidBottomInset
102 103 104 105 106
          // The insets are consumed by the scaffolds and no longer exposed to
          // the descendant subtree.
          ? existingMediaQuery.viewInsets.copyWith(bottom: 0.0)
          : existingMediaQuery.viewInsets;

xster's avatar
xster committed
107
      final bool fullObstruction =
108
        widget.navigationBar.fullObstruction ?? CupertinoTheme.of(context).barBackgroundColor.alpha == 0xFF;
xster's avatar
xster committed
109

110 111
      // If navigation bar is opaquely obstructing, directly shift the main content
      // down. If translucent, let main content draw behind navigation bar but hint the
112
      // obstructed area.
xster's avatar
xster committed
113
      if (fullObstruction) {
114
        paddedContent = MediaQuery(
115 116 117 118
          data: existingMediaQuery
          // If the navigation bar is opaque, the top media query padding is fully consumed by the navigation bar.
          .removePadding(removeTop: true)
          .copyWith(
119 120 121 122
            viewInsets: newViewInsets,
          ),
          child: Padding(
            padding: EdgeInsets.only(top: topPadding, bottom: bottomPadding),
123
            child: paddedContent,
124
          ),
125 126
        );
      } else {
127
        paddedContent = MediaQuery(
128 129 130 131
          data: existingMediaQuery.copyWith(
            padding: existingMediaQuery.padding.copyWith(
              top: topPadding,
            ),
132
            viewInsets: newViewInsets,
133
          ),
134 135
          child: Padding(
            padding: EdgeInsets.only(bottom: bottomPadding),
136
            child: paddedContent,
137
          ),
138 139
        );
      }
140 141 142
    }

    // The main content being at the bottom is added to the stack first.
143 144 145 146
    stacked.add(PrimaryScrollController(
      controller: _primaryScrollController,
      child: paddedContent,
    ));
147

148
    if (widget.navigationBar != null) {
149
      stacked.add(Positioned(
150 151 152
        top: 0.0,
        left: 0.0,
        right: 0.0,
153
        child: widget.navigationBar,
154 155 156
      ));
    }

157 158 159 160 161 162 163 164 165 166 167 168 169 170
    // Add a touch handler the size of the status bar on top of all contents
    // to handle scroll to top by status bar taps.
    stacked.add(Positioned(
      top: 0.0,
      left: 0.0,
      right: 0.0,
      height: existingMediaQuery.padding.top,
      child: GestureDetector(
          excludeFromSemantics: true,
          onTap: _handleStatusBarTap,
        ),
      ),
    );

171
    return DecoratedBox(
xster's avatar
xster committed
172
      decoration: BoxDecoration(
173
        color: widget.backgroundColor ?? CupertinoTheme.of(context).scaffoldBackgroundColor,
xster's avatar
xster committed
174
      ),
175
      child: Stack(
176 177 178 179
        children: stacked,
      ),
    );
  }
180 181 182 183 184 185 186 187 188 189 190 191 192 193
}

/// 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.
abstract class ObstructingPreferredSizeWidget extends PreferredSizeWidget {
  /// If true, this widget fully obstructs widgets behind it by the specified
  /// size.
  ///
  /// If false, this widget partially obstructs.
  bool get fullObstruction;
}