Skip to content

Button overlay color does not adapt when it changes shape #114301

Description

@ferraridamiano

Steps to Reproduce

  1. Execute flutter run on the code sample with a desktop device (tried on Windows and Web)
  2. Hover with the cursor on top of the icon

Expected results:
The overlay color that is showed during the hovering adapts to the shape change in the animation

Actual results:
The overlay color that is showed during the hovering does not change during the button shape animation

flutter doctor -v
[✓] Flutter (Channel stable, 3.3.6, on Microsoft Windows [Versione 10.0.22621.755], locale it-IT)
    • Flutter version 3.3.6 on channel stable at C:\flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 6928314d50 (5 days ago), 2022-10-25 16:34:41 -0400
    • Engine revision 3ad69d7be3
    • Dart version 2.18.2
    • DevTools version 2.15.0

[✓] Android toolchain - develop for Android devices (Android SDK version 33.0.0)
    • Android SDK at C:\Users\Damiano\AppData\Local\Android\sdk
    • Platform android-33, build-tools 33.0.0
    • Java binary at: C:\Program Files\Android\Android Studio\jre\bin\java
    • Java version OpenJDK Runtime Environment (build 11.0.13+0-b1751.21-8125866)
    • All Android licenses accepted.

[✓] Chrome - develop for the web
    • Chrome at C:\Program Files\Google\Chrome\Application\chrome.exe

[✓] Visual Studio - develop for Windows (Visual Studio Community 2022 17.3.3)
    • Visual Studio at C:\Program Files\Microsoft Visual Studio\2022\Community
    • Visual Studio Community 2022 version 17.3.32825.248
    • Windows 10 SDK version 10.0.19041.0

[✓] Android Studio (version 2021.3)
    • 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 11.0.13+0-b1751.21-8125866)

[✓] VS Code (version 1.72.2)
    • VS Code at C:\Users\Damiano\AppData\Local\Programs\Microsoft VS Code
    • Flutter extension version 3.50.0

[✓] Connected device (3 available)
    • Windows (desktop) • windows • windows-x64    • Microsoft Windows [Versione 10.0.22621.755]
    • Chrome (web)      • chrome  • web-javascript • Google Chrome 107.0.5304.88
    • Edge (web)        • edge    • web-javascript • Microsoft Edge 105.0.1343.33

[✓] HTTP Host Availability
    • All required HTTP hosts are available

• No issues found!
Code sample
import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: MyWidget(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Center(child: AnimatedElevatedButton()),
    );
  }
}

class AnimatedElevatedButton extends StatefulWidget {
  const AnimatedElevatedButton({super.key});

  @override
  State<AnimatedElevatedButton> createState() => _AnimatedElevatedButtonState();
}

class _AnimatedElevatedButtonState extends State<AnimatedElevatedButton>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller = AnimationController(
    lowerBound: 0,
    upperBound: 300,
    value: 300,
    duration: const Duration(seconds: 2),
    vsync: this,
  );

  @override
  Widget build(BuildContext context) {
    return ElevatedButtonTransition(
      radius: _controller,
      onPressed: () {},
      onHover: (value) => setState(() => _controller.value = value ? 80 : 150),
    );
  }

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

class ElevatedButtonTransition extends AnimatedWidget {
  final void Function()? onPressed;
  final void Function(bool)? onHover;

  const ElevatedButtonTransition({
    super.key,
    this.onPressed,
    this.onHover,
    required AnimationController radius,
  }) : super(listenable: radius);

  Animation<double> get radius => listenable as Animation<double>;

  @override
  Widget build(BuildContext context) {
    ButtonStyle style = ButtonStyle(
      elevation: MaterialStateProperty.resolveWith<double?>(
        (Set<MaterialState> states) => 0,
      ),
      foregroundColor: MaterialStateProperty.resolveWith<Color?>(
        (Set<MaterialState> states) =>
            Theme.of(context).colorScheme.onSecondaryContainer,
      ),
      backgroundColor: MaterialStateProperty.resolveWith<Color?>(
        (Set<MaterialState> states) =>
            Theme.of(context).colorScheme.secondaryContainer,
      ),
      overlayColor: MaterialStateProperty.resolveWith<Color?>(
        (Set<MaterialState> states) =>
            states.contains(MaterialState.hovered) ? Colors.black26 : null,
      ),
      shape: MaterialStateProperty.resolveWith<OutlinedBorder?>(
        (Set<MaterialState> states) => RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(radius.value),
        ),
      ),
    );

    return ElevatedButton(
      onPressed: onPressed,
      onHover: onHover,
      style: style,
      child: const Icon(Icons.favorite, size: 300),
    );
  }
}

Activity

  1. added
    in triagePresently being triaged by the triage team
    on Oct 31, 2022
  2. exaby73 commented on Oct 31, 2022

    @exaby73
    Member

    Triage report

    I can reproduce this issue on Master (3.5.0-10.0.pre.168)

    Code sample
    import 'package:flutter/material.dart';
    
    void main() => runApp(const MyApp());
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return const MaterialApp(
          home: MyWidget(),
        );
      }
    }
    
    class MyWidget extends StatelessWidget {
      const MyWidget({super.key});
    
      @override
      Widget build(BuildContext context) {
        return const Scaffold(
          body: Center(child: AnimatedElevatedButton()),
        );
      }
    }
    
    class AnimatedElevatedButton extends StatefulWidget {
      const AnimatedElevatedButton({super.key});
    
      @override
      State<AnimatedElevatedButton> createState() => _AnimatedElevatedButtonState();
    }
    
    class _AnimatedElevatedButtonState extends State<AnimatedElevatedButton>
        with SingleTickerProviderStateMixin {
      late final AnimationController _controller = AnimationController(
        lowerBound: 0,
        upperBound: 300,
        value: 300,
        duration: const Duration(seconds: 2),
        vsync: this,
      );
    
      @override
      Widget build(BuildContext context) {
        return ElevatedButtonTransition(
          radius: _controller,
          onPressed: () {},
          onHover: (value) => setState(() => _controller.value = value ? 80 : 150),
        );
      }
    
      @override
      void dispose() {
        _controller.dispose();
        super.dispose();
      }
    }
    
    class ElevatedButtonTransition extends AnimatedWidget {
      final void Function()? onPressed;
      final void Function(bool)? onHover;
    
      const ElevatedButtonTransition({
        super.key,
        this.onPressed,
        this.onHover,
        required AnimationController radius,
      }) : super(listenable: radius);
    
      Animation<double> get radius => listenable as Animation<double>;
    
      @override
      Widget build(BuildContext context) {
        ButtonStyle style = ButtonStyle(
          elevation: MaterialStateProperty.resolveWith<double?>(
            (Set<MaterialState> states) => 0,
          ),
          foregroundColor: MaterialStateProperty.resolveWith<Color?>(
            (Set<MaterialState> states) =>
                Theme.of(context).colorScheme.onSecondaryContainer,
          ),
          backgroundColor: MaterialStateProperty.resolveWith<Color?>(
            (Set<MaterialState> states) =>
                Theme.of(context).colorScheme.secondaryContainer,
          ),
          overlayColor: MaterialStateProperty.resolveWith<Color?>(
            (Set<MaterialState> states) =>
                states.contains(MaterialState.hovered) ? Colors.black26 : null,
          ),
          shape: MaterialStateProperty.resolveWith<OutlinedBorder?>(
            (Set<MaterialState> states) => RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(radius.value),
            ),
          ),
        );
    
        return ElevatedButton(
          onPressed: onPressed,
          onHover: onHover,
          style: style,
          child: const Icon(Icons.favorite, size: 300),
        );
      }
    }
  3. added
    frameworkflutter/packages/flutter repository. See also f: labels.
    p: material_uimaterial_ui package in flutter/packages
    has reproducible stepsThe issue has been confirmed reproducible and is ready to work on
    and removed
    in triagePresently being triaged by the triage team
    on Oct 31, 2022
  4. self-assigned this
    on Nov 3, 2022
  5. HansMuller commented on Nov 3, 2022

    @HansMuller
    Contributor

    Not that it really matters, but. The code that creates the ButtonStyle could be written more simply

        final ColorScheme colorScheme = Theme.of(context).colorScheme;
        ButtonStyle style = ElevatedButton.styleFrom(
          elevation: 0,
          foregroundColor: colorScheme.onSecondaryContainer,
          backgroundColor: colorScheme.secondaryContainer,
          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(radius.value)),
        ).copyWith(
          overlayColor: MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
            return states.contains(MaterialState.hovered) ? Colors.black26 : null;
          },
        ));
  6. HansMuller commented on Nov 3, 2022

    @HansMuller
    Contributor

    This appears to be a problem with InkHighlight. When it's rebuilt with a new customBorder it doesn't update its _highlights[type] table with a new InkHighlight.

  7. moved this to To do in Nevercodeon Nov 7, 2022
  8. moved this from To do to In progress in Nevercodeon Nov 7, 2022
  9. bleroux commented on Nov 18, 2022

    @bleroux
    Contributor

    @HansMuller Thank you for the investigation on this interesting issue.

    I filed two PRs:

    I split this in two PRs because the first one is independent from this issue and will be useful even if the second one is not merged.
    The second one might not fully fix this issue and it will require some discussions.

    An ElevatedButton is a ButtonStyleButton that uses a Material parent and an InkWell child.
    When the button shape is updated:

    • Without the fix, the Material receives a new shape and updates its rendering accordingly but the InkWell highlights are not updated.
    • With the fix, both the Material and the InkWell highlights are updated.

    When allowing highlights to be updated, there is a new problem : Material animates its shape change, InkWell highlights don’t.

    Using the provided code sample, it results in the highlight growing instantly (the grey rounded rectangle in the following video) while the Material is still expanding:

    highlight_expand_before.mp4

    A workaround is to update the code sample to set ElevatedButton clipBehavior:

    with_clipBehavior.mp4
    Updated code sample (simplified + setting ElevatedButton.clipBehavior + timeDilation = 10)
    import 'package:flutter/material.dart';
    import 'package:flutter/scheduler.dart';
    
    void main() {
      timeDilation = 10;
      runApp(const MyApp());
    }
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return const MaterialApp(
          home: MyWidget(),
        );
      }
    }
    
    class MyWidget extends StatelessWidget {
      const MyWidget({super.key});
    
      @override
      Widget build(BuildContext context) {
        return const Scaffold(
          body: Center(child: AnimatedElevatedButton()),
        );
      }
    }
    
    class AnimatedElevatedButton extends StatefulWidget {
      const AnimatedElevatedButton({super.key});
    
      @override
      State<AnimatedElevatedButton> createState() => _AnimatedElevatedButtonState();
    }
    
    class _AnimatedElevatedButtonState extends State<AnimatedElevatedButton> {
      double radius = 150;
    
      @override
      Widget build(BuildContext context) {
        return ElevatedButtonTransition(
          radius: radius,
          onPressed: () {},
          onHover: (value) => setState(() => radius = value ? 30 : 150),
        );
      }
    
      @override
      void dispose() {
        super.dispose();
      }
    }
    
    class ElevatedButtonTransition extends StatelessWidget {
      final void Function()? onPressed;
      final void Function(bool)? onHover;
      final double radius;
    
      const ElevatedButtonTransition({
        super.key,
        this.onPressed,
        this.onHover,
        required this.radius,
      });
    
      @override
      Widget build(BuildContext context) {
        final ColorScheme colorScheme = Theme.of(context).colorScheme;
        ButtonStyle style = ElevatedButton.styleFrom(
          elevation: 0,
          foregroundColor: colorScheme.onSecondaryContainer,
          backgroundColor: colorScheme.secondaryContainer,
          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(radius)),
        ).copyWith(overlayColor: MaterialStateProperty.resolveWith<Color?>(
          (Set<MaterialState> states) {
            if (states.contains(MaterialState.pressed)) return Colors.red;
            if (states.contains(MaterialState.focused)) return Colors.green;
            return states.contains(MaterialState.hovered) ? Colors.black26 : null;
          },
        ));
    
        return ElevatedButton(
          onPressed: onPressed,
          onHover: onHover,
          style: style,
          clipBehavior: Clip.hardEdge,
          child: const Icon(Icons.favorite, size: 300),
        );
      }
    }
    

    @HansMuller, considering the animation problem, I’m very interested to get your insights on how we can proceed on this issue:

    1. Validate this change and emphasize the clipBehavior requirement (it will be imperfect because the clipBehavior works well when the shape expands not when it shrinks. For instance, with the provided code sample, the fix will be ok for the hover highlight, not with a focus highlight if we add one).
    2. Try to animate InkWell highlights customBorder change (it might be difficult to synchronize it with the Material animation, I don't know if it is possible to do so in a reliable way).
    3. Other directions?
  10. Repository owner moved this from In progress to Done (PR merged) in Nevercodeon Nov 18, 2022
  11. HansMuller commented on Nov 18, 2022

    @HansMuller
    Contributor

    Maybe ButtonStyleButton should create its Material with animationDuration=0? The fact that Material implicitly animates some of its properties can be really frustrating. I think what we want to do here is to effectively create a Material widget with the animation feature turned off.

    CC @goderbauer

  12. added
    r: fixedIssue is closed as already fixed in a newer version
    on Nov 21, 2022
  13. github-actions commented on Dec 5, 2022

    @github-actions

    This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.

  14. locked as resolved and limited conversation to collaborators on Dec 5, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

found in release: 3.3Found to occur in 3.3found in release: 3.4Found to occur in 3.4frameworkflutter/packages/flutter repository. See also f: labels.has reproducible stepsThe issue has been confirmed reproducible and is ready to work onp: material_uimaterial_ui package in flutter/packagesr: fixedIssue is closed as already fixed in a newer version

Type

No type

Projects

  • Status
    Done (PR merged)

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions