Repository navigation
ToggleableStateMixin reaction animation duration does not respect overrides #190866
Copy link
Copy link
Closed
Closed
Copy link
Labels
P2Important issues not at the top of the work listImportant issues not at the top of the work listframeworkflutter/packages/flutter repository. See also f: labels.flutter/packages/flutter repository. See also f: labels.team-frameworkOwned by Framework teamOwned by Framework teamtriaged-frameworkTriaged by Framework teamTriaged by Framework teamwaiting for PR to land (fixed)A fix is in flightA fix is in flight
Description
Steps to reproduce
- Make a class that uses ToggleableStateMixin
- Make a painter that extends ToggleablePainter
- Override reactionAnimationDuration to set a desired duration different from the 100ms default.
- Set the painter's reaction to track the Toggleable state reaction
Expected results
Override applies, so painter's reaction duration matches override duration.
Actual results
Duration always 100ms.
Code sample
Code sample
import 'package:flutter/material.dart';
void main() => runApp(const ToggleableOverrideDemoApp());
class ToggleableOverrideDemoApp extends StatelessWidget {
const ToggleableOverrideDemoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: const [
Padding(
padding: EdgeInsets.all(24),
child: Text(
'ToggleableStateMixin.reactionAnimationDuration bug\n\n'
'Both toggles override reactionAnimationDuration to 3 seconds.\n'
'PRESS AND HOLD each box and watch how fast the ripple expands.',
textAlign: TextAlign.center,
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
DemoToggle(
label: 'Current\n(BUG: ripple takes ~100ms)',
applyWorkaround: false,
),
SizedBox(width: 64),
DemoToggle(
label: 'Expected (done with workaround)\n(expected: ripple takes 3s)',
applyWorkaround: true,
),
],
),
],
),
),
),
);
}
}
class DemoToggle extends StatefulWidget {
const DemoToggle({super.key, required this.label, required this.applyWorkaround});
final String label;
final bool applyWorkaround;
@override
State<DemoToggle> createState() => _DemoToggleState();
}
class _DemoToggleState extends State<DemoToggle>
with TickerProviderStateMixin, ToggleableStateMixin {
final _RippleBoxPainter _painter = _RippleBoxPainter();
bool _value = false;
// Silently ignored.
@override
Duration? get reactionAnimationDuration => const Duration(seconds: 3);
@override
void initState() {
super.initState();
if (widget.applyWorkaround) {
// Manually does what the override above should have done.
reactionController.duration = const Duration(seconds: 3);
}
}
@override
ValueChanged<bool?>? get onChanged => (bool? newValue) {
setState(() => _value = newValue ?? false);
animateToValue();
};
@override
bool? get value => _value;
@override
bool get tristate => false;
@override
void dispose() {
_painter.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
_painter
..position = position
..reaction = reaction
..activeColor = Colors.indigo
..inactiveColor = Colors.grey;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(widget.label, textAlign: TextAlign.center),
buildToggleable(size: const Size(120, 120), painter: _painter),
Text(
'reactionController.duration:\n'
'${reactionController.duration!.inMilliseconds} ms',
textAlign: TextAlign.center,
style: const TextStyle(fontFamily: 'monospace', fontSize: 12),
),
],
);
}
}
class _RippleBoxPainter extends ToggleablePainter {
static const double _maxRippleRadius = 52;
@override
void paint(Canvas canvas, Size size) {
final Offset center = size.center(Offset.zero);
// The press ripple, driven by the mixin's reaction animation.
if (!reaction.isDismissed) {
canvas.drawCircle(
center,
_maxRippleRadius * reaction.value,
Paint()..color = activeColor.withValues(alpha: 0.35),
);
}
// The toggle box itself, filled according to the position animation.
final RRect box = RRect.fromRectAndRadius(
Rect.fromCenter(center: center, width: 36, height: 36),
const Radius.circular(8),
);
canvas.drawRRect(
box,
Paint()..color = Color.lerp(inactiveColor, activeColor, position.value)!,
);
}
}Screenshots or Video
Screenshots / Video demonstration
Logs
Logs
Flutter Doctor output
Doctor output
Doctor summary (to see all details, run flutter doctor -v):
[!] Flutter (Channel master, 3.47.0-1.0.pre-316, on macOS 26.5 25F71 darwin-arm64, locale en-US)
! Upstream repository [email protected]:victorsanni/flutter.git is not a standard remote.
Set environment variable "FLUTTER_GIT_URL" to [email protected]:victorsanni/flutter.git to dismiss this error.
[!] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
✗ Flutter requires Android SDK 36 and the Android BuildTools 28.0.3
To update the Android SDK visit https://flutter.dev/to/macos-android-setup for detailed instructions.
! Some Android licenses not accepted. To resolve this, run: flutter doctor --android-licenses
[!] Xcode - develop for iOS and macOS (Xcode 26.6)
! CocoaPods 1.14.3 out of date (1.16.2 is recommended).
CocoaPods is a package manager for iOS or macOS platform code.
Without CocoaPods, plugins will not work on iOS or macOS.
For more info, see https://flutter.dev/to/platform-plugins
To update CocoaPods, see https://guides.cocoapods.org/using/getting-started.html#updating-cocoapods
[✓] Chrome - develop for the web
[✓] Connected device (2 available)
! Error: Browsing on the local area network for Victor’s iPhone. Ensure the device is unlocked and attached with a cable or associated with the same local
area network as this Mac.
The device must be opted into Developer Mode to connect wirelessly. (code -27)
[✓] Network resources
! Doctor found issues in 3 categories.Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
P2Important issues not at the top of the work listImportant issues not at the top of the work listframeworkflutter/packages/flutter repository. See also f: labels.flutter/packages/flutter repository. See also f: labels.team-frameworkOwned by Framework teamOwned by Framework teamtriaged-frameworkTriaged by Framework teamTriaged by Framework teamwaiting for PR to land (fixed)A fix is in flightA fix is in flight
Activity
[-]ToggleableStateMixin reaction animation duration is not overridable[/-][+]ToggleableStateMixin reaction animation duration does not respect overrides[/+]Toggleable reaction duration respects overrides (flutter#190857)
github-actions commentedon Aug 26, 2026
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 -vand a minimal reproduction of the issue.