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';
9library;
10
11import 'package:flutter/foundation.dart';
12import 'package:flutter/widgets.dart';
13
14import 'colors.dart';
15import '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.
41class 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
92class _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.
208class 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.
243abstract 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

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com