Skip to content

Fix ShapeDecoration.lerp crash when interpolating between gradient and color#187368

Merged
auto-submit[bot] merged 3 commits into
flutter:masterfrom
ishivamg:fix/93953-shapedecoration-lerp-gradient-color
Jun 11, 2026
Merged

Fix ShapeDecoration.lerp crash when interpolating between gradient and color#187368
auto-submit[bot] merged 3 commits into
flutter:masterfrom
ishivamg:fix/93953-shapedecoration-lerp-gradient-color

Conversation

@ishivamg

@ishivamg ishivamg commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

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 #93953

Pre-launch Checklist

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

@github-actions github-actions Bot added framework flutter/packages/flutter repository. See also f: labels. f: material design flutter/packages/flutter/material repository. labels Jun 1, 2026
…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
@ishivamg ishivamg force-pushed the fix/93953-shapedecoration-lerp-gradient-color branch from f8e38a6 to 9493a86 Compare June 1, 2026 06:52
@github-actions github-actions Bot removed the f: material design flutter/packages/flutter/material repository. label Jun 1, 2026

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

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.

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.

Comment on lines +231 to +247
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;
}
}

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.

medium

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);
    }

@Piinks Piinks requested a review from dkwingsmt June 2, 2026 22:22

@dkwingsmt dkwingsmt left a comment

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.

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
@ishivamg

Copy link
Copy Markdown
Contributor Author

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?

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 dkwingsmt left a comment

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 is perfect. Thank you so much!

@dkwingsmt dkwingsmt added the CICD Run CI/CD label Jun 10, 2026
@github-actions github-actions Bot removed the CICD Run CI/CD label Jun 10, 2026
@AbdeMohlbi AbdeMohlbi added the CICD Run CI/CD label Jun 10, 2026
@dkwingsmt dkwingsmt added the autosubmit Merge PR when tree becomes green via auto submit App label Jun 10, 2026
@auto-submit auto-submit Bot added this pull request to the merge queue Jun 10, 2026
Merged via the queue into flutter:master with commit b79192e Jun 11, 2026
94 checks passed
@flutter-dashboard flutter-dashboard Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Jun 11, 2026
auto-submit Bot pushed a commit to flutter/packages that referenced this pull request Jun 11, 2026
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
via-guy pushed a commit to via-guy/flutter that referenced this pull request Jun 26, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CICD Run CI/CD framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ShapeDecoration lerp from gradient to color assert error

3 participants