Skip to content

Commit

Permalink
Refactor activities to use SheetExtent.settleTo (#244)
Browse files Browse the repository at this point in the history
## Summary (check all that apply)

- [x] Modified / added code
- [x] Modified / added tests
- [ ] Modified / added examples
- [ ] Modified / added others (pubspec.yaml, workflows, etc...)
- [ ] Updated README
- [ ] Contains breaking changes
  - [ ] Created / updated migration guide
- [ ] Incremented version number
  - [ ] Updated CHANGELOG
  • Loading branch information
fujidaiti authored Sep 20, 2024
1 parent 0054bde commit cb21cc3
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 65 deletions.
107 changes: 59 additions & 48 deletions lib/src/foundation/sheet_activity.dart
Original file line number Diff line number Diff line change
Expand Up @@ -172,38 +172,40 @@ abstract class SheetActivity<T extends SheetExtent> {
}

/// An activity that animates the [SheetExtent]'s `pixels` to a destination
/// position resolved by [destination], using the specified [curve] and
/// position determined by [destination], using the specified [curve] and
/// [duration].
///
/// This activity accepts the destination position as an [Extent], allowing
/// the concrete end position (in pixels) to be adjusted during the animation
/// in response to viewport changes, such as the appearance of the keyboard.
/// the concrete end position (in pixels) to be updated during the animation
/// in response to viewport changes, such as the appearance of the on-screen
/// keyboard.
///
/// When the end position changes, this activity updates the [SheetExtent]'s
/// `pixels` to maintain the sheet's visual position (referred to as *p*).
/// In subsequent frames, the animated position is linearly interpolated
/// between *p* and the new destination.
/// When the bottom viewport inset changes, typically due to the appearance
/// or disappearance of the on-screen keyboard, this activity updates the
/// sheet position to maintain its visual position unchanged. If the
/// end position changes, it starts a [SettlingSheetActivity] for the
/// remaining duration to ensure the animation duration remains consistent.
@internal
class AnimatedSheetActivity extends SheetActivity
with ControlledSheetActivityMixin {
AnimatedSheetActivity({
required this.destination,
required this.duration,
required this.curve,
}) : _effectiveCurve = curve,
assert(duration > Duration.zero);
}) : assert(duration > Duration.zero);

final Extent destination;
final Duration duration;
final Curve curve;

late double _startPixels;
Curve _effectiveCurve;
late final double _startPixels;
late final double _endPixels;

@override
void init(SheetExtent delegate) {
super.init(delegate);
_startPixels = owner.metrics.pixels;
_endPixels = destination.resolve(owner.metrics.contentSize);
}

@override
Expand All @@ -222,13 +224,9 @@ class AnimatedSheetActivity extends SheetActivity

@override
void onAnimationTick() {
// The baseline may change during the animation, so we need to
// interpolate the current pixels in absolute coordinates. This ensures
// visual consistency regardless of baseline changes.
final endPixels = destination.resolve(owner.metrics.contentSize);
final progress = _effectiveCurve.transform(controller.value);
final progress = curve.transform(controller.value);
owner
..setPixels(lerpDouble(_startPixels, endPixels, progress)!)
..setPixels(lerpDouble(_startPixels, _endPixels, progress)!)
..didUpdateMetrics();
}

Expand All @@ -243,32 +241,31 @@ class AnimatedSheetActivity extends SheetActivity
Size? oldViewportSize,
EdgeInsets? oldViewportInsets,
) {
// TODO: DRY with other activities.
// Appends the delta of the bottom inset (typically the keyboard height)
// to keep the visual sheet position unchanged.
final newInsets = owner.metrics.viewportInsets;
final oldInsets = oldViewportInsets ?? newInsets;
final deltaInsetBottom = newInsets.bottom - oldInsets.bottom;
final newPixels = owner.metrics.pixels - deltaInsetBottom;
owner
..setPixels(newPixels)
..didUpdateMetrics();

if (oldContentSize == null) {
if (oldContentSize == null &&
oldViewportSize == null &&
oldViewportInsets == null) {
return;
}

// TODO: Remove the following logic and start a settling activity instead.
final oldEndPixels = destination.resolve(oldContentSize);
if (oldViewportInsets != null) {
// TODO: DRY with other activities.
// Appends the delta of the bottom inset (typically the keyboard height)
// to keep the visual sheet position unchanged.
final newInsets = owner.metrics.viewportInsets;
final deltaInsetBottom = newInsets.bottom - oldViewportInsets.bottom;
final correctedPixels = owner.metrics.pixels - deltaInsetBottom;
if (correctedPixels != owner.metrics.pixels) {
owner
..setPixels(correctedPixels)
..didUpdateMetrics();
}
}

final newEndPixels = destination.resolve(owner.metrics.contentSize);
final progress = controller.value;
if (oldEndPixels != newEndPixels && progress < 1) {
// The gradient of the line passing through the point
// (t=progress, newPixels) and (t=1.0, newEndPixels).
final gradient = (newEndPixels - newPixels) / (1 - progress);
// The new start position is the intersection of that line with t=0.
_startPixels = newEndPixels - gradient;
_effectiveCurve = Curves.linear;
if (newEndPixels != _endPixels) {
final remainingDuration =
duration - (controller.lastElapsedDuration ?? Duration.zero);
owner.settleTo(destination, remainingDuration);
}
}
}
Expand Down Expand Up @@ -333,6 +330,7 @@ class BallisticSheetActivity extends SheetActivity
viewportSize: oldViewportSize,
viewportInsets: oldViewportInsets,
);
final destination = owner.physics.findSettledExtent(velocity, oldMetrics);

// TODO: DRY with other activities.
// Appends the delta of the bottom inset (typically the keyboard height)
Expand All @@ -345,22 +343,35 @@ class BallisticSheetActivity extends SheetActivity
..setPixels(newPixels)
..didUpdateMetrics();

if (owner.physics.findSettledExtent(velocity, oldMetrics) case final detent
when detent.resolve(owner.metrics.contentSize) != newPixels) {
// TODO: Use SheetExtent.settle instead.
owner.beginActivity(
SettlingSheetActivity.withDuration(
const Duration(milliseconds: 150),
destination: detent,
),
);
final endPixels = destination.resolve(owner.metrics.contentSize);
if (endPixels == owner.metrics.pixels) {
return;
}

const maxSettlingDuration = 150; // milliseconds
final distance = (endPixels - owner.metrics.pixels).abs();
final velocityNorm = velocity.abs();
final estimatedSettlingDuration = velocityNorm > 0
? distance / velocityNorm * Duration.millisecondsPerSecond
: double.infinity;

owner.settleTo(
destination,
estimatedSettlingDuration > maxSettlingDuration
? const Duration(milliseconds: maxSettlingDuration)
: Duration(milliseconds: estimatedSettlingDuration.round()),
);
}
}

/// A [SheetActivity] that performs a settling motion in response to changes
/// in the viewport dimensions or content size.
///
/// A [SheetExtent] may start this activity when the viewport insets change
/// during an animation, typically due to the appearance or disappearance of
/// the on-screen keyboard, or when the content size changes (e.g., due to
/// entering a new line of text in a text field).
///
/// This activity animates the sheet position to the [destination] with a
/// constant [velocity] until the destination is reached. Optionally, the
/// animation [duration] can be specified to explicitly control the time it
Expand Down
27 changes: 10 additions & 17 deletions test/foundation/sheet_activity_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ void main() {
var metrics = const SheetMetrics(
pixels: 300,
minExtent: Extent.pixels(300),
maxExtent: Extent.pixels(900),
maxExtent: Extent.proportional(1),
contentSize: Size(400, 900),
viewportSize: Size(400, 900),
viewportInsets: EdgeInsets.zero,
Expand All @@ -107,11 +107,13 @@ void main() {

when(controller.value).thenReturn(0.0);
activity.onAnimationTick();
verify(owner.setPixels(300));
expect(metrics.pixels, 300);

when(controller.value).thenReturn(0.25);
when(controller.lastElapsedDuration)
.thenReturn(const Duration(milliseconds: 75));
activity.onAnimationTick();
verify(owner.setPixels(450));
expect(metrics.pixels, 450);

// The following lines simulate a viewport change, in which:
// 1. The viewport's bottom inset increases, simulating the
Expand All @@ -123,28 +125,19 @@ void main() {
final oldViewportInsets = metrics.viewportInsets;
final oldContentSize = metrics.contentSize;
metrics = metrics.copyWith(
maxExtent: const Extent.pixels(850),
viewportInsets: const EdgeInsets.only(bottom: 50),
contentSize: const Size(400, 850),
);
activity.didChangeViewportDimensions(null, oldViewportInsets);
activity.didChangeContentSize(oldContentSize);
activity.didFinalizeDimensions(oldContentSize, null, oldViewportInsets);
verify(owner.setPixels(400));
expect(metrics.pixels, 400);
expect(metrics.viewPixels, 450,
reason: 'Visual position should not change when viewport changes.');

when(controller.value).thenReturn(0.5);
activity.onAnimationTick();
verify(owner.setPixels(550));

when(controller.value).thenReturn(0.75);
activity.onAnimationTick();
verify(owner.setPixels(700));

when(controller.value).thenReturn(1.0);
activity.onAnimationTick();
verify(owner.setPixels(850));
verify(owner.settleTo(
const Extent.proportional(1),
const Duration(milliseconds: 225),
));
});
});

Expand Down

0 comments on commit cb21cc3

Please sign in to comment.