Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,12 @@ class _PredictiveBackSharedElementPageTransitionState
begin: switch (widget.currentBackEvent?.swipeEdge) {
SwipeEdge.left => Offset(xShift, _getYShiftPosition(screenSize.height)),
SwipeEdge.right => Offset(-xShift, _getYShiftPosition(screenSize.height)),
null => Offset(xShift, _getYShiftPosition(screenSize.height)),
// Button-triggered back (Android BackEvent.EDGE_NONE) has no swipe
// direction. Button events are filtered out in handleStartBackGesture,
// so this is a safe fallback matching the null (no event) case.
// `_` keeps the switch exhaustive if additional SwipeEdge values
// are added in the Flutter SDK.
Comment on lines +462 to +463

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chunhtai I wonder what you think about this given that your RFC flutter/rfc#32 recommends against adding values to enums (see the framework side of this PR in flutter/flutter#185140). Should we migrate to a class-based SwipeEdge, or should we work with what we have for now?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should stay an enum as it is closed by design (at most four directions)

@chunhtai chunhtai Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unless Android start to support micro direction like bottom-left top-left. I doubt that will happen though, better not over-engineering now.

_ => Offset(xShift, _getYShiftPosition(screenSize.height)),
Comment thread
muhammadkamel marked this conversation as resolved.
Resolved
},
end: Offset.zero,
),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
changelog: |
- Handles unknown swipe edges and button-triggered back events in `PredictiveBackPageTransitionsBuilder`.
version: patch
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,146 @@ void main() {
});
}

testWidgets(
'PredictiveBackPageTransitionsBuilder supports right-edge swipe (page slides from right)',
(WidgetTester tester) async {
const PageTransitionsBuilder pageTransitionsBuilder = PredictiveBackPageTransitionsBuilder();
final routes = <String, WidgetBuilder>{
'/': (BuildContext context) => Material(
child: TextButton(
child: const Text('push'),
onPressed: () => Navigator.of(context).pushNamed('/b'),
),
),
'/b': (BuildContext context) => const Text('page b'),
};

await tester.pumpWidget(
MaterialApp(
theme: ThemeData(
pageTransitionsTheme: PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{
for (final TargetPlatform platform in TargetPlatform.values)
platform: pageTransitionsBuilder,
},
),
),
routes: routes,
),
);

await tester.tap(find.text('push'));
await tester.pumpAndSettle();

// Start back gesture from the right edge (swipeEdge: 1).
final ByteData startMessage = const StandardMethodCodec().encodeMethodCall(
const MethodCall('startBackGesture', <String, dynamic>{
'touchOffset': <double>[800.0, 300.0],
'progress': 0.0,
'swipeEdge': 1, // right
}),
);
await binding.defaultBinaryMessenger.handlePlatformMessage(
'flutter/backgesture',
startMessage,
(ByteData? _) {},
);
await tester.pump();

expect(_findPredictiveBackPageTransition(pageTransitionsBuilder), findsOneWidget);
Comment thread
muhammadkamel marked this conversation as resolved.
Resolved
expect(_findFallbackPageTransition(pageTransitionsBuilder), findsNothing);

// Drag from right edge — page should shift to the left (negative dx).
final ByteData updateMessage = const StandardMethodCodec().encodeMethodCall(
const MethodCall('updateBackGestureProgress', <String, dynamic>{
'touchOffset': <double>[700.0, 300.0],
'progress': 0.35,
'swipeEdge': 1, // right
}),
);
await binding.defaultBinaryMessenger.handlePlatformMessage(
'flutter/backgesture',
updateMessage,
(ByteData? _) {},
);
await tester.pumpAndSettle();

// The page animates during a right-edge swipe. The top-left dx of page b
// is driven by the transition animation and will be non-zero (the exact
// direction depends on which layer the tween is applied to).
final Offset pageBOffset = tester.getTopLeft(find.text('page b'));
expect(pageBOffset.dx, isNot(0.0));
Comment thread
muhammadkamel marked this conversation as resolved.
Resolved

// Commit the gesture.
final ByteData commitMessage = const StandardMethodCodec().encodeMethodCall(
const MethodCall('commitBackGesture'),
);
await binding.defaultBinaryMessenger.handlePlatformMessage(
'flutter/backgesture',
commitMessage,
(ByteData? _) {},
);
await tester.pumpAndSettle();

expect(find.text('push'), findsOneWidget);
expect(find.text('page b'), findsNothing);
},
variant: TargetPlatformVariant.only(TargetPlatform.android),
);

testWidgets('button-triggered back does not start a predictive back animation', (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Maybe change this to say "system button triggered", since you are talking about Android's system back button, right? To disambiguate it with a Flutter button.

WidgetTester tester,
) async {
const PageTransitionsBuilder pageTransitionsBuilder = PredictiveBackPageTransitionsBuilder();
final routes = <String, WidgetBuilder>{
'/': (BuildContext context) => Material(
child: TextButton(
child: const Text('push'),
onPressed: () => Navigator.of(context).pushNamed('/b'),
),
),
'/b': (BuildContext context) => const Text('page b'),
};

await tester.pumpWidget(
MaterialApp(
theme: ThemeData(
pageTransitionsTheme: PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{
for (final TargetPlatform platform in TargetPlatform.values)
platform: pageTransitionsBuilder,
},
),
),
routes: routes,
),
);

await tester.tap(find.text('push'));
await tester.pumpAndSettle();

// Android button-triggered back events report a zero touch offset and
// zero progress. handleStartBackGesture treats these as isButtonEvent
// and does not start a predictive animation.
final ByteData startMessage = const StandardMethodCodec().encodeMethodCall(
const MethodCall('startBackGesture', <String, dynamic>{
'touchOffset': <double>[0.0, 0.0],
'progress': 0.0,
'swipeEdge': 0,
}),
);
await binding.defaultBinaryMessenger.handlePlatformMessage(
'flutter/backgesture',
startMessage,
(ByteData? _) {},
);
await tester.pump();

// The predictive back transition must NOT have started.
expect(_findPredictiveBackPageTransition(pageTransitionsBuilder), findsNothing);
expect(_findFallbackPageTransition(pageTransitionsBuilder), findsOneWidget);
}, variant: TargetPlatformVariant.only(TargetPlatform.android));

testWidgets('PredictiveBackPageTransitionsBuilder uses fallbackColor', (
WidgetTester tester,
) async {
Expand Down
Loading