Jaime Blasco

Add custom curve when not dragging

@@ -8,6 +8,7 @@ import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; @@ -8,6 +8,7 @@ import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
8 class CupertinoSharePage extends StatelessWidget { 8 class CupertinoSharePage extends StatelessWidget {
9 @override 9 @override
10 Widget build(BuildContext context) { 10 Widget build(BuildContext context) {
  11 +
11 return Scaffold( 12 return Scaffold(
12 appBar: appBar(context), 13 appBar: appBar(context),
13 body: CupertinoPageScaffold( 14 body: CupertinoPageScaffold(
@@ -162,6 +162,7 @@ class _MyHomePageState extends State<MyHomePage> { @@ -162,6 +162,7 @@ class _MyHomePageState extends State<MyHomePage> {
162 title: Text('Cupertino Modal fit'), 162 title: Text('Cupertino Modal fit'),
163 onTap: () => showCupertinoModalBottomSheet( 163 onTap: () => showCupertinoModalBottomSheet(
164 expand: false, 164 expand: false,
  165 +
165 context: context, 166 context: context,
166 backgroundColor: Colors.transparent, 167 backgroundColor: Colors.transparent,
167 builder: (context, scrollController) => 168 builder: (context, scrollController) =>
@@ -173,6 +174,7 @@ class _MyHomePageState extends State<MyHomePage> { @@ -173,6 +174,7 @@ class _MyHomePageState extends State<MyHomePage> {
173 onTap: () => showCupertinoModalBottomSheet( 174 onTap: () => showCupertinoModalBottomSheet(
174 expand: true, 175 expand: true,
175 context: context, 176 context: context,
  177 + animationCurve: Curves.easeInOutCubic,
176 backgroundColor: Colors.transparent, 178 backgroundColor: Colors.transparent,
177 builder: (context, scrollController) => 179 builder: (context, scrollController) =>
178 ModalFit(scrollController: scrollController), 180 ModalFit(scrollController: scrollController),
@@ -9,28 +9,29 @@ class NestedScrollModal extends StatelessWidget { @@ -9,28 +9,29 @@ class NestedScrollModal extends StatelessWidget {
9 @override 9 @override
10 Widget build(BuildContext context) { 10 Widget build(BuildContext context) {
11 return NestedScrollView( 11 return NestedScrollView(
12 -  
13 - physics: ScrollPhysics(parent: PageScrollPhysics()),  
14 - headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {  
15 - return <Widget>[  
16 - SliverList(  
17 - delegate: SliverChildListDelegate(  
18 - [  
19 - Container(height: 300, color: Colors.blue),  
20 - ],  
21 - ), 12 + controller: ScrollController(),
  13 + physics: ScrollPhysics(parent: PageScrollPhysics()),
  14 + headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
  15 + return <Widget>[
  16 + SliverList(
  17 + delegate: SliverChildListDelegate(
  18 + [
  19 + Container(height: 300, color: Colors.blue),
  20 + ],
22 ), 21 ),
23 - ]; 22 + ),
  23 + ];
  24 + },
  25 + body: ListView.builder(
  26 + controller: scrollController,
  27 + itemBuilder: (context, index) {
  28 + return Container(
  29 + height: 100,
  30 + color: index.isOdd ? Colors.green : Colors.orange,
  31 + );
24 }, 32 },
25 - body: ListView.builder(  
26 - controller: scrollController,  
27 - itemBuilder: (context, index) {  
28 - return Container(  
29 - height: 100,  
30 - color: index.isOdd ? Colors.green : Colors.orange,  
31 - );  
32 - },  
33 - itemCount: 12,  
34 - )); 33 + itemCount: 12,
  34 + ),
  35 + );
35 } 36 }
36 } 37 }
@@ -11,6 +11,12 @@ import 'package:flutter/material.dart'; @@ -11,6 +11,12 @@ import 'package:flutter/material.dart';
11 import 'package:flutter/scheduler.dart'; 11 import 'package:flutter/scheduler.dart';
12 import 'package:flutter/widgets.dart'; 12 import 'package:flutter/widgets.dart';
13 13
  14 +import 'package:modal_bottom_sheet/src/utils/bottom_sheet_suspended_curve.dart';
  15 +import 'package:modal_bottom_sheet/src/utils/primary_scroll_status_bar.dart';
  16 +
  17 +
  18 +const Curve _decelerateEasing = Cubic(0.0, 0.0, 0.2, 1.0);
  19 +const Curve _modalBottomSheetCurve = _decelerateEasing;
14 const Duration _bottomSheetDuration = Duration(milliseconds: 400); 20 const Duration _bottomSheetDuration = Duration(milliseconds: 400);
15 const double _minFlingVelocity = 500.0; 21 const double _minFlingVelocity = 500.0;
16 const double _closeProgressThreshold = 0.6; 22 const double _closeProgressThreshold = 0.6;
@@ -61,7 +67,7 @@ class ModalBottomSheet extends StatefulWidget { @@ -61,7 +67,7 @@ class ModalBottomSheet extends StatefulWidget {
61 67
62 /// The curve used by the animation showing and dismissing the bottom sheet. 68 /// The curve used by the animation showing and dismissing the bottom sheet.
63 /// 69 ///
64 - /// If no curve is provided it falls back to `Curves.easeOutSine`. 70 + /// If no curve is provided it falls back to `decelerateEasing`.
65 final Curve animationCurve; 71 final Curve animationCurve;
66 72
67 /// Allows the bottom sheet to go beyond the top bound of the content, 73 /// Allows the bottom sheet to go beyond the top bound of the content,
@@ -176,7 +182,10 @@ class _ModalBottomSheetState extends State<ModalBottomSheet> @@ -176,7 +182,10 @@ class _ModalBottomSheetState extends State<ModalBottomSheet>
176 return result; 182 return result;
177 } 183 }
178 184
  185 + ParametricCurve<double> animationCurve;
  186 +
179 void _handleDragUpdate(double primaryDelta) async { 187 void _handleDragUpdate(double primaryDelta) async {
  188 + animationCurve = Curves.linear;
180 assert(widget.enableDrag, 'Dragging is disabled'); 189 assert(widget.enableDrag, 'Dragging is disabled');
181 190
182 if (_dismissUnderway) return; 191 if (_dismissUnderway) return;
@@ -210,6 +219,11 @@ class _ModalBottomSheetState extends State<ModalBottomSheet> @@ -210,6 +219,11 @@ class _ModalBottomSheetState extends State<ModalBottomSheet>
210 void _handleDragEnd(double velocity) async { 219 void _handleDragEnd(double velocity) async {
211 assert(widget.enableDrag, 'Dragging is disabled'); 220 assert(widget.enableDrag, 'Dragging is disabled');
212 221
  222 + animationCurve = BottomSheetSuspendedCurve(
  223 + widget.animationController.value,
  224 + curve: _defaultCurve,
  225 + );
  226 +
213 if (_dismissUnderway || !isDragging) return; 227 if (_dismissUnderway || !isDragging) return;
214 isDragging = false; 228 isDragging = false;
215 _bounceDragController.reverse(); 229 _bounceDragController.reverse();
@@ -302,8 +316,12 @@ class _ModalBottomSheetState extends State<ModalBottomSheet> @@ -302,8 +316,12 @@ class _ModalBottomSheetState extends State<ModalBottomSheet>
302 } 316 }
303 } 317 }
304 318
  319 + ParametricCurve<double> get _defaultCurve =>
  320 + widget.animationCurve ?? _modalBottomSheetCurve;
  321 +
305 @override 322 @override
306 void initState() { 323 void initState() {
  324 + animationCurve = _defaultCurve;
307 _bounceDragController = 325 _bounceDragController =
308 AnimationController(vsync: this, duration: Duration(milliseconds: 300)); 326 AnimationController(vsync: this, duration: Duration(milliseconds: 300));
309 _scrollController = widget.scrollController ?? ScrollController(); 327 _scrollController = widget.scrollController ?? ScrollController();
@@ -328,45 +346,57 @@ class _ModalBottomSheetState extends State<ModalBottomSheet> @@ -328,45 +346,57 @@ class _ModalBottomSheetState extends State<ModalBottomSheet>
328 ); 346 );
329 } 347 }
330 348
331 - // Todo: Add curved Animation when push and pop without gesture  
332 - final Animation<double> containerAnimation = CurvedAnimation(  
333 - parent: widget.animationController,  
334 - curve: widget.animationCurve ?? Curves.linear,  
335 - ); 349 + final mediaQuery = MediaQuery.of(context);
336 350
337 - return AnimatedBuilder( 351 + child = AnimatedBuilder(
338 animation: widget.animationController, 352 animation: widget.animationController,
339 - builder: (context, _) => ClipRect(  
340 - child: CustomSingleChildLayout(  
341 - delegate: _ModalBottomSheetLayout(  
342 - containerAnimation.value, widget.expanded),  
343 - child: !widget.enableDrag  
344 - ? child  
345 - : KeyedSubtree(  
346 - key: _childKey,  
347 - child: AnimatedBuilder(  
348 - animation: bounceAnimation,  
349 - builder: (context, _) => CustomSingleChildLayout(  
350 - delegate: _CustomBottomSheetLayout(bounceAnimation.value),  
351 - child: GestureDetector(  
352 - onVerticalDragUpdate: (details) =>  
353 - _handleDragUpdate(details.primaryDelta),  
354 - onVerticalDragEnd: (details) =>  
355 - _handleDragEnd(details.primaryVelocity),  
356 - child: NotificationListener<ScrollNotification>(  
357 - onNotification: (ScrollNotification notification) {  
358 - _handleScrollUpdate(notification);  
359 - return false;  
360 - },  
361 - child: child,  
362 - ), 353 + builder: (context, child) {
  354 + final animationValue = animationCurve.transform(
  355 + mediaQuery.accessibleNavigation
  356 + ? 1.0
  357 + : widget.animationController.value);
  358 +
  359 + final draggableChild = !widget.enableDrag
  360 + ? child
  361 + : KeyedSubtree(
  362 + key: _childKey,
  363 + child: AnimatedBuilder(
  364 + animation: bounceAnimation,
  365 + builder: (context, _) => CustomSingleChildLayout(
  366 + delegate: _CustomBottomSheetLayout(bounceAnimation.value),
  367 + child: GestureDetector(
  368 + onVerticalDragUpdate: (details) {
  369 + _handleDragUpdate(details.primaryDelta);
  370 + },
  371 + onVerticalDragEnd: (details) {
  372 + _handleDragEnd(details.primaryVelocity);
  373 + },
  374 + child: NotificationListener<ScrollNotification>(
  375 + onNotification: (ScrollNotification notification) {
  376 + _handleScrollUpdate(notification);
  377 + return false;
  378 + },
  379 + child: child,
363 ), 380 ),
364 ), 381 ),
365 ), 382 ),
366 ), 383 ),
367 - ),  
368 - ), 384 + );
  385 + return ClipRect(
  386 + child: CustomSingleChildLayout(
  387 + delegate: _ModalBottomSheetLayout(
  388 + animationValue,
  389 + widget.expanded,
  390 + ),
  391 + child: draggableChild,
  392 + ),
  393 + );
  394 + },
  395 + child: RepaintBoundary(child: child),
369 ); 396 );
  397 +
  398 + return PrimaryScrollStatusBarHandler(
  399 + scrollController: _scrollController, child: child);
370 } 400 }
371 } 401 }
372 402
  1 +import 'dart:ui';
  2 +
  3 +import 'package:flutter/animation.dart';
  4 +import 'package:flutter/foundation.dart';
  5 +
  6 +// Copied from bottom_sheet.dart as is a private class
  7 +// https://github.com/flutter/flutter/issues/51627
  8 +
  9 +// TODO(guidezpl): Look into making this public. A copy of this class is in
  10 +// scaffold.dart, for now, https://github.com/flutter/flutter/issues/51627
  11 +
  12 +/// A curve that progresses linearly until a specified [startingPoint], at which
  13 +/// point [curve] will begin. Unlike [Interval], [curve] will not start at zero,
  14 +/// but will use [startingPoint] as the Y position.
  15 +///
  16 +/// For example, if [startingPoint] is set to `0.5`, and [curve] is set to
  17 +/// [Curves.easeOut], then the bottom-left quarter of the curve will be a
  18 +/// straight line, and the top-right quarter will contain the entire contents of
  19 +/// [Curves.easeOut].
  20 +///
  21 +/// This is useful in situations where a widget must track the user's finger
  22 +/// (which requires a linear animation), and afterwards can be flung using a
  23 +/// curve specified with the [curve] argument, after the finger is released. In
  24 +/// such a case, the value of [startingPoint] would be the progress of the
  25 +/// animation at the time when the finger was released.
  26 +///
  27 +/// The [startingPoint] and [curve] arguments must not be null.
  28 +class BottomSheetSuspendedCurve extends ParametricCurve<double> {
  29 + /// Creates a suspended curve.
  30 + const BottomSheetSuspendedCurve(
  31 + this.startingPoint, {
  32 + this.curve = Curves.easeOutCubic,
  33 + }) : assert(startingPoint != null),
  34 + assert(curve != null);
  35 +
  36 + /// The progress value at which [curve] should begin.
  37 + ///
  38 + /// This defaults to [Curves.easeOutCubic].
  39 + final double startingPoint;
  40 +
  41 + /// The curve to use when [startingPoint] is reached.
  42 + final Curve curve;
  43 +
  44 + @override
  45 + double transform(double t) {
  46 + assert(t >= 0.0 && t <= 1.0);
  47 + assert(startingPoint >= 0.0 && startingPoint <= 1.0);
  48 +
  49 + if (t < startingPoint) {
  50 + return t;
  51 + }
  52 +
  53 + if (t == 1.0) {
  54 + return t;
  55 + }
  56 +
  57 + final double curveProgress = (t - startingPoint) / (1 - startingPoint);
  58 + final double transformed = curve.transform(curveProgress);
  59 + return lerpDouble(startingPoint, 1, transformed);
  60 + }
  61 +
  62 + @override
  63 + String toString() {
  64 + return '${describeIdentity(this)}($startingPoint, $curve)';
  65 + }
  66 +}