mirror of
https://github.com/flutter/flutter
synced 2024-10-13 19:52:53 +00:00
Material Bottom Sheet Reveal/Dismiss animation uses a curved animation (#51122)
This commit is contained in:
parent
f3018c378a
commit
ec64f93fdd
|
@ -1657,6 +1657,10 @@ class Curves {
|
||||||
/// animation to finish, and the negative effects of motion are minimized.
|
/// animation to finish, and the negative effects of motion are minimized.
|
||||||
///
|
///
|
||||||
/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_fast_out_slow_in.mp4}
|
/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_fast_out_slow_in.mp4}
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [standardEasing], the name for this curve in the Material specification.
|
||||||
static const Cubic fastOutSlowIn = Cubic(0.4, 0.0, 0.2, 1.0);
|
static const Cubic fastOutSlowIn = Cubic(0.4, 0.0, 0.2, 1.0);
|
||||||
|
|
||||||
/// A cubic animation curve that starts quickly, slows down, and then ends
|
/// A cubic animation curve that starts quickly, slows down, and then ends
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:ui' show lerpDouble;
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
|
@ -10,16 +11,25 @@ import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
import 'bottom_sheet_theme.dart';
|
import 'bottom_sheet_theme.dart';
|
||||||
import 'colors.dart';
|
import 'colors.dart';
|
||||||
|
import 'curves.dart';
|
||||||
import 'debug.dart';
|
import 'debug.dart';
|
||||||
import 'material.dart';
|
import 'material.dart';
|
||||||
import 'material_localizations.dart';
|
import 'material_localizations.dart';
|
||||||
import 'scaffold.dart';
|
import 'scaffold.dart';
|
||||||
import 'theme.dart';
|
import 'theme.dart';
|
||||||
|
|
||||||
const Duration _bottomSheetDuration = Duration(milliseconds: 200);
|
const Duration _bottomSheetEnterDuration = Duration(milliseconds: 250);
|
||||||
|
const Duration _bottomSheetExitDuration = Duration(milliseconds: 200);
|
||||||
|
const Curve _modalBottomSheetCurve = decelerateEasing;
|
||||||
const double _minFlingVelocity = 700.0;
|
const double _minFlingVelocity = 700.0;
|
||||||
const double _closeProgressThreshold = 0.5;
|
const double _closeProgressThreshold = 0.5;
|
||||||
|
|
||||||
|
typedef BottomSheetDragStartHandler = void Function(DragStartDetails details);
|
||||||
|
typedef BottomSheetDragEndHandler = void Function(
|
||||||
|
DragEndDetails details, {
|
||||||
|
bool isClosing,
|
||||||
|
});
|
||||||
|
|
||||||
/// A material design bottom sheet.
|
/// A material design bottom sheet.
|
||||||
///
|
///
|
||||||
/// There are two kinds of bottom sheets in material design:
|
/// There are two kinds of bottom sheets in material design:
|
||||||
|
@ -57,6 +67,8 @@ class BottomSheet extends StatefulWidget {
|
||||||
Key key,
|
Key key,
|
||||||
this.animationController,
|
this.animationController,
|
||||||
this.enableDrag = true,
|
this.enableDrag = true,
|
||||||
|
this.onDragStart,
|
||||||
|
this.onDragEnd,
|
||||||
this.backgroundColor,
|
this.backgroundColor,
|
||||||
this.elevation,
|
this.elevation,
|
||||||
this.shape,
|
this.shape,
|
||||||
|
@ -95,6 +107,21 @@ class BottomSheet extends StatefulWidget {
|
||||||
/// Default is true.
|
/// Default is true.
|
||||||
final bool enableDrag;
|
final bool enableDrag;
|
||||||
|
|
||||||
|
/// Called when the user begins dragging the bottom sheet vertically, if
|
||||||
|
/// [enableDrag] is true.
|
||||||
|
///
|
||||||
|
/// Would typically be used to change the bottom sheet animation curve so
|
||||||
|
/// that it tracks the user's finger accurately.
|
||||||
|
final BottomSheetDragStartHandler onDragStart;
|
||||||
|
|
||||||
|
/// Called when the user stops dragging the bottom sheet, if [enableDrag]
|
||||||
|
/// is true.
|
||||||
|
///
|
||||||
|
/// Would typically be used to reset the bottom sheet animation curve, so
|
||||||
|
/// that it animates non-linearly. Called before [onClosing] if the bottom
|
||||||
|
/// sheet is closing.
|
||||||
|
final BottomSheetDragEndHandler onDragEnd;
|
||||||
|
|
||||||
/// The bottom sheet's background color.
|
/// The bottom sheet's background color.
|
||||||
///
|
///
|
||||||
/// Defines the bottom sheet's [Material.color].
|
/// Defines the bottom sheet's [Material.color].
|
||||||
|
@ -140,7 +167,8 @@ class BottomSheet extends StatefulWidget {
|
||||||
/// animation controller could be provided.
|
/// animation controller could be provided.
|
||||||
static AnimationController createAnimationController(TickerProvider vsync) {
|
static AnimationController createAnimationController(TickerProvider vsync) {
|
||||||
return AnimationController(
|
return AnimationController(
|
||||||
duration: _bottomSheetDuration,
|
duration: _bottomSheetEnterDuration,
|
||||||
|
reverseDuration: _bottomSheetExitDuration,
|
||||||
debugLabel: 'BottomSheet',
|
debugLabel: 'BottomSheet',
|
||||||
vsync: vsync,
|
vsync: vsync,
|
||||||
);
|
);
|
||||||
|
@ -158,6 +186,12 @@ class _BottomSheetState extends State<BottomSheet> {
|
||||||
|
|
||||||
bool get _dismissUnderway => widget.animationController.status == AnimationStatus.reverse;
|
bool get _dismissUnderway => widget.animationController.status == AnimationStatus.reverse;
|
||||||
|
|
||||||
|
void _handleDragStart(DragStartDetails details) {
|
||||||
|
if (widget.onDragStart != null) {
|
||||||
|
widget.onDragStart(details);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _handleDragUpdate(DragUpdateDetails details) {
|
void _handleDragUpdate(DragUpdateDetails details) {
|
||||||
assert(widget.enableDrag);
|
assert(widget.enableDrag);
|
||||||
if (_dismissUnderway)
|
if (_dismissUnderway)
|
||||||
|
@ -169,21 +203,33 @@ class _BottomSheetState extends State<BottomSheet> {
|
||||||
assert(widget.enableDrag);
|
assert(widget.enableDrag);
|
||||||
if (_dismissUnderway)
|
if (_dismissUnderway)
|
||||||
return;
|
return;
|
||||||
|
bool isClosing = false;
|
||||||
if (details.velocity.pixelsPerSecond.dy > _minFlingVelocity) {
|
if (details.velocity.pixelsPerSecond.dy > _minFlingVelocity) {
|
||||||
final double flingVelocity = -details.velocity.pixelsPerSecond.dy / _childHeight;
|
final double flingVelocity = -details.velocity.pixelsPerSecond.dy / _childHeight;
|
||||||
if (widget.animationController.value > 0.0) {
|
if (widget.animationController.value > 0.0) {
|
||||||
widget.animationController.fling(velocity: flingVelocity);
|
widget.animationController.fling(velocity: flingVelocity);
|
||||||
}
|
}
|
||||||
if (flingVelocity < 0.0) {
|
if (flingVelocity < 0.0) {
|
||||||
widget.onClosing();
|
isClosing = true;
|
||||||
}
|
}
|
||||||
} else if (widget.animationController.value < _closeProgressThreshold) {
|
} else if (widget.animationController.value < _closeProgressThreshold) {
|
||||||
if (widget.animationController.value > 0.0)
|
if (widget.animationController.value > 0.0)
|
||||||
widget.animationController.fling(velocity: -1.0);
|
widget.animationController.fling(velocity: -1.0);
|
||||||
widget.onClosing();
|
isClosing = true;
|
||||||
} else {
|
} else {
|
||||||
widget.animationController.forward();
|
widget.animationController.forward();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (widget.onDragEnd != null) {
|
||||||
|
widget.onDragEnd(
|
||||||
|
details,
|
||||||
|
isClosing: isClosing,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isClosing) {
|
||||||
|
widget.onClosing();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool extentChanged(DraggableScrollableNotification notification) {
|
bool extentChanged(DraggableScrollableNotification notification) {
|
||||||
|
@ -213,6 +259,7 @@ class _BottomSheetState extends State<BottomSheet> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return !widget.enableDrag ? bottomSheet : GestureDetector(
|
return !widget.enableDrag ? bottomSheet : GestureDetector(
|
||||||
|
onVerticalDragStart: _handleDragStart,
|
||||||
onVerticalDragUpdate: _handleDragUpdate,
|
onVerticalDragUpdate: _handleDragUpdate,
|
||||||
onVerticalDragEnd: _handleDragEnd,
|
onVerticalDragEnd: _handleDragEnd,
|
||||||
child: bottomSheet,
|
child: bottomSheet,
|
||||||
|
@ -283,6 +330,8 @@ class _ModalBottomSheet<T> extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ModalBottomSheetState<T> extends State<_ModalBottomSheet<T>> {
|
class _ModalBottomSheetState<T> extends State<_ModalBottomSheet<T>> {
|
||||||
|
ParametricCurve<double> animationCurve = _modalBottomSheetCurve;
|
||||||
|
|
||||||
String _getRouteLabel(MaterialLocalizations localizations) {
|
String _getRouteLabel(MaterialLocalizations localizations) {
|
||||||
switch (Theme.of(context).platform) {
|
switch (Theme.of(context).platform) {
|
||||||
case TargetPlatform.iOS:
|
case TargetPlatform.iOS:
|
||||||
|
@ -295,6 +344,19 @@ class _ModalBottomSheetState<T> extends State<_ModalBottomSheet<T>> {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void handleDragStart(DragStartDetails details) {
|
||||||
|
// Allow the bottom sheet to track the user's finger accurately.
|
||||||
|
animationCurve = Curves.linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleDragEnd(DragEndDetails details, {bool isClosing}) {
|
||||||
|
// Allow the bottom sheet to animate smoothly from its current position.
|
||||||
|
animationCurve = _BottomSheetSuspendedCurve(
|
||||||
|
widget.route.animation.value,
|
||||||
|
curve: _modalBottomSheetCurve,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
assert(debugCheckHasMediaQuery(context));
|
assert(debugCheckHasMediaQuery(context));
|
||||||
|
@ -308,7 +370,9 @@ class _ModalBottomSheetState<T> extends State<_ModalBottomSheet<T>> {
|
||||||
builder: (BuildContext context, Widget child) {
|
builder: (BuildContext context, Widget child) {
|
||||||
// Disable the initial animation when accessible navigation is on so
|
// Disable the initial animation when accessible navigation is on so
|
||||||
// that the semantics are added to the tree at the correct time.
|
// that the semantics are added to the tree at the correct time.
|
||||||
final double animationValue = mediaQuery.accessibleNavigation ? 1.0 : widget.route.animation.value;
|
final double animationValue = animationCurve.transform(
|
||||||
|
mediaQuery.accessibleNavigation ? 1.0 : widget.route.animation.value
|
||||||
|
);
|
||||||
return Semantics(
|
return Semantics(
|
||||||
scopesRoute: true,
|
scopesRoute: true,
|
||||||
namesRoute: true,
|
namesRoute: true,
|
||||||
|
@ -330,6 +394,8 @@ class _ModalBottomSheetState<T> extends State<_ModalBottomSheet<T>> {
|
||||||
shape: widget.shape,
|
shape: widget.shape,
|
||||||
clipBehavior: widget.clipBehavior,
|
clipBehavior: widget.clipBehavior,
|
||||||
enableDrag: widget.enableDrag,
|
enableDrag: widget.enableDrag,
|
||||||
|
onDragStart: handleDragStart,
|
||||||
|
onDragEnd: handleDragEnd,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -370,7 +436,10 @@ class _ModalBottomSheetRoute<T> extends PopupRoute<T> {
|
||||||
final bool enableDrag;
|
final bool enableDrag;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Duration get transitionDuration => _bottomSheetDuration;
|
Duration get transitionDuration => _bottomSheetEnterDuration;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Duration get reverseTransitionDuration => _bottomSheetExitDuration;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get barrierDismissible => isDismissible;
|
bool get barrierDismissible => isDismissible;
|
||||||
|
@ -383,7 +452,6 @@ class _ModalBottomSheetRoute<T> extends PopupRoute<T> {
|
||||||
|
|
||||||
AnimationController _animationController;
|
AnimationController _animationController;
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
AnimationController createAnimationController() {
|
AnimationController createAnimationController() {
|
||||||
assert(_animationController == null);
|
assert(_animationController == null);
|
||||||
|
@ -415,6 +483,63 @@ class _ModalBottomSheetRoute<T> extends PopupRoute<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(guidezpl): Look into making this public. A copy of this class is in scaffold.dart, for now.
|
||||||
|
/// A curve that progresses linearly until a specified [startingPoint], at which
|
||||||
|
/// point [curve] will begin. Unlike [Interval], [curve] will not start at zero,
|
||||||
|
/// but will use [startingPoint] as the Y position.
|
||||||
|
///
|
||||||
|
/// For example, if [startingPoint] is set to `0.5`, and [curve] is set to
|
||||||
|
/// [Curves.easeOut], then the bottom-left quarter of the curve will be a
|
||||||
|
/// straight line, and the top-right quarter will contain the entire contents of
|
||||||
|
/// [Curves.easeOut].
|
||||||
|
///
|
||||||
|
/// This is useful in situations where a widget must track the user's finger
|
||||||
|
/// (which requires a linear animation), and afterwards can be flung using a
|
||||||
|
/// curve specified with the [curve] argument, after the finger is released. In
|
||||||
|
/// such a case, the value of [startingPoint] would be the progress of the
|
||||||
|
/// animation at the time when the finger was released.
|
||||||
|
///
|
||||||
|
/// The [startingPoint] and [curve] arguments must not be null.
|
||||||
|
class _BottomSheetSuspendedCurve extends ParametricCurve<double> {
|
||||||
|
/// Creates a suspended curve.
|
||||||
|
const _BottomSheetSuspendedCurve(
|
||||||
|
this.startingPoint, {
|
||||||
|
this.curve = Curves.easeOutCubic,
|
||||||
|
}) : assert(startingPoint != null),
|
||||||
|
assert(curve != null);
|
||||||
|
|
||||||
|
/// The progress value at which [curve] should begin.
|
||||||
|
///
|
||||||
|
/// This defaults to [Curves.easeOutCubic].
|
||||||
|
final double startingPoint;
|
||||||
|
|
||||||
|
/// The curve to use when [startingPoint] is reached.
|
||||||
|
final Curve curve;
|
||||||
|
|
||||||
|
@override
|
||||||
|
double transform(double t) {
|
||||||
|
assert(t >= 0.0 && t <= 1.0);
|
||||||
|
assert(startingPoint >= 0.0 && startingPoint <= 1.0);
|
||||||
|
|
||||||
|
if (t < startingPoint) {
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (t == 1.0) {
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
final double curveProgress = (t - startingPoint) / (1 - startingPoint);
|
||||||
|
final double transformed = curve.transform(curveProgress);
|
||||||
|
return lerpDouble(startingPoint, 1, transformed);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return '${describeIdentity(this)}($startingPoint, $curve)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Shows a modal material design bottom sheet.
|
/// Shows a modal material design bottom sheet.
|
||||||
///
|
///
|
||||||
/// A modal bottom sheet is an alternative to a menu or a dialog and prevents
|
/// A modal bottom sheet is an alternative to a menu or a dialog and prevents
|
||||||
|
@ -446,7 +571,7 @@ class _ModalBottomSheetRoute<T> extends PopupRoute<T> {
|
||||||
/// dismissed when user taps on the scrim.
|
/// dismissed when user taps on the scrim.
|
||||||
///
|
///
|
||||||
/// The [enableDrag] parameter specifies whether the bottom sheet can be
|
/// The [enableDrag] parameter specifies whether the bottom sheet can be
|
||||||
/// dragged up and down and dismissed by swiping downards.
|
/// dragged up and down and dismissed by swiping downwards.
|
||||||
///
|
///
|
||||||
/// The optional [backgroundColor], [elevation], [shape], and [clipBehavior]
|
/// The optional [backgroundColor], [elevation], [shape], and [clipBehavior]
|
||||||
/// parameters can be passed in to customize the appearance and behavior of
|
/// parameters can be passed in to customize the appearance and behavior of
|
||||||
|
|
36
packages/flutter/lib/src/material/curves.dart
Normal file
36
packages/flutter/lib/src/material/curves.dart
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
// Copyright 2014 The Flutter 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/animation.dart';
|
||||||
|
|
||||||
|
// The easing curves of the Material Library
|
||||||
|
|
||||||
|
/// The standard easing curve in the Material specification.
|
||||||
|
///
|
||||||
|
/// Elements that begin and end at rest use standard easing.
|
||||||
|
/// They speed up quickly and slow down gradually, in order
|
||||||
|
/// to emphasize the end of the transition.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
/// * <https://material.io/design/motion/speed.html#easing>
|
||||||
|
const Curve standardEasing = Curves.fastOutSlowIn;
|
||||||
|
|
||||||
|
/// The accelerate easing curve in the Material specification.
|
||||||
|
///
|
||||||
|
/// Elements exiting a screen use acceleration easing,
|
||||||
|
/// where they start at rest and end at peak velocity.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
/// * <https://material.io/design/motion/speed.html#easing>
|
||||||
|
const Curve accelerateEasing = Cubic(0.4, 0.0, 1.0, 1.0);
|
||||||
|
|
||||||
|
/// The decelerate easing curve in the Material specification.
|
||||||
|
///
|
||||||
|
/// Incoming elements are animated using deceleration easing,
|
||||||
|
/// which starts a transition at peak velocity (the fastest
|
||||||
|
/// point of an element’s movement) and ends at rest.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
/// * <https://material.io/design/motion/speed.html#easing>
|
||||||
|
const Curve decelerateEasing = Cubic(0.0, 0.0, 0.2, 1.0);
|
|
@ -9,6 +9,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
import 'dart:ui' show lerpDouble;
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
|
@ -19,6 +20,7 @@ import 'app_bar.dart';
|
||||||
import 'bottom_sheet.dart';
|
import 'bottom_sheet.dart';
|
||||||
import 'button_bar.dart';
|
import 'button_bar.dart';
|
||||||
import 'colors.dart';
|
import 'colors.dart';
|
||||||
|
import 'curves.dart';
|
||||||
import 'divider.dart';
|
import 'divider.dart';
|
||||||
import 'drawer.dart';
|
import 'drawer.dart';
|
||||||
import 'flexible_space_bar.dart';
|
import 'flexible_space_bar.dart';
|
||||||
|
@ -40,6 +42,7 @@ import 'theme_data.dart';
|
||||||
const FloatingActionButtonLocation _kDefaultFloatingActionButtonLocation = FloatingActionButtonLocation.endFloat;
|
const FloatingActionButtonLocation _kDefaultFloatingActionButtonLocation = FloatingActionButtonLocation.endFloat;
|
||||||
const FloatingActionButtonAnimator _kDefaultFloatingActionButtonAnimator = FloatingActionButtonAnimator.scaling;
|
const FloatingActionButtonAnimator _kDefaultFloatingActionButtonAnimator = FloatingActionButtonAnimator.scaling;
|
||||||
|
|
||||||
|
const Curve _standardBottomSheetCurve = standardEasing;
|
||||||
// When the top of the BottomSheet crosses this threshold, it will start to
|
// When the top of the BottomSheet crosses this threshold, it will start to
|
||||||
// shrink the FAB and show a scrim.
|
// shrink the FAB and show a scrim.
|
||||||
const double _kBottomSheetDominatesPercentage = 0.3;
|
const double _kBottomSheetDominatesPercentage = 0.3;
|
||||||
|
@ -2562,6 +2565,63 @@ class ScaffoldFeatureController<T extends Widget, U> {
|
||||||
final StateSetter setState;
|
final StateSetter setState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(guidezpl): Look into making this public. A copy of this class is in bottom_sheet.dart, for now.
|
||||||
|
/// A curve that progresses linearly until a specified [startingPoint], at which
|
||||||
|
/// point [curve] will begin. Unlike [Interval], [curve] will not start at zero,
|
||||||
|
/// but will use [startingPoint] as the Y position.
|
||||||
|
///
|
||||||
|
/// For example, if [startingPoint] is set to `0.5`, and [curve] is set to
|
||||||
|
/// [Curves.easeOut], then the bottom-left quarter of the curve will be a
|
||||||
|
/// straight line, and the top-right quarter will contain the entire contents of
|
||||||
|
/// [Curves.easeOut].
|
||||||
|
///
|
||||||
|
/// This is useful in situations where a widget must track the user's finger
|
||||||
|
/// (which requires a linear animation), and afterwards can be flung using a
|
||||||
|
/// curve specified with the [curve] argument, after the finger is released. In
|
||||||
|
/// such a case, the value of [startingPoint] would be the progress of the
|
||||||
|
/// animation at the time when the finger was released.
|
||||||
|
///
|
||||||
|
/// The [startingPoint] and [curve] arguments must not be null.
|
||||||
|
class _BottomSheetSuspendedCurve extends ParametricCurve<double> {
|
||||||
|
/// Creates a suspended curve.
|
||||||
|
const _BottomSheetSuspendedCurve(
|
||||||
|
this.startingPoint, {
|
||||||
|
this.curve = Curves.easeOutCubic,
|
||||||
|
}) : assert(startingPoint != null),
|
||||||
|
assert(curve != null);
|
||||||
|
|
||||||
|
/// The progress value at which [curve] should begin.
|
||||||
|
///
|
||||||
|
/// This defaults to [Curves.easeOutCubic].
|
||||||
|
final double startingPoint;
|
||||||
|
|
||||||
|
/// The curve to use when [startingPoint] is reached.
|
||||||
|
final Curve curve;
|
||||||
|
|
||||||
|
@override
|
||||||
|
double transform(double t) {
|
||||||
|
assert(t >= 0.0 && t <= 1.0);
|
||||||
|
assert(startingPoint >= 0.0 && startingPoint <= 1.0);
|
||||||
|
|
||||||
|
if (t < startingPoint) {
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (t == 1.0) {
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
final double curveProgress = (t - startingPoint) / (1 - startingPoint);
|
||||||
|
final double transformed = curve.transform(curveProgress);
|
||||||
|
return lerpDouble(startingPoint, 1, transformed);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return '${describeIdentity(this)}($startingPoint, $curve)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _StandardBottomSheet extends StatefulWidget {
|
class _StandardBottomSheet extends StatefulWidget {
|
||||||
const _StandardBottomSheet({
|
const _StandardBottomSheet({
|
||||||
Key key,
|
Key key,
|
||||||
|
@ -2593,6 +2653,8 @@ class _StandardBottomSheet extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _StandardBottomSheetState extends State<_StandardBottomSheet> {
|
class _StandardBottomSheetState extends State<_StandardBottomSheet> {
|
||||||
|
ParametricCurve<double> animationCurve = _standardBottomSheetCurve;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
@ -2617,6 +2679,19 @@ class _StandardBottomSheetState extends State<_StandardBottomSheet> {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleDragStart(DragStartDetails details) {
|
||||||
|
// Allow the bottom sheet to track the user's finger accurately.
|
||||||
|
animationCurve = Curves.linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleDragEnd(DragEndDetails details, { bool isClosing }) {
|
||||||
|
// Allow the bottom sheet to animate smoothly from its current position.
|
||||||
|
animationCurve = _BottomSheetSuspendedCurve(
|
||||||
|
widget.animationController.value,
|
||||||
|
curve: _standardBottomSheetCurve,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _handleStatusChange(AnimationStatus status) {
|
void _handleStatusChange(AnimationStatus status) {
|
||||||
if (status == AnimationStatus.dismissed && widget.onDismissed != null) {
|
if (status == AnimationStatus.dismissed && widget.onDismissed != null) {
|
||||||
widget.onDismissed();
|
widget.onDismissed();
|
||||||
|
@ -2662,7 +2737,7 @@ class _StandardBottomSheetState extends State<_StandardBottomSheet> {
|
||||||
builder: (BuildContext context, Widget child) {
|
builder: (BuildContext context, Widget child) {
|
||||||
return Align(
|
return Align(
|
||||||
alignment: AlignmentDirectional.topStart,
|
alignment: AlignmentDirectional.topStart,
|
||||||
heightFactor: widget.animationController.value,
|
heightFactor: animationCurve.transform(widget.animationController.value),
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -2670,6 +2745,8 @@ class _StandardBottomSheetState extends State<_StandardBottomSheet> {
|
||||||
BottomSheet(
|
BottomSheet(
|
||||||
animationController: widget.animationController,
|
animationController: widget.animationController,
|
||||||
enableDrag: widget.enableDrag,
|
enableDrag: widget.enableDrag,
|
||||||
|
onDragStart: _handleDragStart,
|
||||||
|
onDragEnd: _handleDragEnd,
|
||||||
onClosing: widget.onClosing,
|
onClosing: widget.onClosing,
|
||||||
builder: widget.builder,
|
builder: widget.builder,
|
||||||
backgroundColor: widget.backgroundColor,
|
backgroundColor: widget.backgroundColor,
|
||||||
|
|
|
@ -10,6 +10,21 @@ import 'package:flutter/gestures.dart';
|
||||||
import '../widgets/semantics_tester.dart';
|
import '../widgets/semantics_tester.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
// Pumps and ensures that the BottomSheet animates non-linearly.
|
||||||
|
Future<void> _checkNonLinearAnimation(WidgetTester tester) async {
|
||||||
|
final Offset firstPosition = tester.getCenter(find.text('BottomSheet'));
|
||||||
|
await tester.pump(const Duration(milliseconds: 30));
|
||||||
|
final Offset secondPosition = tester.getCenter(find.text('BottomSheet'));
|
||||||
|
await tester.pump(const Duration(milliseconds: 30));
|
||||||
|
final Offset thirdPosition = tester.getCenter(find.text('BottomSheet'));
|
||||||
|
|
||||||
|
final double dyDelta1 = secondPosition.dy - firstPosition.dy;
|
||||||
|
final double dyDelta2 = thirdPosition.dy - secondPosition.dy;
|
||||||
|
|
||||||
|
// If the animation were linear, these two values would be the same.
|
||||||
|
expect(dyDelta1, isNot(closeTo(dyDelta2, 0.1)));
|
||||||
|
}
|
||||||
|
|
||||||
testWidgets('Tapping on a modal BottomSheet should not dismiss it', (WidgetTester tester) async {
|
testWidgets('Tapping on a modal BottomSheet should not dismiss it', (WidgetTester tester) async {
|
||||||
BuildContext savedContext;
|
BuildContext savedContext;
|
||||||
|
|
||||||
|
@ -115,6 +130,38 @@ void main() {
|
||||||
expect(find.text('BottomSheet'), findsNothing);
|
expect(find.text('BottomSheet'), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('Verify that the BottomSheet animates non-linearly', (WidgetTester tester) async {
|
||||||
|
BuildContext savedContext;
|
||||||
|
|
||||||
|
await tester.pumpWidget(MaterialApp(
|
||||||
|
home: Builder(
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
savedContext = context;
|
||||||
|
return Container();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('BottomSheet'), findsNothing);
|
||||||
|
|
||||||
|
showModalBottomSheet<void>(
|
||||||
|
context: savedContext,
|
||||||
|
builder: (BuildContext context) => const Text('BottomSheet'),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await _checkNonLinearAnimation(tester);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Tap above the bottom sheet to dismiss it.
|
||||||
|
await tester.tapAt(const Offset(20.0, 20.0));
|
||||||
|
await tester.pump();
|
||||||
|
await _checkNonLinearAnimation(tester);
|
||||||
|
await tester.pumpAndSettle(); // Bottom sheet dismiss animation.
|
||||||
|
expect(find.text('BottomSheet'), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('Tapping outside a modal BottomSheet should not dismiss it when isDismissible=false', (WidgetTester tester) async {
|
testWidgets('Tapping outside a modal BottomSheet should not dismiss it when isDismissible=false', (WidgetTester tester) async {
|
||||||
BuildContext savedContext;
|
BuildContext savedContext;
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,21 @@ import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
// Pumps and ensures that the BottomSheet animates non-linearly.
|
||||||
|
Future<void> _checkNonLinearAnimation(WidgetTester tester) async {
|
||||||
|
final Offset firstPosition = tester.getCenter(find.text('One'));
|
||||||
|
await tester.pump(const Duration(milliseconds: 30));
|
||||||
|
final Offset secondPosition = tester.getCenter(find.text('One'));
|
||||||
|
await tester.pump(const Duration(milliseconds: 30));
|
||||||
|
final Offset thirdPosition = tester.getCenter(find.text('One'));
|
||||||
|
|
||||||
|
final double dyDelta1 = secondPosition.dy - firstPosition.dy;
|
||||||
|
final double dyDelta2 = thirdPosition.dy - secondPosition.dy;
|
||||||
|
|
||||||
|
// If the animation were linear, these two values would be the same.
|
||||||
|
expect(dyDelta1, isNot(closeTo(dyDelta2, 0.1)));
|
||||||
|
}
|
||||||
|
|
||||||
testWidgets('Verify that a BottomSheet can be rebuilt with ScaffoldFeatureController.setState()', (WidgetTester tester) async {
|
testWidgets('Verify that a BottomSheet can be rebuilt with ScaffoldFeatureController.setState()', (WidgetTester tester) async {
|
||||||
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
|
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
|
||||||
PersistentBottomSheetController<void> bottomSheet;
|
PersistentBottomSheetController<void> bottomSheet;
|
||||||
|
@ -97,6 +112,41 @@ void main() {
|
||||||
expect(find.text('Two'), findsNothing);
|
expect(find.text('Two'), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('Verify that a BottomSheet animates non-linearly', (WidgetTester tester) async {
|
||||||
|
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
|
||||||
|
|
||||||
|
await tester.pumpWidget(MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
key: scaffoldKey,
|
||||||
|
body: const Center(child: Text('body')),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
scaffoldKey.currentState.showBottomSheet<void>((BuildContext context) {
|
||||||
|
return ListView(
|
||||||
|
shrinkWrap: true,
|
||||||
|
primary: false,
|
||||||
|
children: <Widget>[
|
||||||
|
Container(height: 100.0, child: const Text('One')),
|
||||||
|
Container(height: 100.0, child: const Text('Two')),
|
||||||
|
Container(height: 100.0, child: const Text('Three')),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await tester.pump();
|
||||||
|
await _checkNonLinearAnimation(tester);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Two'), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.drag(find.text('Two'), const Offset(0.0, 200.0));
|
||||||
|
await _checkNonLinearAnimation(tester);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Two'), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('Verify that a scrollControlled BottomSheet can be dismissed', (WidgetTester tester) async {
|
testWidgets('Verify that a scrollControlled BottomSheet can be dismissed', (WidgetTester tester) async {
|
||||||
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
|
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue