Jaime Blasco
Committed by GitHub

Merge pull request #37 from jamesblasco/scroll_top

Support scroll to top when tapped on the status bar
... ... @@ -98,147 +98,153 @@ class _MyHomePageState extends State<MyHomePage> {
Widget build(BuildContext context) {
print(MediaQuery.of(context).size.height);
return Material(
child: CupertinoPageScaffold(
backgroundColor: Colors.white,
navigationBar: CupertinoNavigationBar(
transitionBetweenRoutes: false,
middle: Text('iOS13 Modal Presentation'),
trailing: GestureDetector(
child: Icon(Icons.arrow_forward),
onTap: () => Navigator.of(context).pushNamed('ss'),
child: Scaffold(
body: CupertinoPageScaffold(
backgroundColor: Colors.white,
navigationBar: CupertinoNavigationBar(
transitionBetweenRoutes: false,
middle: Text('iOS13 Modal Presentation'),
trailing: GestureDetector(
child: Icon(Icons.arrow_forward),
onTap: () => Navigator.of(context).pushNamed('ss'),
),
),
),
child: SizedBox.expand(
child: SingleChildScrollView(
child: SafeArea(
bottom: false,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTile(
title: Text('Cupertino Photo Share Example'),
onTap: () => Navigator.of(context).push(
MaterialWithModalsPageRoute(
builder: (context) => CupertinoSharePage()))),
section('STYLES'),
ListTile(
title: Text('Material fit'),
onTap: () => showMaterialModalBottomSheet(
expand: false,
context: context,
backgroundColor: Colors.transparent,
builder: (context, scrollController) =>
ModalFit(scrollController: scrollController),
)),
ListTile(
title: Text('Bar Modal'),
onTap: () => showBarModalBottomSheet(
expand: true,
context: context,
backgroundColor: Colors.transparent,
builder: (context, scrollController) =>
ModalInsideModal(
scrollController: scrollController),
)),
ListTile(
title: Text('Avatar Modal'),
onTap: () => showAvatarModalBottomSheet(
expand: true,
context: context,
backgroundColor: Colors.transparent,
builder: (context, scrollController) =>
ModalInsideModal(
scrollController: scrollController),
)),
ListTile(
title: Text('Float Modal'),
onTap: () => showFloatingModalBottomSheet(
context: context,
builder: (context, scrollController) =>
ModalFit(scrollController: scrollController),
)),
ListTile(
title: Text('Cupertino Modal fit'),
onTap: () => showCupertinoModalBottomSheet(
expand: false,
context: context,
backgroundColor: Colors.transparent,
builder: (context, scrollController) =>
ModalFit(scrollController: scrollController),
)),
section('COMPLEX CASES'),
ListTile(
title: Text('Cupertino Small Modal forced to expand'),
onTap: () => showCupertinoModalBottomSheet(
expand: true,
context: context,
backgroundColor: Colors.transparent,
builder: (context, scrollController) =>
ModalFit(scrollController: scrollController),
)),
ListTile(
title: Text('Reverse list'),
onTap: () => showBarModalBottomSheet(
expand: true,
context: context,
backgroundColor: Colors.transparent,
builder: (context, scrollController) =>
ModalInsideModal(
scrollController: scrollController,
reverse: true),
)),
ListTile(
title: Text('Cupertino Modal inside modal'),
onTap: () => showCupertinoModalBottomSheet(
expand: true,
context: context,
backgroundColor: Colors.transparent,
builder: (context, scrollController) =>
ModalInsideModal(
scrollController: scrollController),
)),
ListTile(
title: Text('Cupertino Modal with inside navigation'),
onTap: () => showCupertinoModalBottomSheet(
expand: true,
context: context,
backgroundColor: Colors.transparent,
builder: (context, scrollController) =>
ModalWithNavigator(
scrollController: scrollController),
)),
ListTile(
title:
Text('Cupertino Navigator + Scroll + WillPopScope'),
onTap: () => showCupertinoModalBottomSheet(
expand: true,
context: context,
backgroundColor: Colors.transparent,
builder: (context, scrollController) =>
ComplexModal(
scrollController: scrollController),
)),
ListTile(
title: Text('Modal with WillPopScope'),
onTap: () => showCupertinoModalBottomSheet(
expand: true,
context: context,
backgroundColor: Colors.transparent,
builder: (context, scrollController) =>
ModalWillScope(
scrollController: scrollController),
)),
ListTile(
title: Text('Modal with Nested Scroll'),
onTap: () => showCupertinoModalBottomSheet(
expand: true,
context: context,
builder: (context, scrollController) =>
NestedScrollModal(
scrollController: scrollController),
)),
],
child: SizedBox.expand(
child: SingleChildScrollView(
primary: true,
child: SafeArea(
bottom: false,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTile(
title: Text('Cupertino Photo Share Example'),
onTap: () => Navigator.of(context).push(
MaterialWithModalsPageRoute(
builder: (context) => CupertinoSharePage()))),
section('STYLES'),
ListTile(
title: Text('Material fit'),
onTap: () => showMaterialModalBottomSheet(
expand: false,
context: context,
backgroundColor: Colors.transparent,
builder: (context, scrollController) =>
ModalFit(scrollController: scrollController),
)),
ListTile(
title: Text('Bar Modal'),
onTap: () => showBarModalBottomSheet(
expand: true,
context: context,
backgroundColor: Colors.transparent,
builder: (context, scrollController) =>
ModalInsideModal(
scrollController: scrollController),
)),
ListTile(
title: Text('Avatar Modal'),
onTap: () => showAvatarModalBottomSheet(
expand: true,
context: context,
backgroundColor: Colors.transparent,
builder: (context, scrollController) =>
ModalInsideModal(
scrollController: scrollController),
)),
ListTile(
title: Text('Float Modal'),
onTap: () => showFloatingModalBottomSheet(
context: context,
builder: (context, scrollController) =>
ModalFit(scrollController: scrollController),
)),
ListTile(
title: Text('Cupertino Modal fit'),
onTap: () => showCupertinoModalBottomSheet(
expand: false,
context: context,
backgroundColor: Colors.transparent,
builder: (context, scrollController) =>
ModalFit(scrollController: scrollController),
)),
section('COMPLEX CASES'),
ListTile(
title: Text('Cupertino Small Modal forced to expand'),
onTap: () => showCupertinoModalBottomSheet(
expand: true,
context: context,
backgroundColor: Colors.transparent,
builder: (context, scrollController) =>
ModalFit(scrollController: scrollController),
)),
ListTile(
title: Text('Reverse list'),
onTap: () => showBarModalBottomSheet(
expand: true,
context: context,
backgroundColor: Colors.transparent,
builder: (context, scrollController) =>
ModalInsideModal(
scrollController: scrollController,
reverse: true),
)),
ListTile(
title: Text('Cupertino Modal inside modal'),
onTap: () => showCupertinoModalBottomSheet(
expand: true,
context: context,
backgroundColor: Colors.transparent,
builder: (context, scrollController) =>
ModalInsideModal(
scrollController: scrollController),
)),
ListTile(
title: Text('Cupertino Modal with inside navigation'),
onTap: () => showCupertinoModalBottomSheet(
expand: true,
context: context,
backgroundColor: Colors.transparent,
builder: (context, scrollController) =>
ModalWithNavigator(
scrollController: scrollController),
)),
ListTile(
title:
Text('Cupertino Navigator + Scroll + WillPopScope'),
onTap: () => showCupertinoModalBottomSheet(
expand: true,
context: context,
backgroundColor: Colors.transparent,
builder: (context, scrollController) =>
ComplexModal(
scrollController: scrollController),
)),
ListTile(
title: Text('Modal with WillPopScope'),
onTap: () => showCupertinoModalBottomSheet(
expand: true,
context: context,
backgroundColor: Colors.transparent,
builder: (context, scrollController) =>
ModalWillScope(
scrollController: scrollController),
)),
ListTile(
title: Text('Modal with Nested Scroll'),
onTap: () => showCupertinoModalBottomSheet(
expand: true,
context: context,
builder: (context, scrollController) =>
NestedScrollModal(
scrollController: scrollController),
)),
SizedBox(
height: 60,
)
],
),
),
),
),
... ...
... ... @@ -10,6 +10,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
import 'package:modal_bottom_sheet/src/utils/primary_scroll_status_bar.dart';
const Duration _bottomSheetDuration = Duration(milliseconds: 400);
const double _minFlingVelocity = 500.0;
... ... @@ -249,7 +250,7 @@ class _ModalBottomSheetState extends State<ModalBottomSheet>
if (scrollPosition.axis == Axis.horizontal) return;
//Check if scrollController is used
if (!_scrollController.hasClients) return;
if (!_scrollController.hasClients) return;
final isScrollReversed = scrollPosition.axisDirection == AxisDirection.down;
final offset = isScrollReversed
... ... @@ -334,36 +335,40 @@ class _ModalBottomSheetState extends State<ModalBottomSheet>
curve: widget.animationCurve ?? Curves.linear,
);
return AnimatedBuilder(
animation: widget.animationController,
builder: (context, _) => ClipRect(
child: CustomSingleChildLayout(
delegate: _ModalBottomSheetLayout(
containerAnimation.value, widget.expanded),
child: !widget.enableDrag
? child
: KeyedSubtree(
key: _childKey,
child: AnimatedBuilder(
animation: bounceAnimation,
builder: (context, _) => CustomSingleChildLayout(
delegate: _CustomBottomSheetLayout(bounceAnimation.value),
child: GestureDetector(
onVerticalDragUpdate: (details) =>
_handleDragUpdate(details.primaryDelta),
onVerticalDragEnd: (details) =>
_handleDragEnd(details.primaryVelocity),
child: NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
_handleScrollUpdate(notification);
return false;
},
child: child,
),
return PrimaryScrollStatusBarHandler(
scrollController: _scrollController,
child: AnimatedBuilder(
animation: widget.animationController,
builder: (context, _) => ClipRect(
child: CustomSingleChildLayout(
delegate: _ModalBottomSheetLayout(
containerAnimation.value, widget.expanded),
child: !widget.enableDrag
? child
: KeyedSubtree(
key: _childKey,
child: AnimatedBuilder(
animation: bounceAnimation,
builder: (context, _) => CustomSingleChildLayout(
delegate:
_CustomBottomSheetLayout(bounceAnimation.value),
child: GestureDetector(
onVerticalDragUpdate: (details) =>
_handleDragUpdate(details.primaryDelta),
onVerticalDragEnd: (details) =>
_handleDragEnd(details.primaryVelocity),
child: NotificationListener<ScrollNotification>(
onNotification:
(ScrollNotification notification) {
_handleScrollUpdate(notification);
return false;
},
child: RepaintBoundary(child: child),
)),
),
),
),
),
),
),
),
);
... ...
... ... @@ -99,6 +99,7 @@ class _ModalBottomSheetState<T> extends State<_ModalBottomSheet<T>> {
builder: widget.route.builder,
enableDrag: widget.enableDrag,
bounce: widget.bounce,
scrollController: widget.scrollController,
animationCurve: widget.animationCurve,
),
);
... ...
... ... @@ -10,8 +10,8 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'
show
Colors,
Theme,
MaterialLocalizations,
Theme,
debugCheckHasMaterialLocalizations;
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
... ... @@ -99,31 +99,32 @@ Future<T> showCupertinoModalBottomSheet<T>({
? MaterialLocalizations.of(context).modalBarrierDismissLabel
: '';
final result = await Navigator.of(context, rootNavigator: useRootNavigator)
.push(CupertinoModalBottomSheetRoute<T>(
builder: builder,
containerBuilder: (context, _, child) => _CupertinoBottomSheetContainer(
child: child,
backgroundColor: backgroundColor,
topRadius: topRadius,
),
secondAnimationController: secondAnimation,
expanded: expand,
barrierLabel: barrierLabel,
elevation: elevation,
bounce: bounce,
shape: shape,
clipBehavior: clipBehavior,
isDismissible: isDismissible ?? expand == false ? true : false,
modalBarrierColor: barrierColor ?? Colors.black12,
enableDrag: enableDrag,
topRadius: topRadius,
animationCurve: animationCurve,
previousRouteAnimationCurve: previousRouteAnimationCurve,
duration: duration,
settings: settings,
transitionBackgroundColor: transitionBackgroundColor ?? Colors.black
));
final result =
await Navigator.of(context, rootNavigator: useRootNavigator).push(
CupertinoModalBottomSheetRoute<T>(
builder: builder,
containerBuilder: (context, _, child) => _CupertinoBottomSheetContainer(
child: child,
backgroundColor: backgroundColor,
topRadius: topRadius,
),
secondAnimationController: secondAnimation,
expanded: expand,
barrierLabel: barrierLabel,
elevation: elevation,
bounce: bounce,
shape: shape,
clipBehavior: clipBehavior,
isDismissible: isDismissible ?? expand == false ? true : false,
modalBarrierColor: barrierColor ?? Colors.black12,
enableDrag: enableDrag,
topRadius: topRadius,
animationCurve: animationCurve,
previousRouteAnimationCurve: previousRouteAnimationCurve,
duration: duration,
settings: settings,
transitionBackgroundColor: transitionBackgroundColor ?? Colors.black),
);
return result;
}
... ... @@ -151,6 +152,7 @@ class CupertinoModalBottomSheetRoute<T> extends ModalBottomSheetRoute<T> {
@required bool expanded,
Duration duration,
RouteSettings settings,
ScrollController scrollController,
this.transitionBackgroundColor,
this.topRadius = _default_top_radius,
this.previousRouteAnimationCurve,
... ... @@ -158,6 +160,7 @@ class CupertinoModalBottomSheetRoute<T> extends ModalBottomSheetRoute<T> {
assert(isDismissible != null),
assert(enableDrag != null),
super(
scrollController: scrollController,
containerBuilder: containerBuilder,
builder: builder,
bounce: bounce,
... ... @@ -184,17 +187,18 @@ class CupertinoModalBottomSheetRoute<T> extends ModalBottomSheetRoute<T> {
(paddingTop + _behind_widget_visible_height) * 0.9;
final offsetY = secondaryAnimation.value * (paddingTop - distanceWithScale);
final scale = 1 - secondaryAnimation.value / 10;
return AnimatedBuilder(
builder: (context, child) => Transform.translate(
offset: Offset(0, offsetY),
child: Transform.scale(
scale: scale,
child: child,
alignment: Alignment.topCenter,
return AnimatedBuilder(
builder: (context, child) => Transform.translate(
offset: Offset(0, offsetY),
child: Transform.scale(
scale: scale,
child: child,
alignment: Alignment.topCenter,
),
),
),
child: child,
animation: secondaryAnimation,
child: child,
animation: secondaryAnimation,
);
}
... ... @@ -243,37 +247,39 @@ class _CupertinoModalTransition extends StatelessWidget {
);
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle.light,
child: AnimatedBuilder(
animation: curvedAnimation,
child: body,
builder: (context, child) {
final progress = curvedAnimation.value;
final yOffset = progress * paddingTop;
final scale = 1 - progress / 10;
final radius = progress == 0
? 0.0
: (1 - progress) * startRoundCorner + progress * topRadius.x;
return Stack(
children: <Widget>[
Container(color: backgroundColor),
Transform.translate(
offset: Offset(0, yOffset),
child: Transform.scale(
scale: scale,
alignment: Alignment.topCenter,
child: ClipRRect(
borderRadius: BorderRadius.circular(radius),
child: child),
),
)
],
);
},
));
value: SystemUiOverlayStyle.light,
child: AnimatedBuilder(
animation: curvedAnimation,
child: body,
builder: (context, child) {
final progress = curvedAnimation.value;
final yOffset = progress * paddingTop;
final scale = 1 - progress / 10;
final radius = progress == 0
? 0.0
: (1 - progress) * startRoundCorner + progress * topRadius.x;
return Stack(
children: <Widget>[
Container(color: backgroundColor),
Transform.translate(
offset: Offset(0, yOffset),
child: Transform.scale(
scale: scale,
alignment: Alignment.topCenter,
child: ClipRRect(
borderRadius: BorderRadius.circular(radius),
child: child),
),
),
],
);
},
),
);
}
}
class _CupertinoScaffold extends InheritedWidget {
final AnimationController animation;
... ...
import 'package:flutter/widgets.dart';
/// Creates a primary scroll controller that will
/// scroll to the top when tapped on the status bar
///
class PrimaryScrollStatusBarHandler extends StatefulWidget {
final ScrollController scrollController;
final Widget child;
const PrimaryScrollStatusBarHandler(
{Key key, this.child, this.scrollController})
: super(key: key);
@override
_PrimaryScrollWidgetState createState() => _PrimaryScrollWidgetState();
}
class _PrimaryScrollWidgetState extends State<PrimaryScrollStatusBarHandler> {
ScrollController controller;
@override
void initState() {
controller = widget.scrollController ?? ScrollController();
super.initState();
}
@override
Widget build(BuildContext context) {
return PrimaryScrollController(
controller: controller,
child: Stack(
fit: StackFit.expand,
children: [
widget.child,
Positioned(
top: 0,
left: 0,
right: 0,
height: MediaQuery.of(context).padding.top,
child: Builder(
builder: (context) => GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => _handleStatusBarTap(context),
// iOS accessibility automatically adds scroll-to-top to the clock in the status bar
excludeFromSemantics: true,
),
),
),
],
),
);
}
void _handleStatusBarTap(BuildContext context) {
final controller = PrimaryScrollController.of(context);
if (controller.hasClients) {
controller.animateTo(
0.0,
duration: const Duration(milliseconds: 300),
curve: Curves.linear, // TODO(ianh): Use a more appropriate curve.
);
}
}
}
... ...