1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | /// @docImport 'button.dart'; |
6 | /// @docImport 'nav_bar.dart'; |
7 | /// @docImport 'route.dart'; |
8 | /// @docImport 'tab_scaffold.dart'; |
9 | library; |
10 | |
11 | import 'package:flutter/foundation.dart'; |
12 | import 'package:flutter/widgets.dart'; |
13 | |
14 | import 'colors.dart'; |
15 | import 'theme.dart'; |
16 | |
17 | /// Implements a single iOS application page's layout. |
18 | /// |
19 | /// The scaffold lays out the navigation bar on top and the content between or |
20 | /// behind the navigation bar. |
21 | /// |
22 | /// When tapping a status bar at the top of the CupertinoPageScaffold, an |
23 | /// animation will complete for the current primary [ScrollView], scrolling to |
24 | /// the beginning. This is done using the [PrimaryScrollController] that |
25 | /// encloses the [ScrollView]. The [ScrollView.primary] flag is used to connect |
26 | /// a [ScrollView] to the enclosing [PrimaryScrollController]. |
27 | /// |
28 | /// {@tool dartpad} |
29 | /// This example shows a [CupertinoPageScaffold] with a [Center] as a [child]. |
30 | /// The [CupertinoButton] is connected to a callback that increments a counter. |
31 | /// The [backgroundColor] can be changed. |
32 | /// |
33 | /// ** See code in examples/api/lib/cupertino/page_scaffold/cupertino_page_scaffold.0.dart ** |
34 | /// {@end-tool} |
35 | /// |
36 | /// See also: |
37 | /// |
38 | /// * [CupertinoTabScaffold], a similar widget for tabbed applications. |
39 | /// * [CupertinoPageRoute], a modal page route that typically hosts a |
40 | /// [CupertinoPageScaffold] with support for iOS-style page transitions. |
41 | class CupertinoPageScaffold extends StatefulWidget { |
42 | /// Creates a layout for pages with a navigation bar at the top. |
43 | const CupertinoPageScaffold({ |
44 | super.key, |
45 | this.navigationBar, |
46 | this.backgroundColor, |
47 | this.resizeToAvoidBottomInset = true, |
48 | required this.child, |
49 | }); |
50 | |
51 | /// The [navigationBar], typically a [CupertinoNavigationBar], is drawn at the |
52 | /// top of the screen. |
53 | /// |
54 | /// If translucent, the main content may slide behind it. |
55 | /// Otherwise, the main content's top margin will be offset by its height. |
56 | /// |
57 | /// The scaffold assumes the navigation bar will account for the [MediaQuery] |
58 | /// top padding, also consume it if the navigation bar is opaque. |
59 | /// |
60 | /// By default [navigationBar] disables text scaling to match the native iOS |
61 | /// behavior. To override such behavior, wrap each of the [navigationBar]'s |
62 | /// components inside a [MediaQuery] with the desired [TextScaler]. |
63 | // TODO(xster): document its page transition animation when ready |
64 | final ObstructingPreferredSizeWidget? navigationBar; |
65 | |
66 | /// Widget to show in the main content area. |
67 | /// |
68 | /// Content can slide under the [navigationBar] when they're translucent. |
69 | /// In that case, the child's [BuildContext]'s [MediaQuery] will have a |
70 | /// top padding indicating the area of obstructing overlap from the |
71 | /// [navigationBar]. |
72 | final Widget child; |
73 | |
74 | /// The color of the widget that underlies the entire scaffold. |
75 | /// |
76 | /// By default uses [CupertinoTheme]'s `scaffoldBackgroundColor` when null. |
77 | final Color? backgroundColor; |
78 | |
79 | /// Whether the [child] should size itself to avoid the window's bottom inset. |
80 | /// |
81 | /// For example, if there is an onscreen keyboard displayed above the |
82 | /// scaffold, the body can be resized to avoid overlapping the keyboard, which |
83 | /// prevents widgets inside the body from being obscured by the keyboard. |
84 | /// |
85 | /// Defaults to true. |
86 | final bool resizeToAvoidBottomInset; |
87 | |
88 | @override |
89 | State<CupertinoPageScaffold> createState() => _CupertinoPageScaffoldState(); |
90 | } |
91 | |
92 | class _CupertinoPageScaffoldState extends State<CupertinoPageScaffold> { |
93 | void _handleStatusBarTap() { |
94 | final ScrollController? primaryScrollController = PrimaryScrollController.maybeOf(context); |
95 | // Only act on the scroll controller if it has any attached scroll positions. |
96 | if (primaryScrollController != null && primaryScrollController.hasClients) { |
97 | primaryScrollController.animateTo( |
98 | 0.0, |
99 | // Eyeballed from iOS. |
100 | duration: const Duration(milliseconds: 500), |
101 | curve: Curves.linearToEaseOut, |
102 | ); |
103 | } |
104 | } |
105 | |
106 | @override |
107 | Widget build(BuildContext context) { |
108 | Widget paddedContent = widget.child; |
109 | |
110 | final Color backgroundColor = |
111 | CupertinoDynamicColor.maybeResolve(widget.backgroundColor, context) ?? |
112 | CupertinoTheme.of(context).scaffoldBackgroundColor; |
113 | |
114 | final MediaQueryData existingMediaQuery = MediaQuery.of(context); |
115 | if (widget.navigationBar != null) { |
116 | // TODO(xster): Use real size after partial layout instead of preferred size. |
117 | // https://github.com/flutter/flutter/issues/12912 |
118 | final double topPadding = |
119 | widget.navigationBar!.preferredSize.height + existingMediaQuery.padding.top; |
120 | |
121 | // Propagate bottom padding and include viewInsets if appropriate |
122 | final double bottomPadding = |
123 | widget.resizeToAvoidBottomInset ? existingMediaQuery.viewInsets.bottom : 0.0; |
124 | |
125 | final EdgeInsets newViewInsets = |
126 | widget.resizeToAvoidBottomInset |
127 | // The insets are consumed by the scaffolds and no longer exposed to |
128 | // the descendant subtree. |
129 | ? existingMediaQuery.viewInsets.copyWith(bottom: 0.0) |
130 | : existingMediaQuery.viewInsets; |
131 | |
132 | final bool fullObstruction = widget.navigationBar!.shouldFullyObstruct(context); |
133 | |
134 | // If navigation bar is opaquely obstructing, directly shift the main content |
135 | // down. If translucent, let main content draw behind navigation bar but hint the |
136 | // obstructed area. |
137 | if (fullObstruction) { |
138 | paddedContent = MediaQuery( |
139 | data: existingMediaQuery |
140 | // If the navigation bar is opaque, the top media query padding is fully consumed by the navigation bar. |
141 | .removePadding(removeTop: true) |
142 | .copyWith(viewInsets: newViewInsets), |
143 | child: Padding( |
144 | padding: EdgeInsets.only(top: topPadding, bottom: bottomPadding), |
145 | child: paddedContent, |
146 | ), |
147 | ); |
148 | } else { |
149 | paddedContent = MediaQuery( |
150 | data: existingMediaQuery.copyWith( |
151 | padding: existingMediaQuery.padding.copyWith(top: topPadding), |
152 | viewInsets: newViewInsets, |
153 | ), |
154 | child: Padding(padding: EdgeInsets.only(bottom: bottomPadding), child: paddedContent), |
155 | ); |
156 | } |
157 | } else if (widget.resizeToAvoidBottomInset) { |
158 | // If there is no navigation bar, still may need to add padding in order |
159 | // to support resizeToAvoidBottomInset. |
160 | paddedContent = MediaQuery( |
161 | data: existingMediaQuery.copyWith( |
162 | viewInsets: existingMediaQuery.viewInsets.copyWith(bottom: 0), |
163 | ), |
164 | child: Padding( |
165 | padding: EdgeInsets.only(bottom: existingMediaQuery.viewInsets.bottom), |
166 | child: paddedContent, |
167 | ), |
168 | ); |
169 | } |
170 | |
171 | return ScrollNotificationObserver( |
172 | child: DecoratedBox( |
173 | decoration: BoxDecoration(color: backgroundColor), |
174 | child: CupertinoPageScaffoldBackgroundColor( |
175 | color: backgroundColor, |
176 | child: Stack( |
177 | children: <Widget>[ |
178 | // The main content being at the bottom is added to the stack first. |
179 | paddedContent, |
180 | if (widget.navigationBar != null) |
181 | Positioned( |
182 | top: 0.0, |
183 | left: 0.0, |
184 | right: 0.0, |
185 | child: MediaQuery.withNoTextScaling(child: widget.navigationBar!), |
186 | ), |
187 | // Add a touch handler the size of the status bar on top of all contents |
188 | // to handle scroll to top by status bar taps. |
189 | Positioned( |
190 | top: 0.0, |
191 | left: 0.0, |
192 | right: 0.0, |
193 | height: existingMediaQuery.padding.top, |
194 | child: GestureDetector(excludeFromSemantics: true, onTap: _handleStatusBarTap), |
195 | ), |
196 | ], |
197 | ), |
198 | ), |
199 | ), |
200 | ); |
201 | } |
202 | } |
203 | |
204 | /// [InheritedWidget] indicating what the current scaffold background color is for its children. |
205 | /// |
206 | /// This is used by the [CupertinoNavigationBar] and the [CupertinoSliverNavigationBar] widgets |
207 | /// to paint themselves with the parent page scaffold color when no content is scrolled under. |
208 | class CupertinoPageScaffoldBackgroundColor extends InheritedWidget { |
209 | /// Constructs a new [CupertinoPageScaffoldBackgroundColor]. |
210 | const CupertinoPageScaffoldBackgroundColor({ |
211 | required super.child, |
212 | required this.color, |
213 | super.key, |
214 | }); |
215 | |
216 | /// The background color defined in [CupertinoPageScaffold]. |
217 | final Color color; |
218 | |
219 | @override |
220 | bool updateShouldNotify(CupertinoPageScaffoldBackgroundColor oldWidget) { |
221 | return color != oldWidget.color; |
222 | } |
223 | |
224 | /// Retrieve the [CupertinoPageScaffold] background color from the context. |
225 | static Color? maybeOf(BuildContext context) { |
226 | final CupertinoPageScaffoldBackgroundColor? scaffoldBackgroundColor = |
227 | context.dependOnInheritedWidgetOfExactType<CupertinoPageScaffoldBackgroundColor>(); |
228 | return scaffoldBackgroundColor?.color; |
229 | } |
230 | |
231 | @override |
232 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
233 | super.debugFillProperties(properties); |
234 | properties.add(ColorProperty('page scaffold background color' , color)); |
235 | } |
236 | } |
237 | |
238 | /// Widget that has a preferred size and reports whether it fully obstructs |
239 | /// widgets behind it. |
240 | /// |
241 | /// Used by [CupertinoPageScaffold] to either shift away fully obstructed content |
242 | /// or provide a padding guide to partially obstructed content. |
243 | abstract class ObstructingPreferredSizeWidget implements PreferredSizeWidget { |
244 | /// If true, this widget fully obstructs widgets behind it by the specified |
245 | /// size. |
246 | /// |
247 | /// If false, this widget partially obstructs. |
248 | bool shouldFullyObstruct(BuildContext context); |
249 | } |
250 | |