mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Let CupertinoPageScaffold have tap status bar to scroll to top (#29946)
This commit is contained in:
parent
0b68712246
commit
7f3485e388
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user