Skip to content

Commit

Permalink
Refine settling behavior during ballistic animation (#241)
Browse files Browse the repository at this point in the history
## Related issues (optional)

Fixes #193.

## 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
- [x] Contains breaking changes
  - [x] Created / updated migration guide
- [ ] Incremented version number
  - [ ] Updated CHANGELOG
  • Loading branch information
fujidaiti authored Sep 14, 2024
1 parent 65724e7 commit e4fb768
Show file tree
Hide file tree
Showing 13 changed files with 1,238 additions and 262 deletions.
4 changes: 2 additions & 2 deletions example/lib/tutorial/bottom_bar_visibility.dart
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,10 @@ class _ExampleSheet extends StatelessWidget {
const halfSize = Extent.proportional(0.5);
const fullSize = Extent.proportional(1);

final multiStopPhysics = BouncingSheetPhysics(
const multiStopPhysics = BouncingSheetPhysics(
parent: SnappingSheetPhysics(
snappingBehavior: SnapToNearest(
snapTo: const [minSize, halfSize, fullSize],
snapTo: [minSize, halfSize, fullSize],
),
),
);
Expand Down
8 changes: 4 additions & 4 deletions example/lib/tutorial/sheet_content_scaffold.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,13 @@ class _ExampleSheet extends StatelessWidget {
),
);

final physics = BouncingSheetPhysics(
const physics = BouncingSheetPhysics(
parent: SnappingSheetPhysics(
snappingBehavior: SnapToNearest(
snapTo: [
const Extent.proportional(0.2),
const Extent.proportional(0.5),
const Extent.proportional(1),
Extent.proportional(0.2),
Extent.proportional(0.5),
Extent.proportional(1),
],
),
),
Expand Down
10 changes: 5 additions & 5 deletions example/lib/tutorial/sheet_physics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,11 @@ class _MySheet extends StatelessWidget {
// - the extent at which ony (_halfwayFraction * 100)% of the content is visible, or
// - the extent at which the entire content is visible.
// Note that the "extent" is the visible height of the sheet.
final snappingPhysics = SnappingSheetPhysics(
const snappingPhysics = SnappingSheetPhysics(
snappingBehavior: SnapToNearest(
snapTo: [
const Extent.proportional(_halfwayFraction),
const Extent.proportional(1),
Extent.proportional(_halfwayFraction),
Extent.proportional(1),
],
),
// Tips: The above configuration can be replaced with a 'SnapToNearestEdge',
Expand All @@ -104,9 +104,9 @@ class _MySheet extends StatelessWidget {
_PhysicsKind.bouncing => const BouncingSheetPhysics(),
_PhysicsKind.clampingSnapping =>
// Use 'parent' to combine multiple physics behaviors.
ClampingSheetPhysics(parent: snappingPhysics),
const ClampingSheetPhysics(parent: snappingPhysics),
_PhysicsKind.bouncingSnapping =>
BouncingSheetPhysics(parent: snappingPhysics),
const BouncingSheetPhysics(parent: snappingPhysics),
};
}

Expand Down
190 changes: 190 additions & 0 deletions lib/src/foundation/sheet_activity.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import 'dart:async';
import 'dart:math';
import 'dart:ui';

import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
import 'package:meta/meta.dart';

import 'sheet_drag.dart';
import 'sheet_extent.dart';
import 'sheet_physics.dart';
import 'sheet_status.dart';

@internal
Expand Down Expand Up @@ -64,6 +66,7 @@ abstract class SheetActivity<T extends SheetExtent> {

void didChangeViewportDimensions(Size? oldSize, EdgeInsets? oldInsets) {}

// TODO: Change `double?` to `Extent?`.
void didChangeBoundaryConstraints(
double? oldMinPixels,
double? oldMaxPixels,
Expand Down Expand Up @@ -209,6 +212,7 @@ 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;
Expand All @@ -223,6 +227,7 @@ class AnimatedSheetActivity extends SheetActivity
return;
}

// TODO: Remove the following logic and start a settling activity instead.
final oldEndPixels = destination.resolve(oldContentSize);
final newEndPixels = destination.resolve(owner.metrics.contentSize);
final progress = controller.value;
Expand All @@ -245,6 +250,7 @@ class BallisticSheetActivity extends SheetActivity
});

final Simulation simulation;
// TODO: Use controller.value instead.
late double _lastAnimatedValue;

@override
Expand Down Expand Up @@ -278,12 +284,196 @@ class BallisticSheetActivity extends SheetActivity
void onAnimationEnd() {
owner.goBallistic(0);
}

@override
void didFinalizeDimensions(
Size? oldContentSize,
Size? oldViewportSize,
EdgeInsets? oldViewportInsets,
) {
if (oldContentSize == null &&
oldViewportSize == null &&
oldViewportInsets == null) {
return;
}

final oldMetrics = owner.metrics.copyWith(
contentSize: oldContentSize,
viewportSize: oldViewportSize,
viewportInsets: 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 (owner.physics.findSettledExtent(velocity, oldMetrics) case final detent
when detent.resolve(owner.metrics.contentSize) != newPixels) {
owner.beginActivity(
SettlingSheetActivity.withDuration(
const Duration(milliseconds: 150),
destination: detent,
),
);
}
}
}

/// A [SheetActivity] that performs a settling motion in response to changes
/// in the viewport dimensions or content size.
///
/// 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
/// takes to reach the [destination]. In this case, the [velocity] is determined
/// based on the distance to the [destination] and the specified [duration].
///
/// When the concrete value of the [destination] changes due to viewport
/// metrics or content size changes, and the [duration] is specified,
/// the [velocity] is recalculated to ensure the animation duration remains
/// consistent.
@internal
class SettlingSheetActivity extends SheetActivity {
/// Creates a settling activity that animates the sheet position to the
/// [destination] with a constant [velocity].
SettlingSheetActivity({
required this.destination,
required double velocity,
}) : assert(velocity > 0),
_velocity = velocity,
duration = null;

/// Creates a settling activity that animates the sheet position to the
/// [destination] over the specified [duration].
SettlingSheetActivity.withDuration(
Duration this.duration, {
required this.destination,
}) : assert(duration > Duration.zero);

/// The amount of time the animation should take to reach the destination.
///
/// If `null`, the animation lasts until the destination is reached
/// or this activity is disposed.
final Duration? duration;

/// The destination position to which the sheet should settle.
final Extent destination;

late final Ticker _ticker;

/// The amount of time that has passed between the time the animation
/// started and the most recent tick of the animation.
var _elapsedDuration = Duration.zero;

@override
double get velocity => _velocity;
late double _velocity;

@override
SheetStatus get status => SheetStatus.animating;

@override
void init(SheetExtent owner) {
super.init(owner);
_ticker = owner.context.vsync.createTicker(_tick)..start();
_invalidateVelocity();
}

/// Updates the sheet position toward the destination based on the current
/// [_velocity] and the time elapsed since the last frame.
///
/// If the destination is reached, a ballistic activity is started with
/// zero velocity to ensure consistency between the settled position
/// and the current [SheetPhysics].
void _tick(Duration elapsedDuration) {
final elapsedFrameTime =
(elapsedDuration - _elapsedDuration).inMicroseconds /
Duration.microsecondsPerSecond;
final destination = this.destination.resolve(owner.metrics.contentSize);
final pixels = owner.metrics.pixels;
final newPixels = destination > pixels
? min(destination, pixels + velocity * elapsedFrameTime)
: max(destination, pixels - velocity * elapsedFrameTime);
owner
..setPixels(newPixels)
..didUpdateMetrics();

_elapsedDuration = elapsedDuration;

if (newPixels == destination) {
owner.goBallistic(0);
}
}

@override
void didFinalizeDimensions(
Size? oldContentSize,
Size? oldViewportSize,
EdgeInsets? oldViewportInsets,
) {
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 oldInsets = oldViewportInsets;
final deltaInsetBottom = newInsets.bottom - oldInsets.bottom;
final newPixels = owner.metrics.pixels - deltaInsetBottom;
owner
..setPixels(newPixels)
..didUpdateMetrics();
}

_invalidateVelocity();
}

@override
void dispose() {
_ticker.dispose();
super.dispose();
}

/// Updates [_velocity] based on the remaining time and distance to the
/// destination position.
///
/// Make sure to call this method on initialization and whenever the
/// destination changes due to the viewport size or content size changing.
///
/// If the animation [duration] is not specified, this method preserves the
/// current velocity.
void _invalidateVelocity() {
if (duration case final duration?) {
final remainingSeconds = (duration - _elapsedDuration).inMicroseconds /
Duration.microsecondsPerSecond;
final destination = this.destination.resolve(owner.metrics.contentSize);
final pixels = owner.metrics.pixels;
_velocity = remainingSeconds > 0
? (destination - pixels).abs() / remainingSeconds
: (destination - pixels).abs();
}
}
}

@internal
class IdleSheetActivity extends SheetActivity {
@override
SheetStatus get status => SheetStatus.stable;

// TODO: Start a settling activity if the keyboard animation is running.
// @override
// void didFinalizeDimensions(
// Size? oldContentSize,
// Size? oldViewportSize,
// EdgeInsets? oldViewportInsets,
// ) {
// }
}

@internal
Expand Down
Loading

0 comments on commit e4fb768

Please sign in to comment.