Skip to content

Use AnimationStyle curve and reverseCurve in ModalBottomSheet animation#181403

Merged
auto-submit[bot] merged 18 commits intoflutter:masterfrom
Mairramer:fix/modal-bottom-sheet-animation-curves
Feb 25, 2026
Merged

Use AnimationStyle curve and reverseCurve in ModalBottomSheet animation#181403
auto-submit[bot] merged 18 commits intoflutter:masterfrom
Mairramer:fix/modal-bottom-sheet-animation-curves

Conversation

@Mairramer
Copy link
Contributor

Fixes #38958
Fixes #174857

Pre-launch Checklist

If you need help, consider asking for advice on the #hackers-new channel on Discord.

Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the gemini-code-assist bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.

@github-actions github-actions bot added framework flutter/packages/flutter repository. See also f: labels. f: material design flutter/packages/flutter/material repository. labels Jan 23, 2026
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

The pull request successfully updates the ModalBottomSheet to utilize AnimationStyle for its animation curves, which is a positive step towards more flexible animation customization. New tests have been added to cover this functionality, ensuring its correctness. However, there appears to be an unintended change in the modal barrier's animation behavior.

I am having trouble creating individual review comments. Click here to see my feedback.

packages/flutter/lib/src/material/bottom_sheet.dart (1140-1142)

high

The ColorTween for the modal barrier no longer chains the CurveTween(curve: barrierCurve). This means the barrierCurve property, if still present and intended for configuration, is no longer being applied to the barrier's animation. This could be an unintended change in behavior for the modal barrier's animation, as the PR description focuses on the sheet's animation.

If barrierCurve is still a configurable property of ModalBottomSheetRoute, its removal here without an alternative mechanism to apply it would be a regression in customization.


final proxy = route.animation! as ProxyAnimation;

final curved = proxy.parent! as CurvedAnimation;
Copy link
Contributor

Choose a reason for hiding this comment

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

Not a big fan of using type casts to regression test instead of testing the actual widget behavior (e.g that the animation follows the passed-in curve rather than a default, etc.).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I find it a bit confusing how tests for these scenarios are written. Is there an easier way to do this?

Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if we can monitor the details of the animation. Maybe use a simple curve like Curves.linear and verify that the sheet is at a certain non-default height halfway through the animation (if the height of the sheet is what is animating) etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done


// Advance the animation by 1/3 of the duration.
await tester.pump(const Duration(milliseconds: 100));
expect(tester.getTopLeft(find.byKey(sheetKey)).dy, closeTo(547.3, 0.1));
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm scared of these magic numbers because they make the test prone to break (and difficult to debug) with the tiniest changes to the underlying implementation. I wonder if we can use a predictable curve such as Curves.linear and somehow express the sheet extent as a function of both the animation duration and the sheet's initial extent.

For example, at 100ms it's at x, at 200ms it's at 2x, etc. That way even if the sheet's internal implementation changes, the test still reflects the relationship between the provided curve and the sheet's extension value.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, exactly, that’s why I don’t like testing animations 😄 . I just followed other existing examples.

Copy link
Contributor

Choose a reason for hiding this comment

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

This is a test I got working for curve:

Details
  testWidgets('ModalBottomSheet uses AnimationStyle curve', (WidgetTester tester) async {
    final Key sheetKey = UniqueKey();

    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Builder(
            builder: (BuildContext context) {
              return GestureDetector(
                onTap: () {
                  showModalBottomSheet<void>(
                    context: context,
                    sheetAnimationStyle: const AnimationStyle(curve: Curves.linear),
                    builder: (BuildContext context) {
                      return SizedBox.expand(
                        key: sheetKey,
                        child: FilledButton(
                          onPressed: () {
                            Navigator.pop(context);
                          },
                          child: const Text('Close'),
                        ),
                      );
                    },
                  );
                },
                child: const Text('X'),
              );
            },
          ),
        ),
      ),
    );

    await tester.tap(find.text('X'));
    await tester.pump();

    // Advance the animation by 50ms.
    await tester.pump(const Duration(milliseconds: 50));
    final double openExtent1 = tester.getTopLeft(find.byKey(sheetKey)).dy;

    // Advance the animation by an additional 50ms.
    await tester.pump(const Duration(milliseconds: 50));
    final double openExtent2 = tester.getTopLeft(find.byKey(sheetKey)).dy;

    // Advance the animation by an additional 50ms.
    await tester.pump(const Duration(milliseconds: 50));
    final double openExtent3 = tester.getTopLeft(find.byKey(sheetKey)).dy;

    // For the linear curve, the distance covered in each time interval should
    // be the same.
    expect(openExtent1 - openExtent2, closeTo(openExtent2 - openExtent3, 0.1));
    await tester.pumpAndSettle();

    // Dismiss the bottom sheet.
    await tester.tap(find.widgetWithText(FilledButton, 'Close'));
    await tester.pumpAndSettle();
  });

Can you add a test for reverseCurve as well?

Copy link
Contributor

Choose a reason for hiding this comment

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

I recommend using Curves.decelerate instead, which is predicatble for being simply a t^2 and also being able to distinguish between t and output.

@Mairramer
Copy link
Contributor Author

I’m getting into a separate topic that we can open an issue for later, but testing widget animations in Flutter is painful. If we look at how animations are tested today, we can see that the approaches have a bit of a code smell.

I’ve been thinking about a few ideas, but I don’t have anything concrete yet. I need to study the animation API in more depth to better understand how everything works.

@Mairramer
Copy link
Contributor Author

@victorsanni How should I proceed with this PR, given that Flutter doesn’t provide a very good way to write tests for curves?

I also created a proposal addressing this, which can be found here: #181597.

@Mairramer Mairramer force-pushed the fix/modal-bottom-sheet-animation-curves branch from 857e8d9 to 6b0c7fb Compare February 3, 2026 20:32
Copy link
Contributor

@dkwingsmt dkwingsmt left a comment

Choose a reason for hiding this comment

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

LGTM except for Victor's comment.

@Mairramer Mairramer force-pushed the fix/modal-bottom-sheet-animation-curves branch from 6b0c7fb to 6842ae6 Compare February 4, 2026 11:50
@Mairramer Mairramer requested a review from victorsanni February 4, 2026 12:42
@jmagman
Copy link
Member

jmagman commented Feb 4, 2026

@dkwingsmt Google Testing failures look related to this fix rounded_bottom_sheet_test_test_rounded_bottom_sheet_test

@Mairramer Mairramer force-pushed the fix/modal-bottom-sheet-animation-curves branch from bbc0eb0 to 0179d5f Compare February 5, 2026 12:51
Copy link
Contributor

@victorsanni victorsanni left a comment

Choose a reason for hiding this comment

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

Google testing is still failing, I will rerun again. The images look like only the barrier color is changing, not the vertical extent of the bottom sheet.


final ValueNotifier<EdgeInsets> _clipDetailsNotifier = ValueNotifier<EdgeInsets>(EdgeInsets.zero);

CurvedAnimation? _animation;
Copy link
Contributor

Choose a reason for hiding this comment

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

_ModalBottomSheetState.handleDragEnd uses the const default curve, should it use the provided curves?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

IMHO, the behavior feels a bit odd. Unless someone explicitly wants this in the future, I don’t see a strong reason to change it.

Copy link
Contributor

Choose a reason for hiding this comment

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

the behavior feels a bit odd.

Can you explain further here?

My assumption is the sheet should animate with the provided animation when the drag ends. Currently it just uses its default curve for all these animations, but now that this PR overrides the default with a user-provided curve I'm wondering if that can cause discrepancies with the sheet animating one way in a certain state and a different way in another state.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I understand what you mean. Should it be reverseCurve in this case?

return _animation ??= CurvedAnimation(
parent: super.createAnimation(),
curve: sheetAnimationStyle?.curve ?? _modalBottomSheetCurve,
reverseCurve: sheetAnimationStyle?.reverseCurve ?? _modalBottomSheetCurve,
Copy link
Contributor

Choose a reason for hiding this comment

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

Should the fallback here be _modalBottomSheetCurve or _modalBottomSheetCurve.reverse?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think so.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The default animation is Curves.linear, right? I think we can use that.

Copy link
Contributor

Choose a reason for hiding this comment

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

The default animation is Curves.linear

I can't find this default, can you point out where this is set?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I might be mistaken, but the Material documentation mentions Emphasized decelerate. So we could use Easing.legacyDecelerate as the default.

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we want to change the defaults here, so it would be nice to confirm within the code what the current default is for both forward and reverse animations and ensure that this PR falls back to those defaults in all states.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Based on the Material documentation:
https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration
and
https://m3.material.io/styles/motion/easing-and-duration/tokens-specs

The token for Enter the screen is md.sys.motion.easing.emphasized.decelerate. However, Flutter currently uses md.sys.motion.easing.legacy.decelerate.

From what I can see, these tokens were introduced in Flutter in #116525.

In Material 2, the documentation describes Decelerated easing (cubic-bezier(0.0, 0.0, 0.2, 1)), which corresponds to what Flutter currently exposes as legacyDecelerate.

Given that, using Easing.legacyDecelerate as the default is correct.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let me know if I missed anything.

Copy link
Contributor

Choose a reason for hiding this comment

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

Out of scope of this PR, but we should change to the M3 spec in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Out of scope of this PR, but we should change to the M3 spec in the future.

Should we file an issue for this?

@victorsanni victorsanni self-requested a review February 5, 2026 19:27
@victorsanni
Copy link
Contributor

Google testing is still failing with a lighter modal barrier color even without setting a sheetAnimationStyle.

@victorsanni
Copy link
Contributor

Google testing is still failing with a lighter modal barrier color even without setting a sheetAnimationStyle.

Failures are image diffs, I am ok with overriding (with code owner's approval) if we have a clear understanding of why the modal barrier color is changing mid animation due to this PR.

Copy link
Contributor

@victorsanni victorsanni left a comment

Choose a reason for hiding this comment

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

Hi @Mairramer, sorry for the delay here. I have dug deeper into the showModalBottomSheet implementation and I think we can land this change by localizing the application of the sheetAnimationStyle.curve/reverseCurve.

Before this PR, ModalBottomSheetRoute used the default super.createAnimation, which creates an un-curved basic AnimationController with only the duration/reverseDuration specified (from sheetAnimationStyle.duration/reverseDuration). Making this animation curved will affect other children animations (e.g barrier color animations) since Flutter will now stack the child curves on top of the createAnimation curve.

What we want to do here instead is to apply the curves provided by sheetAnimationStyle.curve/reverseCurve only to the ModalBottomSheet (and not the barrier, etc). We can do this by supplying sheetAnimationStyle into the _ModalBottomSheet returned in ModalBottomSheetRoute.buildPage. Then, in _ModalBottomSheet, we can just replace instances of _modalBottomSheetCurve with sheetAnimationStyle?.curve ?? _modalBottomSheetCurve.

Copy link
Contributor

@victorsanni victorsanni left a comment

Choose a reason for hiding this comment

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

Requesting changes.

@auto-submit auto-submit bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Feb 23, 2026
@auto-submit
Copy link
Contributor

auto-submit bot commented Feb 23, 2026

autosubmit label was removed for flutter/flutter/181403, because The base commit of the PR is older than 7 days and can not be merged. Please merge the latest changes from the main into this branch and resubmit the PR.

@chunhtai chunhtai added the autosubmit Merge PR when tree becomes green via auto submit App label Feb 23, 2026
@auto-submit auto-submit bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Feb 23, 2026
@auto-submit
Copy link
Contributor

auto-submit bot commented Feb 23, 2026

autosubmit label was removed for flutter/flutter/181403, because - The status or check suite Google testing has failed. Please fix the issues identified (or deflake) before re-applying this label.

@chunhtai
Copy link
Contributor

looks like there is something wrong in the google test ci currently. wait a bit before retrying

@dkwingsmt dkwingsmt added the autosubmit Merge PR when tree becomes green via auto submit App label Feb 25, 2026
@auto-submit auto-submit bot added this pull request to the merge queue Feb 25, 2026
Merged via the queue into flutter:master with commit d6adb6d Feb 25, 2026
75 of 76 checks passed
@flutter-dashboard flutter-dashboard bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Feb 25, 2026
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 25, 2026
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 25, 2026
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 25, 2026
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 26, 2026
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 26, 2026
auto-submit bot pushed a commit to flutter/packages that referenced this pull request Feb 26, 2026
Roll Flutter from dad6f9d4107a to b31548feb941 (39 revisions)

flutter/flutter@dad6f9d...b31548f

2026-02-25 [email protected] [web] Fix failure on Firefox 148 (flutter/flutter#182855)
2026-02-25 [email protected] Roll Fuchsia Linux SDK from KfPgw04T0OEADLJA5... to XI0Ax7fbtYE4XKYAQ... (flutter/flutter#182887)
2026-02-25 [email protected] Use AnimationStyle curve and reverseCurve in ModalBottomSheet animation (flutter/flutter#181403)
2026-02-25 [email protected] Roll Dart SDK from fd3dce5b6a4e to 5c57e75f1102 (9 revisions) (flutter/flutter#182801)
2026-02-25 98614782+auto-submit[bot]@users.noreply.github.com Reverts "refactor: remove material in context_menu_controller_test, icon_test, list_wheel_scroll_view_test, media_query_test, platform_menu_bar_test (#182697)" (flutter/flutter#182879)
2026-02-25 [email protected] Make sure that an AnimatedSlide doesn't crash in 0x0 environment (flutter/flutter#181535)
2026-02-24 [email protected] Reland Standardize on Test* widgets in *_tester.dart files (flutter/flutter#182632)
2026-02-24 [email protected] docs(Path): clarify that zero-length contours are excluded from computeMetrics (flutter/flutter#180165)
2026-02-24 [email protected] Fix typo in assert message (flutter/flutter#182843)
2026-02-24 [email protected] [win32] Fix overflow in TaskRunnerWindow. (flutter/flutter#182822)
2026-02-24 [email protected] feat: Add --no-uninstall flag to flutter test for integration tests (flutter/flutter#182714)
2026-02-24 [email protected] Rename noFrequencyBasedMinification to useFrequencyBasedMinification (flutter/flutter#182684)
2026-02-24 [email protected] [Impeller] Fix fail to render pixel buffer texture on Linux (flutter/flutter#181656)
2026-02-24 [email protected] Remove FlutterFramework app migration (flutter/flutter#182100)
2026-02-24 [email protected] Roll Packages from 12b43a1 to 062c8d4 (5 revisions) (flutter/flutter#182839)
2026-02-24 [email protected] [web] Run webparagraph tests in CI (flutter/flutter#182092)
2026-02-24 [email protected] Fix a race in EmbedderTest.CanSpecifyCustomUITaskRunner (flutter/flutter#182649)
2026-02-24 [email protected] flutter_tools: Use a super-parameter in several missed cases (flutter/flutter#182581)
2026-02-24 [email protected] Replace more references to `flutter/engine` with `flutter/flutter` (flutter/flutter#182654)
2026-02-24 [email protected] Carousel: Migration from Scrollable+Viewport to CustomScrollView (flutter/flutter#182475)
2026-02-24 [email protected] Refactor impellerc_main to better organize some of its logic (flutter/flutter#182783)
2026-02-24 [email protected] Remove unused `getPluginList ` (flutter/flutter#182660)
2026-02-24 [email protected] Refactor: Remove material from ticker provider test (flutter/flutter#181697)
2026-02-24 [email protected] Roll Skia from 26eebffe12bd to f44d7db68805 (3 revisions) (flutter/flutter#182821)
2026-02-24 [email protected] refactor: remove material in context_menu_controller_test, icon_test, list_wheel_scroll_view_test, media_query_test, platform_menu_bar_test (flutter/flutter#182697)
2026-02-24 [email protected] Roll Skia from 7dad66aae75a to 26eebffe12bd (5 revisions) (flutter/flutter#182810)
2026-02-24 [email protected] Update roadmap for 2026 (flutter/flutter#182798)
2026-02-24 [email protected] Marks Windows tool_tests_commands_1_2 to be unflaky (flutter/flutter#179670)
2026-02-23 [email protected] [web] scroll iOS iframe text input into view (flutter/flutter#179759)
2026-02-23 [email protected] Fix textscaler clamp assertion error (flutter/flutter#181716)
2026-02-23 [email protected] Roll Skia from 9a5a3c92c336 to 7dad66aae75a (4 revisions) (flutter/flutter#182779)
2026-02-23 [email protected] Move more getters from userMessages class to the appropriate places (flutter/flutter#182656)
2026-02-23 [email protected] Manual roll Dart SDK from f8fac50475b8 to fd3dce5b6a4e (6 revisions) (flutter/flutter#182768)
2026-02-23 [email protected] Copy Flutter framework to Add to App FlutterPluginRgistrant (flutter/flutter#182523)
2026-02-23 [email protected] Add progress indicator to artifact downloads (flutter/flutter#181808)
2026-02-23 [email protected] Clarify batch release mode requirements (flutter/flutter#182228)
2026-02-23 [email protected] [web] Remove --disable-gpu from flutter chrome tests (flutter/flutter#182618)
2026-02-23 [email protected] running-apps: update running-apps to use Duration.ago() (flutter/flutter#182172)
2026-02-23 [email protected] Refactor bin/ shell scripts for better performance and safety (flutter/flutter#182674)

If this roll has caused a breakage, revert this CL and stop the roller
using the controls here:
https://autoroll.skia.org/r/flutter-packages
Please CC [email protected],[email protected] on the revert to ensure that a human
is aware of the problem.

...
ahmedsameha1 pushed a commit to ahmedsameha1/flutter that referenced this pull request Feb 27, 2026
…on (flutter#181403)

Fixes flutter#38958
Fixes flutter#174857

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md

---------

Co-authored-by: chunhtai <[email protected]>
@gaaclarke
Copy link
Member

@chunhtai
Copy link
Contributor

chunhtai commented Mar 2, 2026

@gaaclarke This is unexpected, I don't think this pr adds any assets. Is there possible change to the devices, for example xcode update

@gaaclarke
Copy link
Member

@chunhtai yea I think this is a misfire from the performance bot. It also flagged this regression to a Dart roll, which is more likely the cause.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

6 participants