diff --git a/packages/flutter/lib/src/material/scrollbar.dart b/packages/flutter/lib/src/material/scrollbar.dart index 8cdc51f56535..b56fa1a16b85 100644 --- a/packages/flutter/lib/src/material/scrollbar.dart +++ b/packages/flutter/lib/src/material/scrollbar.dart @@ -77,6 +77,7 @@ class Scrollbar extends StatelessWidget { required this.child, this.controller, this.isAlwaysShown, + this.trackVisibility, this.showTrackOnHover, this.hoverThickness, this.thickness, @@ -95,11 +96,26 @@ class Scrollbar extends StatelessWidget { /// {@macro flutter.widgets.Scrollbar.isAlwaysShown} final bool? isAlwaysShown; + /// Controls the track visibility. + /// + /// If this property is null, then [ScrollbarThemeData.trackVisibility] of + /// [ThemeData.scrollbarTheme] is used. If that is also null, the default value + /// is false. + /// + /// If the track visibility is related to the scrollbar's material state, + /// use the global [ScrollbarThemeData.trackVisibility] or override the + /// sub-tree's theme data. + /// + /// [showTrackOnHover] can be replaced by this and will be deprecated. + final bool? trackVisibility; + /// Controls if the track will show on hover and remain, including during drag. /// /// If this property is null, then [ScrollbarThemeData.showTrackOnHover] of /// [ThemeData.scrollbarTheme] is used. If that is also null, the default value /// is false. + /// + /// This will be deprecated, and [trackVisibility] is recommended. final bool? showTrackOnHover; /// The thickness of the scrollbar when a hover state is active and @@ -153,6 +169,7 @@ class Scrollbar extends StatelessWidget { return _MaterialScrollbar( controller: controller, isAlwaysShown: isAlwaysShown, + trackVisibility: trackVisibility, showTrackOnHover: showTrackOnHover, hoverThickness: hoverThickness, thickness: thickness, @@ -171,6 +188,7 @@ class _MaterialScrollbar extends RawScrollbar { required Widget child, ScrollController? controller, bool? isAlwaysShown, + this.trackVisibility, this.showTrackOnHover, this.hoverThickness, double? thickness, @@ -193,6 +211,7 @@ class _MaterialScrollbar extends RawScrollbar { scrollbarOrientation: scrollbarOrientation, ); + final bool? trackVisibility; final bool? showTrackOnHover; final double? hoverThickness; @@ -217,6 +236,13 @@ class _MaterialScrollbarState extends RawScrollbarState<_MaterialScrollbar> { bool get _showTrackOnHover => widget.showTrackOnHover ?? _scrollbarTheme.showTrackOnHover ?? false; + MaterialStateProperty get _trackVisibility => MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.hovered) && _showTrackOnHover) { + return true; + } + return widget.trackVisibility ?? _scrollbarTheme.trackVisibility?.resolve(states) ?? false; + }); + Set get _states => { if (_dragIsActive) MaterialState.dragged, if (_hoverIsActive) MaterialState.hovered, @@ -251,7 +277,7 @@ class _MaterialScrollbarState extends RawScrollbarState<_MaterialScrollbar> { // If the track is visible, the thumb color hover animation is ignored and // changes immediately. - if (states.contains(MaterialState.hovered) && _showTrackOnHover) + if (_trackVisibility.resolve(states)) return _scrollbarTheme.thumbColor?.resolve(states) ?? hoverColor; return Color.lerp( @@ -266,7 +292,7 @@ class _MaterialScrollbarState extends RawScrollbarState<_MaterialScrollbar> { final Color onSurface = _colorScheme.onSurface; final Brightness brightness = _colorScheme.brightness; return MaterialStateProperty.resolveWith((Set states) { - if (states.contains(MaterialState.hovered) && _showTrackOnHover) { + if (_trackVisibility.resolve(states)) { return _scrollbarTheme.trackColor?.resolve(states) ?? (brightness == Brightness.light ? onSurface.withOpacity(0.03) @@ -280,7 +306,7 @@ class _MaterialScrollbarState extends RawScrollbarState<_MaterialScrollbar> { final Color onSurface = _colorScheme.onSurface; final Brightness brightness = _colorScheme.brightness; return MaterialStateProperty.resolveWith((Set states) { - if (states.contains(MaterialState.hovered) && _showTrackOnHover) { + if (_trackVisibility.resolve(states)) { return _scrollbarTheme.trackBorderColor?.resolve(states) ?? (brightness == Brightness.light ? onSurface.withOpacity(0.1) @@ -292,7 +318,7 @@ class _MaterialScrollbarState extends RawScrollbarState<_MaterialScrollbar> { MaterialStateProperty get _thickness { return MaterialStateProperty.resolveWith((Set states) { - if (states.contains(MaterialState.hovered) && _showTrackOnHover) + if (states.contains(MaterialState.hovered) && _trackVisibility.resolve(states)) return widget.hoverThickness ?? _scrollbarTheme.thickness?.resolve(states) ?? _kScrollbarThicknessWithTrack; diff --git a/packages/flutter/lib/src/material/scrollbar_theme.dart b/packages/flutter/lib/src/material/scrollbar_theme.dart index 02c9414d37ac..b16b3bf074d9 100644 --- a/packages/flutter/lib/src/material/scrollbar_theme.dart +++ b/packages/flutter/lib/src/material/scrollbar_theme.dart @@ -32,6 +32,7 @@ class ScrollbarThemeData with Diagnosticable { /// Creates a theme that can be used for [ThemeData.scrollbarTheme]. const ScrollbarThemeData({ this.thickness, + this.trackVisibility, this.showTrackOnHover, this.isAlwaysShown, this.radius, @@ -51,6 +52,10 @@ class ScrollbarThemeData with Diagnosticable { /// * [MaterialState.hovered] on web and desktop platforms. final MaterialStateProperty? thickness; + /// Overrides the default value of [Scrollbar.trackVisibility] in all + /// descendant [Scrollbar] widgets. + final MaterialStateProperty? trackVisibility; + /// Overrides the default value of [Scrollbar.showTrackOnHover] in all /// descendant [Scrollbar] widgets. final bool? showTrackOnHover; @@ -122,6 +127,7 @@ class ScrollbarThemeData with Diagnosticable { /// new values. ScrollbarThemeData copyWith({ MaterialStateProperty? thickness, + MaterialStateProperty? trackVisibility, bool? showTrackOnHover, bool? isAlwaysShown, bool? interactive, @@ -135,6 +141,7 @@ class ScrollbarThemeData with Diagnosticable { }) { return ScrollbarThemeData( thickness: thickness ?? this.thickness, + trackVisibility: trackVisibility ?? this.trackVisibility, showTrackOnHover: showTrackOnHover ?? this.showTrackOnHover, isAlwaysShown: isAlwaysShown ?? this.isAlwaysShown, interactive: interactive ?? this.interactive, @@ -157,9 +164,10 @@ class ScrollbarThemeData with Diagnosticable { assert(t != null); return ScrollbarThemeData( thickness: _lerpProperties(a?.thickness, b?.thickness, t, lerpDouble), - showTrackOnHover: t < 0.5 ? a?.showTrackOnHover : b?.showTrackOnHover, - isAlwaysShown: t < 0.5 ? a?.isAlwaysShown : b?.isAlwaysShown, - interactive: t < 0.5 ? a?.interactive : b?.interactive, + trackVisibility: _lerpProperties(a?.trackVisibility, b?.trackVisibility, t, _lerpBool), + showTrackOnHover: _lerpBool(a?.showTrackOnHover, b?.showTrackOnHover, t), + isAlwaysShown: _lerpBool(a?.isAlwaysShown, b?.isAlwaysShown, t), + interactive: _lerpBool(a?.interactive, b?.interactive, t), radius: Radius.lerp(a?.radius, b?.radius, t), thumbColor: _lerpProperties(a?.thumbColor, b?.thumbColor, t, Color.lerp), trackColor: _lerpProperties(a?.trackColor, b?.trackColor, t, Color.lerp), @@ -174,6 +182,7 @@ class ScrollbarThemeData with Diagnosticable { int get hashCode { return hashValues( thickness, + trackVisibility, showTrackOnHover, isAlwaysShown, interactive, @@ -195,6 +204,7 @@ class ScrollbarThemeData with Diagnosticable { return false; return other is ScrollbarThemeData && other.thickness == thickness + && other.trackVisibility == trackVisibility && other.showTrackOnHover == showTrackOnHover && other.isAlwaysShown == isAlwaysShown && other.interactive == interactive @@ -211,6 +221,7 @@ class ScrollbarThemeData with Diagnosticable { void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(DiagnosticsProperty>('thickness', thickness, defaultValue: null)); + properties.add(DiagnosticsProperty>('trackVisibility', trackVisibility, defaultValue: null)); properties.add(DiagnosticsProperty('showTrackOnHover', showTrackOnHover, defaultValue: null)); properties.add(DiagnosticsProperty('isAlwaysShown', isAlwaysShown, defaultValue: null)); properties.add(DiagnosticsProperty('interactive', interactive, defaultValue: null)); @@ -252,6 +263,8 @@ class _LerpProperties implements MaterialStateProperty { } } +bool? _lerpBool(bool? a, bool? b, double t) => t < 0.5 ? a : b; + /// Applies a scrollbar theme to descendant [Scrollbar] widgets. /// /// Descendant widgets obtain the current theme's [ScrollbarThemeData] using diff --git a/packages/flutter/test/material/scrollbar_theme_test.dart b/packages/flutter/test/material/scrollbar_theme_test.dart index 7aa97ab38093..1032e32e40ac 100644 --- a/packages/flutter/test/material/scrollbar_theme_test.dart +++ b/packages/flutter/test/material/scrollbar_theme_test.dart @@ -574,6 +574,52 @@ void main() { }), ); + testWidgets('ScrollbarThemeData.trackVisibility test', (WidgetTester tester) async { + final ScrollController scrollController = ScrollController(); + bool? _getTrackVisibility(Set states) { + return true; + } + await tester.pumpWidget( + MaterialApp( + theme: ThemeData().copyWith( + scrollbarTheme: _scrollbarTheme( + trackVisibility: MaterialStateProperty.resolveWith(_getTrackVisibility), + ), + ), + home: ScrollConfiguration( + behavior: const NoScrollbarBehavior(), + child: Scrollbar( + isAlwaysShown: true, + showTrackOnHover: true, + controller: scrollController, + child: SingleChildScrollView( + controller: scrollController, + child: const SizedBox(width: 4000.0, height: 4000.0), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + expect( + find.byType(Scrollbar), + paints + ..rect(color: const Color(0x08000000)) + ..line( + strokeWidth: 1.0, + color: const Color(0x1a000000), + ) + ..rrect(color: const Color(0xff4caf50)), + ); + }, variant: const TargetPlatformVariant({ + TargetPlatform.linux, + TargetPlatform.macOS, + TargetPlatform.windows, + TargetPlatform.fuchsia, + }), + ); + testWidgets('Default ScrollbarTheme debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const ScrollbarThemeData().debugFillProperties(builder); @@ -636,6 +682,7 @@ class NoScrollbarBehavior extends ScrollBehavior { ScrollbarThemeData _scrollbarTheme({ MaterialStateProperty? thickness, + MaterialStateProperty? trackVisibility, bool showTrackOnHover = true, bool isAlwaysShown = true, Radius radius = const Radius.circular(6.0), @@ -648,6 +695,7 @@ ScrollbarThemeData _scrollbarTheme({ }) { return ScrollbarThemeData( thickness: thickness ?? MaterialStateProperty.resolveWith(_getThickness), + trackVisibility: trackVisibility, showTrackOnHover: showTrackOnHover, isAlwaysShown: isAlwaysShown, radius: radius,