Fix ShapeDecoration.lerp crash when interpolating between gradient and color#187368
Conversation
…d color ShapeDecoration.color and ShapeDecoration.gradient are mutually exclusive (enforced by an assert in the constructor). ShapeDecoration.lerp computed Color.lerp(...) and Gradient.lerp(...) independently, so when interpolating between a decoration that uses a color and one that uses a gradient, both results were non-null mid-transition, tripping the assert: 'package:flutter/src/painting/shape_decoration.dart': Failed assertion: '!(color != null && gradient != null)': is not true. Resolve the conflict by keeping the source's fill type (color or gradient) before the half-way point and the target's after it, so only one of the two is ever set. Adds a regression test covering both lerp directions. Fixes flutter#93953
f8e38a6 to
9493a86
Compare
There was a problem hiding this comment.
Code Review
This pull request defers the dismissal of an open dropdown menu during orientation changes to after the current frame to avoid mutating the Navigator during build. It also resolves a conflict when interpolating ShapeDecoration between a color and a gradient by selecting the source's fill type before the half-way point and the target's after it, preventing assertion failures. Corresponding tests are added for both changes. Feedback suggests optimizing ShapeDecoration.lerp by avoiding redundant and potentially expensive gradient or color interpolation calculations when they will be discarded based on the interpolation value t.
| Color? color = Color.lerp(a?.color, b?.color, t); | ||
| Gradient? gradient = Gradient.lerp(a?.gradient, b?.gradient, t); | ||
| // A ShapeDecoration's color and gradient are mutually exclusive (see the | ||
| // assert in the constructor). When interpolating between a decoration that | ||
| // uses a color and one that uses a gradient, both lerped values can be | ||
| // non-null at the same time. Resolve the conflict by using the source's | ||
| // fill type before the half-way point and the target's after it. | ||
| // See https://github.com/flutter/flutter/issues/93953 | ||
| if (color != null && gradient != null) { | ||
| if (t < 0.5) { | ||
| color = a?.color == null ? null : color; | ||
| gradient = a?.gradient == null ? null : gradient; | ||
| } else { | ||
| color = b?.color == null ? null : color; | ||
| gradient = b?.gradient == null ? null : gradient; | ||
| } | ||
| } |
There was a problem hiding this comment.
When interpolating between a ShapeDecoration with a color and one with a gradient, both Color.lerp and Gradient.lerp are computed, only for one of them to be immediately discarded. Gradients in particular can be expensive to interpolate.
We can optimize this by checking if there is a conflict beforehand, and only computing the required lerp depending on whether t < 0.5.
Color? color;
Gradient? gradient;
if ((a?.color != null || b?.color != null) && (a?.gradient != null || b?.gradient != null)) {
if (t < 0.5) {
if (a?.color != null) {
color = Color.lerp(a!.color, b?.color, t);
} else if (a?.gradient != null) {
gradient = Gradient.lerp(a!.gradient, b?.gradient, t);
}
} else {
if (b?.color != null) {
color = Color.lerp(a?.color, b!.color, t);
} else if (b?.gradient != null) {
gradient = Gradient.lerp(a?.gradient, b!.gradient, t);
}
}
} else {
color = Color.lerp(a?.color, b?.color, t);
gradient = Gradient.lerp(a?.gradient, b?.gradient, t);
}There was a problem hiding this comment.
While this addresses the crash, it isn't an ideal result, since both halves of the transition are static and there's a sudden jump between the halves. I think we should convert the full color to a gradient with a uniform color, and utilize the Gradient.lerp to lerp it. The biggest obstacle of this solution might be that there isn't a way to create such a gradient. My suggestion is to add a new method,
Gradient fromColor(Color color);
which returns a gradient by replacing all colors in its colors with the input color. Then we can lerp between the two gradients.
What do you think?
Fix ShapeDecoration.lerp crash interpolating between gradient and color ShapeDecoration.color and ShapeDecoration.gradient are mutually exclusive (enforced by an assert in the constructor). ShapeDecoration.lerp computed Color.lerp(...) and Gradient.lerp(...) independently, so when interpolating between a decoration that uses a color and one that uses a gradient, both results were non-null mid-transition, tripping the assert: 'package:flutter/src/painting/shape_decoration.dart': Failed assertion: '!(color != null && gradient != null)': is not true. To interpolate smoothly (instead of snapping between fill types), the color is now represented as a uniform-color gradient built from the other side's gradient, and the two are interpolated gradient-to-gradient via Gradient.lerp. This adds Gradient.fromColor(Color), which returns a copy of the gradient with every entry in `colors` replaced by the given color while preserving the rest of its geometry. It has a concrete default on the base class (returning a uniform LinearGradient, which is sufficient because a uniform gradient paints as a solid color regardless of geometry) so it is non-breaking for out-of-tree Gradient subclasses; LinearGradient, RadialGradient, and SweepGradient override it to retain their own geometry. Adds tests for Gradient.fromColor on each gradient type and updates the ShapeDecoration.lerp regression test for the smooth behavior. Fixes flutter#93953
Thanks, agreed. The static halves with a jump at the midpoint weren't ideal. I've reworked it along the lines you suggested. I added Gradient.fromColor(Color), which returns a copy of the gradient with every entry in colors replaced by the given color while preserving stops, tileMode, transform, and the subclass-specific positioning. ShapeDecoration.lerp now, when one side is a color and the other a gradient, builds a uniform gradient from the other side via fromColor and interpolates gradient-to-gradient with Gradient.lerp, so the result is a single gradient (and a smooth fade) throughout the transition. The pure color↔color and gradient↔gradient paths are unchanged. One deviation from your sketch: to keep it non-breaking for out-of-tree Gradient subclasses, fromColor has a concrete default on the base class (returning a uniform LinearGradient, which is fine since a uniform gradient paints as a solid color regardless of geometry) rather than being abstract; LinearGradient/RadialGradient/SweepGradient override it to retain their geometry. Happy to make it abstract instead if you'd prefer. Added unit tests for fromColor on each gradient type and updated the ShapeDecoration.lerp regression test. Also fine to rename (withColor, etc.) if you have a preference. Suggestions Appreciated. |
dkwingsmt
left a comment
There was a problem hiding this comment.
This is perfect. Thank you so much!
flutter/flutter@c0a1129...8bdce07 2026-06-11 bernaferrari2@gmail.com Make shape border lerp symmetric (flutter/flutter#187282) 2026-06-11 matt.kosarek@canonical.com Sized to content for regular and dialog windows on win32 (flutter/flutter#186829) 2026-06-11 jason-simmons@users.noreply.github.com Ensure that directory names are typed as strings in the CIPD package YAML file generated by merge_and_upload_debug_symbols.py (flutter/flutter#187813) 2026-06-11 stuartmorgan@google.com Add core-packages to ecosystem triage (flutter/flutter#187796) 2026-06-11 engine-flutter-autoroll@skia.org Roll Fuchsia Linux SDK from 8azSyvz57mKcPqTwk... to 2KosSR4ONUjIB7tP_... (flutter/flutter#187842) 2026-06-11 ishaquehassan@gmail.com Document moveStep direction on WidgetController.dragUntilVisible (flutter/flutter#186943) 2026-06-11 ahmedsameha1@gmail.com Add more 0x0 size tests part 11 (flutter/flutter#186822) 2026-06-10 kumarshivam72@gmail.com Fix ShapeDecoration.lerp crash when interpolating between gradient and color (flutter/flutter#187368) 2026-06-10 codedoctor@linwood.dev Reland "Add support for stylus buttons" (flutter/flutter#187629) 2026-06-10 tanyabouman@gmail.com Api docs: typo fix in Navigator (flutter/flutter#187572) 2026-06-10 engine-flutter-autoroll@skia.org Roll Packages from bd297cf to 1b56cde (4 revisions) (flutter/flutter#187784) 2026-06-10 116356835+AbdeMohlbi@users.noreply.github.com Improve docs on MediaQuery: highContrast, invertColors and disableAnimations (flutter/flutter#186614) 2026-06-10 matt.boetger@gmail.com [Android] Test to verify AnnounceSemanticsEvent deprecation warning on API 36 (flutter/flutter#187754) 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 louisehsu@google.com,stuartmorgan@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
…d color (flutter#187368) ## Description `ShapeDecoration.color` and `ShapeDecoration.gradient` are mutually exclusive — the constructor asserts `!(color != null && gradient != null)`. However, [`ShapeDecoration.lerp`] computed `Color.lerp(...)` and `Gradient.lerp(...)` independently. When animating between a `ShapeDecoration` that uses a **color** and one that uses a **gradient**, both interpolated values are non-null mid-transition, which trips the assert and crashes: ``` 'package:flutter/src/painting/shape_decoration.dart': Failed assertion: line 76 pos 14: '!(color != null && gradient != null)': is not true. ``` This reproduces with a simple `DecorationTween`/`TweenAnimationBuilder` between a color `ShapeDecoration` and a gradient `ShapeDecoration` (see the linked issue). `BoxDecoration` is unaffected because it does not enforce the mutual-exclusion invariant. ### Fix Resolve the conflict in `ShapeDecoration.lerp`: when both the lerped color and the lerped gradient would be non-null, keep the **source's** fill type before the half-way point (`t < 0.5`) and the **target's** after it, so only one of `color`/`gradient` is ever set. The end points (`t == 0.0`/`t == 1.0`) are unchanged. ### Tests Added `ShapeDecoration.lerp between gradient and color does not throw`, which verifies the end points are preserved, that exactly one of color/gradient is set on each side of the midpoint, and that interpolating across the full range (in both directions) never throws. The test fails on `master` (assertion) and passes with this change. ## Related Issues Fixes flutter#93953 ## 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. - [x] 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]. - [x] All existing and new tests are passing. [Contributor Guide]: https://github.com/flutter/flutter/blob/master/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/master/docs/contributing/Tree-hygiene.md [Flutter Style Guide]: https://github.com/flutter/flutter/blob/master/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/master/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [test-exempt]: https://github.com/flutter/flutter/blob/master/docs/contributing/Tree-hygiene.md#tests --------- Co-authored-by: Tong Mu <dkwingsmt@users.noreply.github.com>
Description
ShapeDecoration.colorandShapeDecoration.gradientare mutually exclusive — the constructor asserts!(color != null && gradient != null). However, [ShapeDecoration.lerp] computedColor.lerp(...)andGradient.lerp(...)independently. When animating between aShapeDecorationthat uses a color and one that uses a gradient, both interpolated values are non-null mid-transition, which trips the assert and crashes:This reproduces with a simple
DecorationTween/TweenAnimationBuilderbetween a colorShapeDecorationand a gradientShapeDecoration(see the linked issue).BoxDecorationis unaffected because it does not enforce the mutual-exclusion invariant.Fix
Resolve the conflict in
ShapeDecoration.lerp: when both the lerped color and the lerped gradient would be non-null, keep the source's fill type before the half-way point (t < 0.5) and the target's after it, so only one ofcolor/gradientis ever set. The end points (t == 0.0/t == 1.0) are unchanged.Tests
Added
ShapeDecoration.lerp between gradient and color does not throw, which verifies the end points are preserved, that exactly one of color/gradient is set on each side of the midpoint, and that interpolating across the full range (in both directions) never throws. The test fails onmaster(assertion) and passes with this change.Related Issues
Fixes #93953
Pre-launch Checklist
///).