Repository navigation
[cupertino_ui] Dim disabled CupertinoSlider controls - #13419
auto-submit[bot] merged 3 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request updates CupertinoSlider to dim when disabled by wrapping it in an Opacity widget, matching the native iOS appearance. It also adds tests to verify the visual and functional behavior of the disabled slider and updates existing semantics tests. The review feedback suggests conditionally applying the Opacity widget only when the slider is disabled to avoid unnecessary widget tree overhead, and updating the tests to assert the absence of the Opacity widget when the slider is enabled.
| return Opacity( | ||
| opacity: widget.onChanged == null ? _kDisabledOpacity : 1.0, | ||
| child: _CupertinoSliderRenderObjectWidget( | ||
| value: (widget.value - widget.min) / (widget.max - widget.min), | ||
| divisions: widget.divisions, | ||
| activeColor: CupertinoDynamicColor.resolve( | ||
| widget.activeColor ?? CupertinoTheme.of(context).primaryColor, | ||
| context, | ||
| ), | ||
| thumbColor: widget.thumbColor, | ||
| onChanged: widget.onChanged != null ? _handleChanged : null, | ||
| onChangeStart: widget.onChangeStart != null ? _handleDragStart : null, | ||
| onChangeEnd: widget.onChangeEnd != null ? _handleDragEnd : null, | ||
| vsync: this, | ||
| ), | ||
| thumbColor: widget.thumbColor, | ||
| onChanged: widget.onChanged != null ? _handleChanged : null, | ||
| onChangeStart: widget.onChangeStart != null ? _handleDragStart : null, | ||
| onChangeEnd: widget.onChangeEnd != null ? _handleDragEnd : null, | ||
| vsync: this, | ||
| ); |
There was a problem hiding this comment.
Wrapping the slider in an Opacity widget when it is enabled (i.e., onChanged != null) introduces unnecessary overhead in the widget tree. In Flutter, it is a best practice to conditionally wrap widgets in Opacity only when the opacity is actually less than 1.0 (similar to how CupertinoButton handles disabledOpacity). This keeps the widget tree shallower and avoids any potential performance cost of the Opacity widget during normal enabled use.
final Widget result = _CupertinoSliderRenderObjectWidget(
value: (widget.value - widget.min) / (widget.max - widget.min),
divisions: widget.divisions,
activeColor: CupertinoDynamicColor.resolve(
widget.activeColor ?? CupertinoTheme.of(context).primaryColor,
context,
),
thumbColor: widget.thumbColor,
onChanged: widget.onChanged != null ? _handleChanged : null,
onChangeStart: widget.onChangeStart != null ? _handleDragStart : null,
onChangeEnd: widget.onChangeEnd != null ? _handleDragEnd : null,
vsync: this,
);
if (widget.onChanged == null) {
return Opacity(
opacity: _kDisabledOpacity,
child: result,
);
}
return result;There was a problem hiding this comment.
I think we should keep the wrapper here. Opacity at 1.0 already uses a fast path without an intermediate buffer. Adding and removing it would recreate the slider render object whenever enabled changes. Flutter also recommends keeping the subtree structure stable, and CupertinoSwitch uses the same approach.
I added checks in 4b4cde2 to verify that the render object stays the same after disabling and re-enabling, in both themes.
| final Finder opacity = find.descendant( | ||
| of: find.byType(CupertinoSlider), | ||
| matching: find.byType(Opacity), | ||
| ); | ||
| expect(tester.widget<Opacity>(opacity).opacity, 1.0); | ||
| final Size enabledSize = tester.getSize(find.byType(CupertinoSlider)); | ||
|
|
||
| updateState(() => enabled = false); | ||
| await tester.pump(); | ||
| expect(tester.widget<Opacity>(opacity).opacity, 0.5); | ||
| expect(tester.getSize(find.byType(CupertinoSlider)), enabledSize); | ||
| final CupertinoSlider slider = tester.widget<CupertinoSlider>(find.byType(CupertinoSlider)); | ||
| expect(slider.activeColor, CupertinoColors.systemPurple); | ||
| expect(slider.thumbColor, CupertinoColors.systemOrange); | ||
| await tester.dragFrom(tester.getCenter(find.byType(CupertinoSlider)), const Offset(50, 0)); | ||
| expect(value, 0.5); | ||
|
|
||
| updateState(() => enabled = true); | ||
| await tester.pump(); | ||
| expect(tester.widget<Opacity>(opacity).opacity, 1.0); | ||
| await tester.dragFrom(tester.getCenter(find.byType(CupertinoSlider)), const Offset(50, 0)); | ||
| expect(value, greaterThan(0.5)); |
There was a problem hiding this comment.
Since the Opacity widget is now conditionally applied only when the slider is disabled, update the tests to assert that no Opacity widget is present when the slider is enabled.
expect(find.byType(Opacity), findsNothing);
final Size enabledSize = tester.getSize(find.byType(CupertinoSlider));
updateState(() => enabled = false);
await tester.pump();
final Finder opacity = find.descendant(
of: find.byType(CupertinoSlider),
matching: find.byType(Opacity),
);
expect(tester.widget<Opacity>(opacity).opacity, 0.5);
expect(tester.getSize(find.byType(CupertinoSlider)), enabledSize);
final CupertinoSlider slider = tester.widget<CupertinoSlider>(find.byType(CupertinoSlider));
expect(slider.activeColor, CupertinoColors.systemPurple);
expect(slider.thumbColor, CupertinoColors.systemOrange);
await tester.dragFrom(tester.getCenter(find.byType(CupertinoSlider)), const Offset(50, 0));
expect(value, 0.5);
updateState(() => enabled = true);
await tester.pump();
expect(find.byType(Opacity), findsNothing);
await tester.dragFrom(tester.getCenter(find.byType(CupertinoSlider)), const Offset(50, 0));
expect(value, greaterThan(0.5));There was a problem hiding this comment.
This depends on the conditional-wrapper suggestion above, which I have not adopted. The tests continue checking opacity 1.0 / 0.5 / 1.0 and now also check that the slider render object is preserved across both transitions. Slider tests, targeted analysis, and formatting pass.
…r#196450) flutter/packages@653628e...14f9933 2026-11-12 [email protected] [material_ui] Fix SnackBarThemeData.lerp dropping showCloseIcon (flutter/packages#13449) 2026-11-11 [email protected] [ci] Update repo for 3.50 stable release (flutter/packages#13628) 2026-11-11 [email protected] [vector_graphics] Use font-family lists as a family with fallbacks (flutter/packages#13552) 2026-11-11 [email protected] Roll Flutter (stable) from c197ca5 to ab29120 (1647 revisions) (flutter/packages#13625) 2026-11-11 [email protected] [shared_prferences] Adopts mockito-kotlin in tests (flutter/packages#13624) 2026-11-11 [email protected] [cupertino_ui] Dim disabled CupertinoSlider controls (flutter/packages#13419) 2026-11-11 [email protected] [flutter_svg] Invalidate pending cache entries on clear and evict (flutter/packages#13595) 2026-11-11 [email protected] [cupertino_ui] Fix the tinted button's fill opacity (flutter/packages#13436) 2026-11-11 [email protected] [various] Support SVG filters (blur, offset, feMerge) (flutter/packages#11909) If this roll has caused a breakage, revert this CL and set the roller to dry run mode using the controls here: https://autoroll.skia.org/r/flutter-packages-flutter-autoroll Please CC [email protected] on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: 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
Dims
CupertinoSliderto 50% opacity whenonChangedis null, matching the disabled appearance in native UIKit Slider component on iOS. Adds light/dark regression tests for disabling and re-enabling the slider.Fixes flutter/flutter#106691.
Screenshots
Flutter before/after: iPhone 16 Pro simulator, iOS 18.6, light mode, value 0.67. The top slider is enabled; the bottom is disabled. The native reference shows stock
UISlidercontrols in both themes.Also checked native UIKit on iOS 26.5, where the disabled visual element uses 0.5 opacity. This fixes disabled styling, not the slider's overall design.
Validation: all 23 tests in
test/slider_test.dartpass; targeted analysis and formatting pass. The full package suite has not been run locally. No public API changes.Pre-Review Checklist
[shared_preferences]///).If you need help, consider asking for advice on the #hackers-new channel on Discord.
Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the
gemini-code-assistbot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.Footnotes
Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling. ↩ ↩2