Skip to content

Gradient subclasses' scale methods lose the GradientTransform #163972

@Zabadam

Description

@Zabadam

Steps to reproduce

  1. Construct a Gradient with some arbitrary GradientTransform.
  2. Create a second gradient using the scale() method on the first, using any value for factor. Note that a factor such as 0.9999 should return a gradient nearly identical to the original and 1.0 should return one visually identical.

Expected results

The gradient should scale according to factor but otherwise retain all its other properties, considering that the scale methods are documented to only affect the colors property. They do not necessarily only affect colors by superclass design, but the comments on LinearGradient.scale, for example:

Returns a new [LinearGradient] with its colors scaled by the given factor. [...] Since the alpha component of the Color is what is scaled, a factor of 0.0 or less results in a gradient that is fully transparent.

This is very similar result as the relatively new Gradient.withOpacity() method, but theoretically other Gradient subclasses could induce different effects on their properties with scale() if desired while withOpacity() is very clearly named and purposed.

At any rate, I do not expect the transform property to entirely go missing.

Actual results

The GradientTransform provided at construction is lost when the scale() method internally constructs a new gradient without a transform. Even when factor is 1.0, the method still constructs a new gradient internally and drops the transform.

[Perhaps if factor is 1.0 the original gradient should be returned unfettered anyway; same applies to withOpacity().]

Code sample

Code sample
import 'package:flutter/material.dart';

const scale = 1.0;
const transformedGradient = LinearGradient(
  colors: [Color(0xFFCC5555), Color(0xFF55BB55), Color(0xFF5555CC)],
  transform: GradientRotation(0.7853981634),
);
final scaledTransformedGradient = transformedGradient.scale(scale);

void main() => runApp(const GradientScaleBug());

class GradientScaleBug extends StatelessWidget {
  const GradientScaleBug({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: Stack(children: [
          Row(children: [
            buildGradientContainer(
              transformedGradient,
              'transformedGradient',
              'transform: ${transformedGradient.transform}',
            ),
            const SizedBox(width: 4),
            buildGradientContainer(
              scaledTransformedGradient,
              'transformedGradient.scale(${scale.toStringAsFixed(5)})',
              'transform: ${scaledTransformedGradient.transform}',
            ),
          ]),
          const Center(child: AnimatedGradientScale()),
        ]),
      ),
    );
  }

  Expanded buildGradientContainer(Gradient gradient, String title, String subtitle) =>
      Expanded(
          child: Container(
              decoration: BoxDecoration(gradient: gradient),
              padding: const EdgeInsets.all(15),
              child: Column(
                  mainAxisAlignment: MainAxisAlignment.spaceAround,
                  children: [
                    Text(title, style: const TextStyle(fontSize: 32)),
                    Text(subtitle, style: const TextStyle(fontSize: 20)),
                  ])));
}

class AnimatedGradientScale extends StatefulWidget {
  const AnimatedGradientScale({super.key});
  @override
  State<AnimatedGradientScale> createState() => _AnimatedGradientScaleState();
}

class _AnimatedGradientScaleState extends State<AnimatedGradientScale> with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
        vsync: this, duration: const Duration(milliseconds: 2000))
      ..repeat(reverse: true);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final animation = CurvedAnimation(parent: _controller, curve: Interval(0.5, 1.0));
    return AnimatedBuilder(
      animation: animation,
      builder: (_, __) {
        final value = (1.0 - animation.value);
        return Container(
            width: 200,
            height: 200,
            decoration: BoxDecoration(
                gradient: value == 1
                    ? transformedGradient
                    : transformedGradient.scale(value)),
            child: Center(child: Text(
                    value == 1 ? 'Not scaled' : value.toStringAsFixed(3))));
    });
  }
}

Media

Screenshots / Video demonstration
gradient_scale_drops_transform.mp4

Flutter Doctor output

Doctor output
[√] Flutter (Channel stable, 3.29.0, on Microsoft Windows [Version 10.0.19045.5011], locale en-US) [939ms]
    • Flutter version 3.29.0 on channel stable at C:\src\flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 35c388afb5 (10 days ago), 2025-02-10 12:48:41 -0800
    • Engine revision f73bfc4522
    • Dart version 3.7.0
    • DevTools version 2.42.2

[√] Windows Version (10 Pro 64-bit, 22H2, 2009) [16.4s]

[√] Android toolchain - develop for Android devices (Android SDK version 34.0.0) [8.4s]
    • Android SDK at C:\src\android
    • Platform android-35, build-tools 34.0.0
    • ANDROID_SDK_ROOT = C:\src\android
    • Java binary at: C:\Program Files\Android\Android Studio\jbr\bin\java
      This is the JDK bundled with the latest Android Studio installation on this machine.
      To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`.
    • Java version OpenJDK Runtime Environment (build 17.0.7+0-b2043.56-10550314)
    • All Android licenses accepted.

[√] Chrome - develop for the web [201ms]
    • Chrome at C:\Program Files (x86)\Google\Chrome\Application\chrome.exe

[√] Visual Studio - develop Windows apps (Visual Studio Community 2022 17.8.5) [199ms]
    • Visual Studio at C:\Program Files\Microsoft Visual Studio\2022\Community
    • Visual Studio Community 2022 version 17.8.34511.84
    • Windows 10 SDK version 10.0.20348.0

[√] Android Studio (version 2023.1) [69ms]
    • Android Studio at C:\Program Files\Android\Android Studio
    • Flutter plugin can be installed from:
       https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
       https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 17.0.7+0-b2043.56-10550314)

[√] VS Code, 64-bit edition (version 1.97.1) [65ms]
    • VS Code at C:\Program Files\Microsoft VS Code
    • Flutter extension version 3.105.20250203

[√] Connected device (4 available) [1,068ms]
    • SM G986U1 (mobile) • XXX.XXX.XXX.XXX:5555 • android-arm64  • Android 13 (API 33)
    • Windows (desktop)  • windows              • windows-x64    • Microsoft Windows [Version 10.0.19045.5011]
    • Chrome (web)       • chrome               • web-javascript • Google Chrome 127.0.6533.90
    • Edge (web)         • edge                 • web-javascript • Microsoft Edge 132.0.2957.140

[√] Network resources [680ms]
    • All expected network resources are available.

• No issues found!

Notes

I have had this thought in my head: the Gradients could benefit from a public copyWith() method. A lot of code out there constructs a new gradient by hand.

The abstract copyWith could mandate at least colors, stops and transform (leaving out tileMode since it is a unique property on each subclass anyway). Then the abstract scale and withOpacity methods could be implemented on the base Gradient as something like:

Gradient scale(double factor) => factor == 1.0
	? this
	: copyWith(colors: <Color>[for (final Color color in colors) Color.lerp(null, color, factor)!]);

Gradient withOpacity(double opacity) => opacity == 1.0
	? this
	: copyWith(colors: <Color>[for (final Color color in colors) color.withOpacity(opacity)]);

A Gradient subclass need only implement the copyWith and the scale and withOpacity could come for free...
as Gradient objects. They could be casted to appropriate subclass.

LinearGradient scale(double factor) => super.scale(factor) as LinearGradient;

Currently subclasses need to implement both scale and withOpacity, prone to the same error as already present: droppping properties such as transform.

Just a thought. I give the gradients copyWith methods in an extension in a package of mine and it is really handy for gradient-centered design work. (Some of us are gradient-crazy!)

Metadata

Metadata

Assignees

Labels

P2Important issues not at the top of the work listfound in release: 3.29Found to occur in 3.29found in release: 3.30Found to occur in 3.30frameworkflutter/packages/flutter repository. See also f: labels.has reproducible stepsThe issue has been confirmed reproducible and is ready to work onr: fixedIssue is closed as already fixed in a newer versionteam-frameworkOwned by Framework teamtriaged-frameworkTriaged by Framework team

Type

No type

Projects

Status

Done (PR merged)

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions