From 3109b1118ee8207957ae76b2e5a952aeee5948cf Mon Sep 17 00:00:00 2001 From: Pierre-Louis <6655696+guidezpl@users.noreply.github.com> Date: Sat, 16 Mar 2024 09:08:00 +0100 Subject: [PATCH] Introduce `Split` curve (#143130) `Split` is a curve that progresses according to `beginCurve` until `split`, then according to `endCurve`. This curve is used with bottom sheets to allow linear finger dragging and non-linear enter/exit animations. This PR cleans up a previously private class I introduced and replaces it with the more customisable `Split`. Fixes https://github.com/flutter/flutter/issues/51627 Diagram to be added with https://github.com/flutter/assets-for-api-docs/pull/239 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. [Contributor Guide]: https://github.com/flutter/flutter/wiki/Tree-hygiene#overview [Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene [test-exempt]: https://github.com/flutter/flutter/wiki/Tree-hygiene#tests [Flutter Style Guide]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo [Features we expect every widget to implement]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/wiki/Chat --- .../flutter/lib/src/animation/curves.dart | 74 ++++++++++++++++++- .../lib/src/material/bottom_sheet.dart | 62 +--------------- .../flutter/lib/src/material/scaffold.dart | 60 +-------------- .../flutter/test/animation/curves_test.dart | 20 +++++ 4 files changed, 97 insertions(+), 119 deletions(-) diff --git a/packages/flutter/lib/src/animation/curves.dart b/packages/flutter/lib/src/animation/curves.dart index c5b5b7a3f8f..25f4659eae0 100644 --- a/packages/flutter/lib/src/animation/curves.dart +++ b/packages/flutter/lib/src/animation/curves.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. - import 'dart:math' as math; import 'dart:ui'; @@ -193,6 +192,79 @@ class Interval extends Curve { } } +/// A curve that progresses according to [beginCurve] until [split], then +/// according to [endCurve]. +/// +/// Split curves are useful in situations where a widget must track the +/// user's finger (which requires a linear animation), but can also be flung +/// using a curve specified with the [endCurve] argument, after the finger is +/// released. In such a case, the value of [split] would be the progress +/// of the animation at the time when the finger was released. +/// +/// For example, if [split] is set to 0.5, [beginCurve] is [Curves.linear], +/// and [endCurve] is [Curves.easeOutCubic], then the bottom-left quarter of the +/// curve will be a straight line, and the top-right quarter will contain the +/// entire [Curves.easeOutCubic] curve. +/// +/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_split.mp4} +class Split extends Curve { + /// Creates a split curve. + const Split( + this.split, { + this.beginCurve = Curves.linear, + this.endCurve = Curves.easeOutCubic, + }); + + /// The progress value separating [beginCurve] from [endCurve]. + /// + /// The value before which the curve progresses according to [beginCurve] and + /// after which the curve progresses according to [endCurve]. + /// + /// When t is exactly `split`, the curve has the value `split`. + /// + /// Must be between 0 and 1.0, inclusively. + final double split; + + /// The curve to use before [split] is reached. + /// + /// Defaults to [Curves.linear]. + final Curve beginCurve; + + /// The curve to use after [split] is reached. + /// + /// Defaults to [Curves.easeOutCubic]. + final Curve endCurve; + + @override + double transform(double t) { + assert(t >= 0.0 && t <= 1.0); + assert(split >= 0.0 && split <= 1.0); + + if (t == 0.0 || t == 1.0) { + return t; + } + + if (t == split) { + return split; + } + + if (t < split) { + final double curveProgress = t / split; + final double transformed = beginCurve.transform(curveProgress); + return lerpDouble(0, split, transformed)!; + } else { + final double curveProgress = (t - split) / (1 - split); + final double transformed = endCurve.transform(curveProgress); + return lerpDouble(split, 1, transformed)!; + } + } + + @override + String toString() { + return '${describeIdentity(this)}($split, $beginCurve, $endCurve)'; + } +} + /// A curve that is 0.0 until it hits the threshold, then it jumps to 1.0. /// /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_threshold.mp4} diff --git a/packages/flutter/lib/src/material/bottom_sheet.dart b/packages/flutter/lib/src/material/bottom_sheet.dart index 1d62275b9ff..f63e2c03989 100644 --- a/packages/flutter/lib/src/material/bottom_sheet.dart +++ b/packages/flutter/lib/src/material/bottom_sheet.dart @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show lerpDouble; - -import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; @@ -705,9 +702,9 @@ class _ModalBottomSheetState extends State<_ModalBottomSheet> { void handleDragEnd(DragEndDetails details, {bool? isClosing}) { // Allow the bottom sheet to animate smoothly from its current position. - animationCurve = _BottomSheetSuspendedCurve( + animationCurve = Split( widget.route.animation!.value, - curve: _modalBottomSheetCurve, + endCurve: _modalBottomSheetCurve, ); } @@ -1124,61 +1121,6 @@ class ModalBottomSheetRoute extends PopupRoute { } } -// TODO(guidezpl): Look into making this public. A copy of this class is in -// scaffold.dart, for now, https://github.com/flutter/flutter/issues/51627 -/// 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. -class _BottomSheetSuspendedCurve extends ParametricCurve { - /// Creates a suspended curve. - const _BottomSheetSuspendedCurve( - this.startingPoint, { - this.curve = Curves.easeOutCubic, - }); - - /// The progress value at which [curve] should begin. - final double startingPoint; - - /// The curve to use when [startingPoint] is reached. - /// - /// This defaults to [Curves.easeOutCubic]. - 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. /// /// {@macro flutter.material.ModalBottomSheetRoute} diff --git a/packages/flutter/lib/src/material/scaffold.dart b/packages/flutter/lib/src/material/scaffold.dart index cdc6c02cf74..0eb3b8f40b3 100644 --- a/packages/flutter/lib/src/material/scaffold.dart +++ b/packages/flutter/lib/src/material/scaffold.dart @@ -5,7 +5,6 @@ import 'dart:async'; import 'dart:collection'; import 'dart:math' as math; -import 'dart:ui' show lerpDouble; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart' show DragStartBehavior; @@ -3119,61 +3118,6 @@ class ScaffoldFeatureController { final StateSetter? setState; } -// TODO(guidezpl): Look into making this public. A copy of this class is in -// bottom_sheet.dart, for now, https://github.com/flutter/flutter/issues/51627 -/// 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. -class _BottomSheetSuspendedCurve extends ParametricCurve { - /// Creates a suspended curve. - const _BottomSheetSuspendedCurve( - this.startingPoint, { - this.curve = Curves.easeOutCubic, - }); - - /// 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 { const _StandardBottomSheet({ super.key, @@ -3247,9 +3191,9 @@ class _StandardBottomSheetState extends State<_StandardBottomSheet> { void _handleDragEnd(DragEndDetails details, { bool? isClosing }) { // Allow the bottom sheet to animate smoothly from its current position. - animationCurve = _BottomSheetSuspendedCurve( + animationCurve = Split( widget.animationController.value, - curve: _standardBottomSheetCurve, + endCurve: _standardBottomSheetCurve, ); } diff --git a/packages/flutter/test/animation/curves_test.dart b/packages/flutter/test/animation/curves_test.dart index f7b0757fd5d..123987ee302 100644 --- a/packages/flutter/test/animation/curves_test.dart +++ b/packages/flutter/test/animation/curves_test.dart @@ -13,6 +13,7 @@ void main() { expect(const SawTooth(3), hasOneLineDescription); expect(const Interval(0.25, 0.75), hasOneLineDescription); expect(const Interval(0.25, 0.75, curve: Curves.ease), hasOneLineDescription); + expect(const Split(0.25, beginCurve: Curves.ease), hasOneLineDescription); }); test('Curve flipped control test', () { @@ -187,6 +188,9 @@ void main() { expect(() => const Interval(0.0, 1.0).transform(-0.0001), throwsAssertionError); expect(() => const Interval(0.0, 1.0).transform(1.0001), throwsAssertionError); + expect(() => const Split(0.0).transform(-0.0001), throwsAssertionError); + expect(() => const Split(0.0).transform(1.0001), throwsAssertionError); + expect(() => const Threshold(0.5).transform(-0.0001), throwsAssertionError); expect(() => const Threshold(0.5).transform(1.0001), throwsAssertionError); @@ -222,6 +226,9 @@ void main() { expect(const Interval(0, 1).transform(0), 0); expect(const Interval(0, 1).transform(1), 1); + expect(const Split(0.5).transform(0), 0); + expect(const Split(0.5).transform(1), 1); + expect(const Threshold(0.5).transform(0), 0); expect(const Threshold(0.5).transform(1), 1); @@ -259,6 +266,19 @@ void main() { expect(Curves.bounceInOut.transform(1), 1); }); + test('Split interpolates values properly', () { + const Split curve = Split(0.3); + + const double tolerance = 1e-6; + expect(curve.transform(0.0), equals(0.0)); + expect(curve.transform(0.1), equals(0.1)); + expect(curve.transform(0.25), equals(0.25)); + expect(curve.transform(0.3), equals(0.3)); + expect(curve.transform(0.5), moreOrLessEquals(0.760461, epsilon: tolerance)); + expect(curve.transform(0.75), moreOrLessEquals(0.962055, epsilon: tolerance)); + expect(curve.transform(1.0), equals(1.0)); + }); + test('CatmullRomSpline interpolates values properly', () { final CatmullRomSpline curve = CatmullRomSpline( const [