Repository: fujidaiti/smooth_sheets
Pull request #244 (2024-09-20 12:29:55)

Refactor activities to use SheetExtent.settleTo
## 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



Patch:
diff --git a/lib/src/foundation/sheet_activity.dart b/lib/src/foundation/sheet_activity.dart
index 3e4fac7..297b6e0 100644
--- a/lib/src/foundation/sheet_activity.dart
+++ b/lib/src/foundation/sheet_activity.dart
@@ -172,17 +172,19 @@ 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 {
@@ -190,20 +192,20 @@ class AnimatedSheetActivity extends SheetActivity
     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
@@ -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();
   }
 
@@ -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);
     }
   }
 }
@@ -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)
@@ -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


Test patch:
diff --git a/test/foundation/sheet_activity_test.dart b/test/foundation/sheet_activity_test.dart
index 2098635..52cb2e6 100644
--- a/test/foundation/sheet_activity_test.dart
+++ b/test/foundation/sheet_activity_test.dart
@@ -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,
@@ -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
@@ -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),
+      ));
     });
   });