Skip to content

Commit 3109b11

Browse files
authored
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 flutter/flutter#51627 Diagram to be added with flutter/assets-for-api-docs#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]. <!-- Links --> [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
1 parent 988bee8 commit 3109b11

File tree

4 files changed

+97
-119
lines changed

4 files changed

+97
-119
lines changed

packages/flutter/lib/src/animation/curves.dart

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5-
65
import 'dart:math' as math;
76
import 'dart:ui';
87

@@ -193,6 +192,79 @@ class Interval extends Curve {
193192
}
194193
}
195194

195+
/// A curve that progresses according to [beginCurve] until [split], then
196+
/// according to [endCurve].
197+
///
198+
/// Split curves are useful in situations where a widget must track the
199+
/// user's finger (which requires a linear animation), but can also be flung
200+
/// using a curve specified with the [endCurve] argument, after the finger is
201+
/// released. In such a case, the value of [split] would be the progress
202+
/// of the animation at the time when the finger was released.
203+
///
204+
/// For example, if [split] is set to 0.5, [beginCurve] is [Curves.linear],
205+
/// and [endCurve] is [Curves.easeOutCubic], then the bottom-left quarter of the
206+
/// curve will be a straight line, and the top-right quarter will contain the
207+
/// entire [Curves.easeOutCubic] curve.
208+
///
209+
/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_split.mp4}
210+
class Split extends Curve {
211+
/// Creates a split curve.
212+
const Split(
213+
this.split, {
214+
this.beginCurve = Curves.linear,
215+
this.endCurve = Curves.easeOutCubic,
216+
});
217+
218+
/// The progress value separating [beginCurve] from [endCurve].
219+
///
220+
/// The value before which the curve progresses according to [beginCurve] and
221+
/// after which the curve progresses according to [endCurve].
222+
///
223+
/// When t is exactly `split`, the curve has the value `split`.
224+
///
225+
/// Must be between 0 and 1.0, inclusively.
226+
final double split;
227+
228+
/// The curve to use before [split] is reached.
229+
///
230+
/// Defaults to [Curves.linear].
231+
final Curve beginCurve;
232+
233+
/// The curve to use after [split] is reached.
234+
///
235+
/// Defaults to [Curves.easeOutCubic].
236+
final Curve endCurve;
237+
238+
@override
239+
double transform(double t) {
240+
assert(t >= 0.0 && t <= 1.0);
241+
assert(split >= 0.0 && split <= 1.0);
242+
243+
if (t == 0.0 || t == 1.0) {
244+
return t;
245+
}
246+
247+
if (t == split) {
248+
return split;
249+
}
250+
251+
if (t < split) {
252+
final double curveProgress = t / split;
253+
final double transformed = beginCurve.transform(curveProgress);
254+
return lerpDouble(0, split, transformed)!;
255+
} else {
256+
final double curveProgress = (t - split) / (1 - split);
257+
final double transformed = endCurve.transform(curveProgress);
258+
return lerpDouble(split, 1, transformed)!;
259+
}
260+
}
261+
262+
@override
263+
String toString() {
264+
return '${describeIdentity(this)}($split, $beginCurve, $endCurve)';
265+
}
266+
}
267+
196268
/// A curve that is 0.0 until it hits the threshold, then it jumps to 1.0.
197269
///
198270
/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_threshold.mp4}

packages/flutter/lib/src/material/bottom_sheet.dart

Lines changed: 2 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5-
import 'dart:ui' show lerpDouble;
6-
7-
import 'package:flutter/foundation.dart';
85
import 'package:flutter/gestures.dart';
96
import 'package:flutter/rendering.dart';
107
import 'package:flutter/widgets.dart';
@@ -705,9 +702,9 @@ class _ModalBottomSheetState<T> extends State<_ModalBottomSheet<T>> {
705702

706703
void handleDragEnd(DragEndDetails details, {bool? isClosing}) {
707704
// Allow the bottom sheet to animate smoothly from its current position.
708-
animationCurve = _BottomSheetSuspendedCurve(
705+
animationCurve = Split(
709706
widget.route.animation!.value,
710-
curve: _modalBottomSheetCurve,
707+
endCurve: _modalBottomSheetCurve,
711708
);
712709
}
713710

@@ -1124,61 +1121,6 @@ class ModalBottomSheetRoute<T> extends PopupRoute<T> {
11241121
}
11251122
}
11261123

1127-
// TODO(guidezpl): Look into making this public. A copy of this class is in
1128-
// scaffold.dart, for now, https://github.com/flutter/flutter/issues/51627
1129-
/// A curve that progresses linearly until a specified [startingPoint], at which
1130-
/// point [curve] will begin. Unlike [Interval], [curve] will not start at zero,
1131-
/// but will use [startingPoint] as the Y position.
1132-
///
1133-
/// For example, if [startingPoint] is set to `0.5`, and [curve] is set to
1134-
/// [Curves.easeOut], then the bottom-left quarter of the curve will be a
1135-
/// straight line, and the top-right quarter will contain the entire contents of
1136-
/// [Curves.easeOut].
1137-
///
1138-
/// This is useful in situations where a widget must track the user's finger
1139-
/// (which requires a linear animation), and afterwards can be flung using a
1140-
/// curve specified with the [curve] argument, after the finger is released. In
1141-
/// such a case, the value of [startingPoint] would be the progress of the
1142-
/// animation at the time when the finger was released.
1143-
class _BottomSheetSuspendedCurve extends ParametricCurve<double> {
1144-
/// Creates a suspended curve.
1145-
const _BottomSheetSuspendedCurve(
1146-
this.startingPoint, {
1147-
this.curve = Curves.easeOutCubic,
1148-
});
1149-
1150-
/// The progress value at which [curve] should begin.
1151-
final double startingPoint;
1152-
1153-
/// The curve to use when [startingPoint] is reached.
1154-
///
1155-
/// This defaults to [Curves.easeOutCubic].
1156-
final Curve curve;
1157-
1158-
@override
1159-
double transform(double t) {
1160-
assert(t >= 0.0 && t <= 1.0);
1161-
assert(startingPoint >= 0.0 && startingPoint <= 1.0);
1162-
1163-
if (t < startingPoint) {
1164-
return t;
1165-
}
1166-
1167-
if (t == 1.0) {
1168-
return t;
1169-
}
1170-
1171-
final double curveProgress = (t - startingPoint) / (1 - startingPoint);
1172-
final double transformed = curve.transform(curveProgress);
1173-
return lerpDouble(startingPoint, 1, transformed)!;
1174-
}
1175-
1176-
@override
1177-
String toString() {
1178-
return '${describeIdentity(this)}($startingPoint, $curve)';
1179-
}
1180-
}
1181-
11821124
/// Shows a modal Material Design bottom sheet.
11831125
///
11841126
/// {@macro flutter.material.ModalBottomSheetRoute}

packages/flutter/lib/src/material/scaffold.dart

Lines changed: 2 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import 'dart:async';
66
import 'dart:collection';
77
import 'dart:math' as math;
8-
import 'dart:ui' show lerpDouble;
98

109
import 'package:flutter/foundation.dart';
1110
import 'package:flutter/gestures.dart' show DragStartBehavior;
@@ -3119,61 +3118,6 @@ class ScaffoldFeatureController<T extends Widget, U> {
31193118
final StateSetter? setState;
31203119
}
31213120

3122-
// TODO(guidezpl): Look into making this public. A copy of this class is in
3123-
// bottom_sheet.dart, for now, https://github.com/flutter/flutter/issues/51627
3124-
/// A curve that progresses linearly until a specified [startingPoint], at which
3125-
/// point [curve] will begin. Unlike [Interval], [curve] will not start at zero,
3126-
/// but will use [startingPoint] as the Y position.
3127-
///
3128-
/// For example, if [startingPoint] is set to `0.5`, and [curve] is set to
3129-
/// [Curves.easeOut], then the bottom-left quarter of the curve will be a
3130-
/// straight line, and the top-right quarter will contain the entire contents of
3131-
/// [Curves.easeOut].
3132-
///
3133-
/// This is useful in situations where a widget must track the user's finger
3134-
/// (which requires a linear animation), and afterwards can be flung using a
3135-
/// curve specified with the [curve] argument, after the finger is released. In
3136-
/// such a case, the value of [startingPoint] would be the progress of the
3137-
/// animation at the time when the finger was released.
3138-
class _BottomSheetSuspendedCurve extends ParametricCurve<double> {
3139-
/// Creates a suspended curve.
3140-
const _BottomSheetSuspendedCurve(
3141-
this.startingPoint, {
3142-
this.curve = Curves.easeOutCubic,
3143-
});
3144-
3145-
/// The progress value at which [curve] should begin.
3146-
///
3147-
/// This defaults to [Curves.easeOutCubic].
3148-
final double startingPoint;
3149-
3150-
/// The curve to use when [startingPoint] is reached.
3151-
final Curve curve;
3152-
3153-
@override
3154-
double transform(double t) {
3155-
assert(t >= 0.0 && t <= 1.0);
3156-
assert(startingPoint >= 0.0 && startingPoint <= 1.0);
3157-
3158-
if (t < startingPoint) {
3159-
return t;
3160-
}
3161-
3162-
if (t == 1.0) {
3163-
return t;
3164-
}
3165-
3166-
final double curveProgress = (t - startingPoint) / (1 - startingPoint);
3167-
final double transformed = curve.transform(curveProgress);
3168-
return lerpDouble(startingPoint, 1, transformed)!;
3169-
}
3170-
3171-
@override
3172-
String toString() {
3173-
return '${describeIdentity(this)}($startingPoint, $curve)';
3174-
}
3175-
}
3176-
31773121
class _StandardBottomSheet extends StatefulWidget {
31783122
const _StandardBottomSheet({
31793123
super.key,
@@ -3247,9 +3191,9 @@ class _StandardBottomSheetState extends State<_StandardBottomSheet> {
32473191

32483192
void _handleDragEnd(DragEndDetails details, { bool? isClosing }) {
32493193
// Allow the bottom sheet to animate smoothly from its current position.
3250-
animationCurve = _BottomSheetSuspendedCurve(
3194+
animationCurve = Split(
32513195
widget.animationController.value,
3252-
curve: _standardBottomSheetCurve,
3196+
endCurve: _standardBottomSheetCurve,
32533197
);
32543198
}
32553199

packages/flutter/test/animation/curves_test.dart

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ void main() {
1313
expect(const SawTooth(3), hasOneLineDescription);
1414
expect(const Interval(0.25, 0.75), hasOneLineDescription);
1515
expect(const Interval(0.25, 0.75, curve: Curves.ease), hasOneLineDescription);
16+
expect(const Split(0.25, beginCurve: Curves.ease), hasOneLineDescription);
1617
});
1718

1819
test('Curve flipped control test', () {
@@ -187,6 +188,9 @@ void main() {
187188
expect(() => const Interval(0.0, 1.0).transform(-0.0001), throwsAssertionError);
188189
expect(() => const Interval(0.0, 1.0).transform(1.0001), throwsAssertionError);
189190

191+
expect(() => const Split(0.0).transform(-0.0001), throwsAssertionError);
192+
expect(() => const Split(0.0).transform(1.0001), throwsAssertionError);
193+
190194
expect(() => const Threshold(0.5).transform(-0.0001), throwsAssertionError);
191195
expect(() => const Threshold(0.5).transform(1.0001), throwsAssertionError);
192196

@@ -222,6 +226,9 @@ void main() {
222226
expect(const Interval(0, 1).transform(0), 0);
223227
expect(const Interval(0, 1).transform(1), 1);
224228

229+
expect(const Split(0.5).transform(0), 0);
230+
expect(const Split(0.5).transform(1), 1);
231+
225232
expect(const Threshold(0.5).transform(0), 0);
226233
expect(const Threshold(0.5).transform(1), 1);
227234

@@ -259,6 +266,19 @@ void main() {
259266
expect(Curves.bounceInOut.transform(1), 1);
260267
});
261268

269+
test('Split interpolates values properly', () {
270+
const Split curve = Split(0.3);
271+
272+
const double tolerance = 1e-6;
273+
expect(curve.transform(0.0), equals(0.0));
274+
expect(curve.transform(0.1), equals(0.1));
275+
expect(curve.transform(0.25), equals(0.25));
276+
expect(curve.transform(0.3), equals(0.3));
277+
expect(curve.transform(0.5), moreOrLessEquals(0.760461, epsilon: tolerance));
278+
expect(curve.transform(0.75), moreOrLessEquals(0.962055, epsilon: tolerance));
279+
expect(curve.transform(1.0), equals(1.0));
280+
});
281+
262282
test('CatmullRomSpline interpolates values properly', () {
263283
final CatmullRomSpline curve = CatmullRomSpline(
264284
const <Offset>[

0 commit comments

Comments
 (0)