Skip to content

Commit

Permalink
Fix: Opening keyboard interrupts sheet animation (#189)
Browse files Browse the repository at this point in the history
Fixes #14.

The sheet position adjustment strategy of `AnimatedSheetActivity`, which
is used when either the sheet content size or the viewport size changes,
was modified to:

1. append the delta of `MediaQueryData.viewInsets.bottom` (the keyboard
height) to keep the visual sheet position unchanged, and
3. if the animation is still running, start a new linear animation to
bring the sheet position to the recalculated final position in the
remaining duration. We use a linear curve here because starting a curved
animation in the middle of another curved animation tends to look jerky.
  • Loading branch information
fujidaiti authored Jul 11, 2024
1 parent c582cbd commit b9bf455
Show file tree
Hide file tree
Showing 9 changed files with 419 additions and 63 deletions.
4 changes: 4 additions & 0 deletions package/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 0.8.2 Jul 11, 2024

- Fix: Opening keyboard interrupts sheet animation (#189)

## 0.8.1 Jun 23, 2024

- Fix: Cupertino style modal transition not working with NavigationSheet (#182)
Expand Down
42 changes: 36 additions & 6 deletions package/lib/src/foundation/sheet_activity.dart
Original file line number Diff line number Diff line change
Expand Up @@ -131,32 +131,62 @@ abstract class SheetActivity<T extends SheetExtent> {
class AnimatedSheetActivity extends SheetActivity
with ControlledSheetActivityMixin {
AnimatedSheetActivity({
required this.from,
required this.to,
required this.destination,
required this.duration,
required this.curve,
}) : assert(duration > Duration.zero);

final double from;
final double to;
final Extent destination;
final Duration duration;
final Curve curve;

@override
AnimationController createAnimationController() {
return AnimationController.unbounded(
value: from, vsync: owner.context.vsync);
value: owner.metrics.pixels,
vsync: owner.context.vsync,
);
}

@override
TickerFuture onAnimationStart() {
return controller.animateTo(to, duration: duration, curve: curve);
return controller.animateTo(
destination.resolve(owner.metrics.contentSize),
duration: duration,
curve: curve,
);
}

@override
void onAnimationEnd() {
owner.goBallistic(0);
}

@override
void didFinalizeDimensions(
Size? oldContentSize,
Size? oldViewportSize,
EdgeInsets? oldViewportInsets,
) {
// 1. 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;
owner.setPixels(owner.metrics.pixels - deltaInsetBottom);

// 2. If the animation is still running, we start a new linear animation
// to bring the sheet position to the recalculated final position in the
// remaining duration. We use a linear curve here because starting a curved
// animation in the middle of another curved animation tends to look jerky.
final newDestination = destination.resolve(owner.metrics.contentSize);
final elapsedDuration = controller.lastElapsedDuration ?? duration;
if (newDestination != controller.upperBound && elapsedDuration < duration) {
final carriedDuration = duration - elapsedDuration;
owner.animateTo(destination,
duration: carriedDuration, curve: Curves.linear);
}
}
}

@internal
Expand Down
6 changes: 2 additions & 4 deletions package/lib/src/foundation/sheet_extent.dart
Original file line number Diff line number Diff line change
Expand Up @@ -507,13 +507,11 @@ abstract class SheetExtent extends ChangeNotifier
Duration duration = const Duration(milliseconds: 300),
}) {
assert(metrics.hasDimensions);
final destination = newExtent.resolve(metrics.contentSize);
if (metrics.pixels == destination) {
if (metrics.pixels == newExtent.resolve(metrics.contentSize)) {
return Future.value();
} else {
final activity = AnimatedSheetActivity(
from: metrics.pixels,
to: destination,
destination: newExtent,
duration: duration,
curve: curve,
);
Expand Down
14 changes: 14 additions & 0 deletions package/lib/src/navigation/navigation_sheet_extent.dart
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,20 @@ class NavigationSheetExtent extends SheetExtent {
}
}

@override
Future<void> animateTo(
Extent newExtent, {
Curve curve = Curves.easeInOut,
Duration duration = const Duration(milliseconds: 300),
}) {
if (activity case ProxySheetActivity(:final route)) {
return route.scopeKey.currentExtent
.animateTo(newExtent, curve: curve, duration: duration);
} else {
return super.animateTo(newExtent, curve: curve, duration: duration);
}
}

@override
void dispatchUpdateNotification() {
// Do not dispatch a notifications if a local extent is active.
Expand Down
2 changes: 1 addition & 1 deletion package/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: smooth_sheets
description: Sheet widgets with smooth motion and great flexibility. Also supports nested navigation in both imperative and declarative ways.
version: 0.8.1
version: 0.8.2
repository: https://github.com/fujidaiti/smooth_sheets
screenshots:
- description: Practical examples of smooth_sheets.
Expand Down
124 changes: 103 additions & 21 deletions package/test/draggable/draggable_sheet_test.dart
Original file line number Diff line number Diff line change
@@ -1,34 +1,56 @@
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:smooth_sheets/smooth_sheets.dart';
import 'package:smooth_sheets/src/foundation/sheet_controller.dart';

class _TestWidget extends StatelessWidget {
const _TestWidget({
this.contentKey,
this.contentHeight = 500,
import '../src/keyboard_inset_simulation.dart';

class _TestApp extends StatelessWidget {
const _TestApp({
this.useMaterial = false,
required this.child,
});

final Key? contentKey;
final double contentHeight;
final bool useMaterial;
final Widget child;

@override
Widget build(BuildContext context) {
final content = Container(
key: contentKey,
color: Colors.white,
height: contentHeight,
width: double.infinity,
);

return Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: DraggableSheet(
child: content,
if (useMaterial) {
return MaterialApp(
home: child,
);
} else {
return Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: child,
),
),
);
}
}
}

class _TestSheetContent extends StatelessWidget {
const _TestSheetContent({
super.key,
this.height = 500,
this.child,
});

final double? height;
final Widget? child;

@override
Widget build(BuildContext context) {
return Container(
height: height,
width: double.infinity,
color: Colors.white,
child: child,
);
}
}
Expand All @@ -39,11 +61,71 @@ void main() {
await tester.pumpWidget(
SheetControllerScope(
controller: controller,
child: const _TestWidget(),
child: const _TestApp(
child: DraggableSheet(
child: _TestSheetContent(),
),
),
),
);

expect(controller.hasClient, isTrue,
reason: 'The controller should have a client.');
});

// Regression test for https://github.com/fujidaiti/smooth_sheets/issues/14
testWidgets('Opening keyboard does not interrupt sheet animation',
(tester) async {
final controller = SheetController();
final sheetKey = GlobalKey();
final keyboardSimulationKey = GlobalKey<KeyboardInsetSimulationState>();

await tester.pumpWidget(
_TestApp(
useMaterial: true,
child: KeyboardInsetSimulation(
key: keyboardSimulationKey,
keyboardHeight: 200,
child: DraggableSheet(
key: sheetKey,
controller: controller,
minExtent: const Extent.pixels(200),
initialExtent: const Extent.pixels(200),
child: const Material(
child: _TestSheetContent(
height: 500,
),
),
),
),
),
);

expect(controller.value.pixels, 200,
reason: 'The sheet should be at the initial extent.');
expect(controller.value.minPixels < controller.value.maxPixels, isTrue,
reason: 'The sheet should be draggable.');

// Start animating the sheet to the max extent.
unawaited(
controller.animateTo(
const Extent.proportional(1),
duration: const Duration(milliseconds: 250),
),
);
// Then, show the keyboard while the animation is running.
unawaited(
keyboardSimulationKey.currentState!
.showKeyboard(const Duration(milliseconds: 250)),
);
await tester.pumpAndSettle();
expect(MediaQuery.viewInsetsOf(sheetKey.currentContext!).bottom, 200,
reason: 'The keyboard should be fully shown.');
expect(
controller.value.pixels,
controller.value.maxPixels,
reason: 'After the keyboard is fully shown, '
'the entire sheet should also be visible.',
);
});
}
Loading

0 comments on commit b9bf455

Please sign in to comment.