1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
// 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';
import 'theme.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.
///
/// See also:
///
/// * [CupertinoTabScaffold], a similar widget for tabbed applications.
/// * [CupertinoPageRoute], a modal page route that typically hosts a
/// [CupertinoPageScaffold] with support for iOS-style page transitions.
class CupertinoPageScaffold extends StatefulWidget {
/// Creates a layout for pages with a navigation bar at the top.
const CupertinoPageScaffold({
Key key,
this.navigationBar,
this.backgroundColor,
this.resizeToAvoidBottomInset = true,
@required this.child,
}) : assert(child != null),
assert(resizeToAvoidBottomInset != null),
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.
///
/// 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].
// TODO(xster): document its page transition animation when ready
final ObstructingPreferredSizeWidget navigationBar;
/// 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
_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,
);
}
}
@override
Widget build(BuildContext context) {
final List<Widget> stacked = <Widget>[];
Widget paddedContent = widget.child;
final MediaQueryData existingMediaQuery = MediaQuery.of(context);
if (widget.navigationBar != null) {
// TODO(xster): Use real size after partial layout instead of preferred size.
// https://github.com/flutter/flutter/issues/12912
final double topPadding =
widget.navigationBar.preferredSize.height + existingMediaQuery.padding.top;
// Propagate bottom padding and include viewInsets if appropriate
final double bottomPadding = widget.resizeToAvoidBottomInset
? existingMediaQuery.viewInsets.bottom
: 0.0;
final EdgeInsets newViewInsets = widget.resizeToAvoidBottomInset
// The insets are consumed by the scaffolds and no longer exposed to
// the descendant subtree.
? existingMediaQuery.viewInsets.copyWith(bottom: 0.0)
: existingMediaQuery.viewInsets;
final bool fullObstruction =
widget.navigationBar.fullObstruction ?? CupertinoTheme.of(context).barBackgroundColor.alpha == 0xFF;
// If navigation bar is opaquely obstructing, directly shift the main content
// down. If translucent, let main content draw behind navigation bar but hint the
// obstructed area.
if (fullObstruction) {
paddedContent = MediaQuery(
data: existingMediaQuery
// If the navigation bar is opaque, the top media query padding is fully consumed by the navigation bar.
.removePadding(removeTop: true)
.copyWith(
viewInsets: newViewInsets,
),
child: Padding(
padding: EdgeInsets.only(top: topPadding, bottom: bottomPadding),
child: paddedContent,
),
);
} else {
paddedContent = MediaQuery(
data: existingMediaQuery.copyWith(
padding: existingMediaQuery.padding.copyWith(
top: topPadding,
),
viewInsets: newViewInsets,
),
child: Padding(
padding: EdgeInsets.only(bottom: bottomPadding),
child: paddedContent,
),
);
}
} 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,
);
}
// The main content being at the bottom is added to the stack first.
stacked.add(PrimaryScrollController(
controller: _primaryScrollController,
child: paddedContent,
));
if (widget.navigationBar != null) {
stacked.add(Positioned(
top: 0.0,
left: 0.0,
right: 0.0,
child: MediaQuery(
data: existingMediaQuery.copyWith(textScaleFactor: 1),
child: widget.navigationBar,
),
));
}
// 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,
),
),
);
return DecoratedBox(
decoration: BoxDecoration(
color: widget.backgroundColor ?? CupertinoTheme.of(context).scaffoldBackgroundColor,
),
child: Stack(
children: stacked,
),
);
}
}
/// 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;
}