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].

<!-- 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
This commit is contained in:
Pierre-Louis 2024-03-16 09:08:00 +01:00 committed by GitHub
parent 988bee803c
commit 3109b1118e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 97 additions and 119 deletions

View file

@ -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}

View file

@ -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<T> extends State<_ModalBottomSheet<T>> {
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<T> extends PopupRoute<T> {
}
}
// 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<double> {
/// 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}

View file

@ -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<T extends Widget, U> {
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<double> {
/// 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,
);
}

View file

@ -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 <Offset>[