Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refine settling behavior during ballistic animation #241

Merged
merged 10 commits into from
Sep 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading