Unverified Commit 7f3485e3 authored by xster's avatar xster Committed by GitHub

Let CupertinoPageScaffold have tap status bar to scroll to top (#29946)

parent 0b687122
......@@ -16,7 +16,7 @@ import 'theme.dart';
/// * [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 StatelessWidget {
class CupertinoPageScaffold extends StatefulWidget {
/// Creates a layout for pages with a navigation bar at the top.
const CupertinoPageScaffold({
Key key,
......@@ -61,32 +61,51 @@ class CupertinoPageScaffold extends StatelessWidget {
/// 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 = child;
if (navigationBar != null) {
final MediaQueryData existingMediaQuery = MediaQuery.of(context);
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 =
navigationBar.preferredSize.height + existingMediaQuery.padding.top;
widget.navigationBar.preferredSize.height + existingMediaQuery.padding.top;
// Propagate bottom padding and include viewInsets if appropriate
final double bottomPadding = resizeToAvoidBottomInset
final double bottomPadding = widget.resizeToAvoidBottomInset
? existingMediaQuery.viewInsets.bottom
: 0.0;
final EdgeInsets newViewInsets = resizeToAvoidBottomInset
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 =
navigationBar.fullObstruction ?? CupertinoTheme.of(context).barBackgroundColor.alpha == 0xFF;
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
......@@ -101,7 +120,7 @@ class CupertinoPageScaffold extends StatelessWidget {
),
child: Padding(
padding: EdgeInsets.only(top: topPadding, bottom: bottomPadding),
child: child,
child: paddedContent,
),
);
} else {
......@@ -114,27 +133,44 @@ class CupertinoPageScaffold extends StatelessWidget {
),
child: Padding(
padding: EdgeInsets.only(bottom: bottomPadding),
child: child,
child: paddedContent,
),
);
}
}
// The main content being at the bottom is added to the stack first.
stacked.add(paddedContent);
stacked.add(PrimaryScrollController(
controller: _primaryScrollController,
child: paddedContent,
));
if (navigationBar != null) {
if (widget.navigationBar != null) {
stacked.add(Positioned(
top: 0.0,
left: 0.0,
right: 0.0,
child: navigationBar,
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: backgroundColor ?? CupertinoTheme.of(context).scaffoldBackgroundColor,
color: widget.backgroundColor ?? CupertinoTheme.of(context).scaffoldBackgroundColor,
),
child: Stack(
children: stacked,
......
......@@ -343,4 +343,56 @@ testWidgets('Opaque bar pushes contents down', (WidgetTester tester) async {
final BoxDecoration decoration = decoratedBox.decoration;
expect(decoration.color, const Color(0xFF010203));
});
testWidgets('Lists in CupertinoPageScaffold scroll to the top when status bar tapped', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
builder: (BuildContext context, Widget child) {
// Acts as a 20px status bar at the root of the app.
return MediaQuery(
data: MediaQuery.of(context).copyWith(padding: const EdgeInsets.only(top: 20)),
child: child,
);
},
home: CupertinoPageScaffold(
// Default nav bar is translucent.
navigationBar: const CupertinoNavigationBar(
middle: Text('Title'),
),
child: ListView.builder(
itemExtent: 50,
itemBuilder: (BuildContext context, int index) => Text(index.toString()),
),
),
),
);
// Top media query padding 20 + translucent nav bar 44.
expect(tester.getTopLeft(find.text('0')).dy, 64);
expect(tester.getTopLeft(find.text('6')).dy, 364);
await tester.fling(
find.text('5'), // Find some random text on the screen.
const Offset(0, -200),
20,
);
await tester.pumpAndSettle();
expect(tester.getTopLeft(find.text('6')).dy, moreOrLessEquals(166.833, epsilon: 0.1));
expect(tester.getTopLeft(find.text('12')).dy, moreOrLessEquals(466.8333333333334, epsilon: 0.1));
// The media query top padding is 20. Tapping at 20 should do nothing.
await tester.tapAt(const Offset(400, 20));
await tester.pumpAndSettle();
expect(tester.getTopLeft(find.text('6')).dy, moreOrLessEquals(166.833, epsilon: 0.1));
expect(tester.getTopLeft(find.text('12')).dy, moreOrLessEquals(466.8333333333334, epsilon: 0.1));
// Tap 1 pixel higher.
await tester.tapAt(const Offset(400, 19));
await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
expect(tester.getTopLeft(find.text('0')).dy, 64);
expect(tester.getTopLeft(find.text('6')).dy, 364);
expect(find.text('12'), findsNothing);
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment