Unverified Commit 09008e6f authored by Kate Lovett's avatar Kate Lovett Committed by GitHub

Add ScrollbarTheme/ScrollbarThemeData (#72308)

parent cbe72db1
......@@ -115,6 +115,7 @@ export 'src/material/refresh_indicator.dart';
export 'src/material/reorderable_list.dart';
export 'src/material/scaffold.dart';
export 'src/material/scrollbar.dart';
export 'src/material/scrollbar_theme.dart';
export 'src/material/search.dart';
export 'src/material/selectable_text.dart';
export 'src/material/shadows.dart';
......
......@@ -8,6 +8,7 @@ import 'package:flutter/widgets.dart';
import 'color_scheme.dart';
import 'material_state.dart';
import 'scrollbar_theme.dart';
import 'theme.dart';
const double _kScrollbarThickness = 8.0;
......@@ -37,6 +38,7 @@ const Duration _kScrollbarTimeToFade = Duration(milliseconds: 600);
///
/// * [RawScrollbar], a basic scrollbar that fades in and out, extended
/// by this class to add more animations and behaviors.
/// * [ScrollbarTheme], which configures the Scrollbar's appearance.
/// * [CupertinoScrollbar], an iOS style scrollbar.
/// * [ListView], which displays a linear, scrollable list of children.
/// * [GridView], which displays a 2 dimensional, scrollable array of children.
......@@ -59,8 +61,8 @@ class Scrollbar extends RawScrollbar {
Key? key,
required Widget child,
ScrollController? controller,
bool isAlwaysShown = false,
this.showTrackOnHover = false,
bool? isAlwaysShown,
this.showTrackOnHover,
this.hoverThickness,
double? thickness,
Radius? radius,
......@@ -78,13 +80,17 @@ class Scrollbar extends RawScrollbar {
/// Controls if the track will show on hover and remain, including during drag.
///
/// Defaults to false, cannot be null.
final bool showTrackOnHover;
/// If this property is null, then [ScrollbarThemeData.showTrackOnHover] of
/// [ThemeData.scrollbarTheme] is used. If that is also null, the default value
/// is false.
final bool? showTrackOnHover;
/// The thickness of the scrollbar when a hover state is active and
/// [showTrackOnHover] is true.
///
/// Defaults to 12.0 dp when null.
/// If this property is null, then [ScrollbarThemeData.thickness] of
/// [ThemeData.scrollbarTheme] is used to resolve a thickness. If that is also
/// null, the default value is 12.0 pixels.
final double? hoverThickness;
@override
......@@ -96,9 +102,15 @@ class _ScrollbarState extends RawScrollbarState<Scrollbar> {
bool _dragIsActive = false;
bool _hoverIsActive = false;
late ColorScheme _colorScheme;
late ScrollbarThemeData _scrollbarTheme;
// On Android, scrollbars should match native appearance.
late bool _useAndroidScrollbar;
@override
bool get showScrollbar => widget.isAlwaysShown ?? _scrollbarTheme.isAlwaysShown ?? false;
bool get _showTrackOnHover => widget.showTrackOnHover ?? _scrollbarTheme.showTrackOnHover ?? false;
Set<MaterialState> get _states => <MaterialState>{
if (_dragIsActive) MaterialState.dragged,
if (_hoverIsActive) MaterialState.hovered,
......@@ -125,16 +137,16 @@ class _ScrollbarState extends RawScrollbarState<Scrollbar> {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.dragged))
return dragColor;
return _scrollbarTheme.thumbColor?.resolve(states) ?? dragColor;
// If the track is visible, the thumb color hover animation is ignored and
// changes immediately.
if (states.contains(MaterialState.hovered) && widget.showTrackOnHover)
return hoverColor;
if (states.contains(MaterialState.hovered) && _showTrackOnHover)
return _scrollbarTheme.thumbColor?.resolve(states) ?? hoverColor;
return Color.lerp(
idleColor,
hoverColor,
_scrollbarTheme.thumbColor?.resolve(states) ?? idleColor,
_scrollbarTheme.thumbColor?.resolve(states) ?? hoverColor,
_hoverAnimationController.value,
)!;
});
......@@ -144,10 +156,11 @@ class _ScrollbarState extends RawScrollbarState<Scrollbar> {
final Color onSurface = _colorScheme.onSurface;
final Brightness brightness = _colorScheme.brightness;
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.hovered) && widget.showTrackOnHover) {
return brightness == Brightness.light
? onSurface.withOpacity(0.03)
: onSurface.withOpacity(0.05);
if (states.contains(MaterialState.hovered) && _showTrackOnHover) {
return _scrollbarTheme.trackColor?.resolve(states)
?? (brightness == Brightness.light
? onSurface.withOpacity(0.03)
: onSurface.withOpacity(0.05));
}
return const Color(0x00000000);
});
......@@ -157,10 +170,11 @@ class _ScrollbarState extends RawScrollbarState<Scrollbar> {
final Color onSurface = _colorScheme.onSurface;
final Brightness brightness = _colorScheme.brightness;
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.hovered) && widget.showTrackOnHover) {
return brightness == Brightness.light
? onSurface.withOpacity(0.1)
: onSurface.withOpacity(0.25);
if (states.contains(MaterialState.hovered) && _showTrackOnHover) {
return _scrollbarTheme.trackBorderColor?.resolve(states)
?? (brightness == Brightness.light
? onSurface.withOpacity(0.1)
: onSurface.withOpacity(0.25));
}
return const Color(0x00000000);
});
......@@ -168,10 +182,14 @@ class _ScrollbarState extends RawScrollbarState<Scrollbar> {
MaterialStateProperty<double> get _thickness {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.hovered) && widget.showTrackOnHover)
return widget.hoverThickness ?? _kScrollbarThicknessWithTrack;
if (states.contains(MaterialState.hovered) && _showTrackOnHover)
return widget.hoverThickness
?? _scrollbarTheme.thickness?.resolve(states)
?? _kScrollbarThicknessWithTrack;
// The default scrollbar thickness is smaller on mobile.
return widget.thickness ?? (_kScrollbarThickness / (_useAndroidScrollbar ? 2 : 1));
return widget.thickness
?? _scrollbarTheme.thickness?.resolve(states)
?? (_kScrollbarThickness / (_useAndroidScrollbar ? 2 : 1));
});
}
......@@ -190,6 +208,8 @@ class _ScrollbarState extends RawScrollbarState<Scrollbar> {
@override
void didChangeDependencies() {
final ThemeData theme = Theme.of(context);
_colorScheme = theme.colorScheme;
_scrollbarTheme = theme.scrollbarTheme;
switch (theme.platform) {
case TargetPlatform.android:
_useAndroidScrollbar = true;
......@@ -207,16 +227,16 @@ class _ScrollbarState extends RawScrollbarState<Scrollbar> {
@override
void updateScrollbarPainter() {
_colorScheme = Theme.of(context).colorScheme;
scrollbarPainter
..color = _thumbColor.resolve(_states)
..trackColor = _trackColor.resolve(_states)
..trackBorderColor = _trackBorderColor.resolve(_states)
..textDirection = Directionality.of(context)
..thickness = _thickness.resolve(_states)
..radius = widget.radius ?? (_useAndroidScrollbar ? null : _kScrollbarRadius)
..crossAxisMargin = (_useAndroidScrollbar ? 0.0 : _kScrollbarMargin)
..minLength = _kScrollbarMinLength
..radius = widget.radius ?? _scrollbarTheme.radius ?? (_useAndroidScrollbar ? null : _kScrollbarRadius)
..crossAxisMargin = _scrollbarTheme.crossAxisMargin ?? (_useAndroidScrollbar ? 0.0 : _kScrollbarMargin)
..mainAxisMargin = _scrollbarTheme.mainAxisMargin ?? 0.0
..minLength = _scrollbarTheme.minThumbLength ?? _kScrollbarMinLength
..padding = MediaQuery.of(context).padding;
}
......
This diff is collapsed.
......@@ -34,6 +34,7 @@ import 'outlined_button_theme.dart';
import 'page_transitions_theme.dart';
import 'popup_menu_theme.dart';
import 'radio_theme.dart';
import 'scrollbar_theme.dart';
import 'slider_theme.dart';
import 'snack_bar_theme.dart';
import 'switch_theme.dart';
......@@ -278,6 +279,7 @@ class ThemeData with Diagnosticable {
bool? applyElevationOverlayColor,
PageTransitionsTheme? pageTransitionsTheme,
AppBarTheme? appBarTheme,
ScrollbarThemeData? scrollbarTheme,
BottomAppBarTheme? bottomAppBarTheme,
ColorScheme? colorScheme,
DialogTheme? dialogTheme,
......@@ -410,6 +412,7 @@ class ThemeData with Diagnosticable {
tabBarTheme ??= const TabBarTheme();
tooltipTheme ??= const TooltipThemeData();
appBarTheme ??= const AppBarTheme();
scrollbarTheme ??= const ScrollbarThemeData();
bottomAppBarTheme ??= const BottomAppBarTheme();
cardTheme ??= const CardTheme();
chipTheme ??= ChipThemeData.fromDefaults(
......@@ -493,6 +496,7 @@ class ThemeData with Diagnosticable {
applyElevationOverlayColor: applyElevationOverlayColor,
pageTransitionsTheme: pageTransitionsTheme,
appBarTheme: appBarTheme,
scrollbarTheme: scrollbarTheme,
bottomAppBarTheme: bottomAppBarTheme,
colorScheme: colorScheme,
dialogTheme: dialogTheme,
......@@ -583,6 +587,7 @@ class ThemeData with Diagnosticable {
required this.applyElevationOverlayColor,
required this.pageTransitionsTheme,
required this.appBarTheme,
required this.scrollbarTheme,
required this.bottomAppBarTheme,
required this.colorScheme,
required this.dialogTheme,
......@@ -657,6 +662,7 @@ class ThemeData with Diagnosticable {
assert(materialTapTargetSize != null),
assert(pageTransitionsTheme != null),
assert(appBarTheme != null),
assert(scrollbarTheme != null),
assert(bottomAppBarTheme != null),
assert(colorScheme != null),
assert(dialogTheme != null),
......@@ -1085,6 +1091,9 @@ class ThemeData with Diagnosticable {
/// textTheme of [AppBar]s.
final AppBarTheme appBarTheme;
/// A theme for customizing the colors, thickness, and shape of [Scrollbar]s.
final ScrollbarThemeData scrollbarTheme;
/// A theme for customizing the shape, elevation, and color of a [BottomAppBar].
final BottomAppBarTheme bottomAppBarTheme;
......@@ -1270,6 +1279,7 @@ class ThemeData with Diagnosticable {
bool? applyElevationOverlayColor,
PageTransitionsTheme? pageTransitionsTheme,
AppBarTheme? appBarTheme,
ScrollbarThemeData? scrollbarTheme,
BottomAppBarTheme? bottomAppBarTheme,
ColorScheme? colorScheme,
DialogTheme? dialogTheme,
......@@ -1353,6 +1363,7 @@ class ThemeData with Diagnosticable {
applyElevationOverlayColor: applyElevationOverlayColor ?? this.applyElevationOverlayColor,
pageTransitionsTheme: pageTransitionsTheme ?? this.pageTransitionsTheme,
appBarTheme: appBarTheme ?? this.appBarTheme,
scrollbarTheme: scrollbarTheme ?? this.scrollbarTheme,
bottomAppBarTheme: bottomAppBarTheme ?? this.bottomAppBarTheme,
colorScheme: (colorScheme ?? this.colorScheme).copyWith(brightness: brightness),
dialogTheme: dialogTheme ?? this.dialogTheme,
......@@ -1510,6 +1521,7 @@ class ThemeData with Diagnosticable {
applyElevationOverlayColor: t < 0.5 ? a.applyElevationOverlayColor : b.applyElevationOverlayColor,
pageTransitionsTheme: t < 0.5 ? a.pageTransitionsTheme : b.pageTransitionsTheme,
appBarTheme: AppBarTheme.lerp(a.appBarTheme, b.appBarTheme, t),
scrollbarTheme: ScrollbarThemeData.lerp(a.scrollbarTheme, b.scrollbarTheme, t),
bottomAppBarTheme: BottomAppBarTheme.lerp(a.bottomAppBarTheme, b.bottomAppBarTheme, t),
colorScheme: ColorScheme.lerp(a.colorScheme, b.colorScheme, t),
dialogTheme: DialogTheme.lerp(a.dialogTheme, b.dialogTheme, t),
......@@ -1595,6 +1607,7 @@ class ThemeData with Diagnosticable {
&& other.applyElevationOverlayColor == applyElevationOverlayColor
&& other.pageTransitionsTheme == pageTransitionsTheme
&& other.appBarTheme == appBarTheme
&& other.scrollbarTheme == scrollbarTheme
&& other.bottomAppBarTheme == bottomAppBarTheme
&& other.colorScheme == colorScheme
&& other.dialogTheme == dialogTheme
......@@ -1679,6 +1692,7 @@ class ThemeData with Diagnosticable {
applyElevationOverlayColor,
pageTransitionsTheme,
appBarTheme,
scrollbarTheme,
bottomAppBarTheme,
colorScheme,
dialogTheme,
......@@ -1760,6 +1774,7 @@ class ThemeData with Diagnosticable {
properties.add(DiagnosticsProperty<bool>('applyElevationOverlayColor', applyElevationOverlayColor, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<PageTransitionsTheme>('pageTransitionsTheme', pageTransitionsTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<AppBarTheme>('appBarTheme', appBarTheme, defaultValue: defaultData.appBarTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<ScrollbarThemeData>('ScrollbarTheme', scrollbarTheme, defaultValue: defaultData.scrollbarTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<BottomAppBarTheme>('bottomAppBarTheme', bottomAppBarTheme, defaultValue: defaultData.bottomAppBarTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<ColorScheme>('colorScheme', colorScheme, defaultValue: defaultData.colorScheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<DialogTheme>('dialogTheme', dialogTheme, defaultValue: defaultData.dialogTheme, level: DiagnosticLevel.debug));
......
......@@ -600,15 +600,14 @@ class RawScrollbar extends StatefulWidget {
Key? key,
required this.child,
this.controller,
this.isAlwaysShown = false,
this.isAlwaysShown,
this.radius,
this.thickness,
this.thumbColor,
this.fadeDuration = _kScrollbarFadeDuration,
this.timeToFade = _kScrollbarTimeToFade,
this.pressDuration = Duration.zero,
}) : assert(isAlwaysShown != null),
assert(child != null),
}) : assert(child != null),
assert(fadeDuration != null),
assert(timeToFade != null),
assert(pressDuration != null),
......@@ -683,7 +682,7 @@ class RawScrollbar extends StatefulWidget {
/// [controller] property has not been set, the [PrimaryScrollController] will
/// be used.
///
/// Defaults to false.
/// Defaults to false when null.
///
/// {@tool snippet}
///
......@@ -728,7 +727,7 @@ class RawScrollbar extends StatefulWidget {
/// }
/// ```
/// {@end-tool}
final bool isAlwaysShown;
final bool? isAlwaysShown;
/// The [Radius] of the scrollbar thumb's rounded rectangle corners.
///
......@@ -790,6 +789,14 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
@protected
late final ScrollbarPainter scrollbarPainter;
/// Overridable getter to indicate that the scrollbar should be visible, even
/// when a scroll is not underway.
///
/// Subclasses can override this getter to make its value depend on an inherited
/// theme.
@protected
bool get showScrollbar => widget.isAlwaysShown ?? false;
@override
void initState() {
super.initState();
......@@ -820,7 +827,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
// A scroll event is required in order to paint the thumb.
void _maybeTriggerScrollbar() {
WidgetsBinding.instance!.addPostFrameCallback((Duration duration) {
if (widget.isAlwaysShown) {
if (showScrollbar) {
_fadeoutTimer?.cancel();
// Wait one frame and cause an empty scroll event. This allows the
// thumb to show immediately when isAlwaysShown is true. A scroll
......@@ -881,7 +888,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
}
void _maybeStartFadeoutTimer() {
if (!widget.isAlwaysShown) {
if (!showScrollbar) {
_fadeoutTimer?.cancel();
_fadeoutTimer = Timer(widget.timeToFade, () {
_fadeoutAnimationController.reverse();
......
This diff is collapsed.
......@@ -742,6 +742,7 @@ void main() {
applyElevationOverlayColor: false,
pageTransitionsTheme: pageTransitionTheme,
appBarTheme: const AppBarTheme(color: Colors.black),
scrollbarTheme: const ScrollbarThemeData(radius: Radius.circular(10.0)),
bottomAppBarTheme: const BottomAppBarTheme(color: Colors.black),
colorScheme: const ColorScheme.light(),
dialogTheme: const DialogTheme(backgroundColor: Colors.black),
......
......@@ -292,6 +292,7 @@ void main() {
applyElevationOverlayColor: false,
pageTransitionsTheme: pageTransitionTheme,
appBarTheme: const AppBarTheme(color: Colors.black),
scrollbarTheme: const ScrollbarThemeData(radius: Radius.circular(10.0)),
bottomAppBarTheme: const BottomAppBarTheme(color: Colors.black),
colorScheme: const ColorScheme.light(),
dialogTheme: const DialogTheme(backgroundColor: Colors.black),
......@@ -384,6 +385,7 @@ void main() {
applyElevationOverlayColor: true,
pageTransitionsTheme: const PageTransitionsTheme(),
appBarTheme: const AppBarTheme(color: Colors.white),
scrollbarTheme: const ScrollbarThemeData(radius: Radius.circular(10.0)),
bottomAppBarTheme: const BottomAppBarTheme(color: Colors.white),
colorScheme: const ColorScheme.light(),
dialogTheme: const DialogTheme(backgroundColor: Colors.white),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment