Add ScrollbarTheme/ScrollbarThemeData (#72308)

This commit is contained in:
Kate Lovett 2021-01-12 19:14:04 -06:00 committed by GitHub
parent cbe72db135
commit 09008e6f71
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 919 additions and 32 deletions

View file

@ -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';

View file

@ -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;
}

View file

@ -0,0 +1,284 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' show lerpDouble;
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'material_state.dart';
import 'theme.dart';
/// Defines default property values for descendant [Scrollbar] widgets.
///
/// Descendant widgets obtain the current [ScrollbarThemeData] object with
/// `ScrollbarTheme.of(context)`. Instances of [ScrollbarThemeData] can be customized
/// with [ScrollbarThemeData.copyWith].
///
/// Typically the [ScrollbarThemeData] of a [ScrollbarTheme] is specified as part of the overall
/// [Theme] with [ThemeData.scrollbarTheme].
///
/// All [ScrollbarThemeData] properties are `null` by default. When null, the [Scrollbar]
/// computes its own default values, typically based on the overall theme's
/// [ThemeData.colorScheme].
///
/// See also:
///
/// * [ThemeData], which describes the overall theme information for the
/// application.
@immutable
class ScrollbarThemeData with Diagnosticable {
/// Creates a theme that can be used for [ThemeData.scrollbarTheme].
const ScrollbarThemeData({
this.thickness,
this.showTrackOnHover,
this.isAlwaysShown,
this.radius,
this.thumbColor,
this.trackColor,
this.trackBorderColor,
this.crossAxisMargin,
this.mainAxisMargin,
this.minThumbLength,
});
/// Overrides the default value of [Scrollbar.thickness] in all
/// descendant [Scrollbar] widgets.
///
/// Resolves in the following states:
/// * [MaterialState.hovered] on web and desktop platforms.
final MaterialStateProperty<double?>? thickness;
/// Overrides the default value of [Scrollbar.showTrackOnHover] in all
/// descendant [Scrollbar] widgets.
final bool? showTrackOnHover;
/// Overrides the default value of [Scrollbar.isAlwaysShown] in all
/// descendant [Scrollbar] widgets.
final bool? isAlwaysShown;
/// Overrides the default value of [Scrollbar.radius] in all
/// descendant widgets.
final Radius? radius;
/// Overrides the default [Color] of the [Scrollbar] thumb in all descendant
/// [Scrollbar] widgets.
///
/// Resolves in the following states:
/// * [MaterialState.dragged].
/// * [MaterialState.hovered] on web and desktop platforms.
final MaterialStateProperty<Color?>? thumbColor;
/// Overrides the default [Color] of the [Scrollbar] track when
/// [showTrackOnHover] is true in all descendant [Scrollbar] widgets.
///
/// Resolves in the following states:
/// * [MaterialState.hovered] on web and desktop platforms.
final MaterialStateProperty<Color?>? trackColor;
/// Overrides the default [Color] of the [Scrollbar] track border when
/// [showTrackOnHover] is true in all descendant [Scrollbar] widgets.
///
/// Resolves in the following states:
/// * [MaterialState.hovered] on web and desktop platforms.
final MaterialStateProperty<Color?>? trackBorderColor;
/// Overrides the default value of the [ScrollbarPainter.crossAxisMargin]
/// property in all descendant [Scrollbar] widgets.
///
/// See also:
///
/// * [ScrollbarPainter.crossAxisMargin], which sets the distance from the
/// scrollbar's side to the nearest edge in logical pixels.
final double? crossAxisMargin;
/// Overrides the default value of the [ScrollbarPainter.mainAxisMargin]
/// property in all descendant [Scrollbar] widgets.
///
/// See also:
///
/// * [ScrollbarPainter.mainAxisMargin], which sets the distance from the
/// scrollbar's start and end to the edge of the viewport in logical pixels.
final double? mainAxisMargin;
/// Overrides the default value of the [ScrollbarPainter.minLength]
/// property in all descendant [Scrollbar] widgets.
///
/// See also:
///
/// * [ScrollbarPainter.minLength], which sets the preferred smallest size
/// the scrollbar can shrink to when the total scrollable extent is large,
/// the current visible viewport is small, and the viewport is not
/// overscrolled.
final double? minThumbLength;
/// Creates a copy of this object with the given fields replaced with the
/// new values.
ScrollbarThemeData copyWith({
MaterialStateProperty<double?>? thickness,
bool? showTrackOnHover,
bool? isAlwaysShown,
Radius? radius,
MaterialStateProperty<Color?>? thumbColor,
MaterialStateProperty<Color?>? trackColor,
MaterialStateProperty<Color?>? trackBorderColor,
double? crossAxisMargin,
double? mainAxisMargin,
double? minThumbLength,
}) {
return ScrollbarThemeData(
thickness: thickness ?? this.thickness,
showTrackOnHover: showTrackOnHover ?? this.showTrackOnHover,
isAlwaysShown: isAlwaysShown ?? this.isAlwaysShown,
radius: radius ?? this.radius,
thumbColor: thumbColor ?? this.thumbColor,
trackColor: trackColor ?? this.trackColor,
trackBorderColor: trackBorderColor ?? this.trackBorderColor,
crossAxisMargin: crossAxisMargin ?? this.crossAxisMargin,
mainAxisMargin: mainAxisMargin ?? this.mainAxisMargin,
minThumbLength: minThumbLength ?? this.minThumbLength,
);
}
/// Linearly interpolate between two Scrollbar themes.
///
/// The argument `t` must not be null.
///
/// {@macro dart.ui.shadow.lerp}
static ScrollbarThemeData lerp(ScrollbarThemeData? a, ScrollbarThemeData? b, double t) {
assert(t != null);
return ScrollbarThemeData(
thickness: _lerpProperties<double?>(a?.thickness, b?.thickness, t, lerpDouble),
showTrackOnHover: t < 0.5 ? a?.showTrackOnHover : b?.showTrackOnHover,
isAlwaysShown: t < 0.5 ? a?.isAlwaysShown : b?.isAlwaysShown,
radius: Radius.lerp(a?.radius, b?.radius, t),
thumbColor: _lerpProperties<Color?>(a?.thumbColor, b?.thumbColor, t, Color.lerp),
trackColor: _lerpProperties<Color?>(a?.trackColor, b?.trackColor, t, Color.lerp),
trackBorderColor: _lerpProperties<Color?>(a?.trackBorderColor, b?.trackBorderColor, t, Color.lerp),
crossAxisMargin: lerpDouble(a?.crossAxisMargin, b?.crossAxisMargin, t),
mainAxisMargin: lerpDouble(a?.mainAxisMargin, b?.mainAxisMargin, t),
minThumbLength: lerpDouble(a?.minThumbLength, b?.minThumbLength, t),
);
}
@override
int get hashCode {
return hashValues(
thickness,
showTrackOnHover,
isAlwaysShown,
radius,
thumbColor,
trackColor,
trackBorderColor,
crossAxisMargin,
mainAxisMargin,
minThumbLength,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other))
return true;
if (other.runtimeType != runtimeType)
return false;
return other is ScrollbarThemeData
&& other.thickness == thickness
&& other.showTrackOnHover == showTrackOnHover
&& other.isAlwaysShown == isAlwaysShown
&& other.radius == radius
&& other.thumbColor == thumbColor
&& other.trackColor == trackColor
&& other.trackBorderColor == trackBorderColor
&& other.crossAxisMargin == crossAxisMargin
&& other.mainAxisMargin == mainAxisMargin
&& other.minThumbLength == minThumbLength;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<MaterialStateProperty<double?>>('thickness', thickness, defaultValue: null));
properties.add(DiagnosticsProperty<bool>('showTrackOnHover', showTrackOnHover, defaultValue: null));
properties.add(DiagnosticsProperty<bool>('isAlwaysShown', isAlwaysShown, defaultValue: null));
properties.add(DiagnosticsProperty<Radius>('radius', radius, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('thumbColor', thumbColor, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('trackColor', trackColor, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('trackBorderColor', trackBorderColor, defaultValue: null));
properties.add(DiagnosticsProperty<double>('crossAxisMargin', crossAxisMargin, defaultValue: null));
properties.add(DiagnosticsProperty<double>('mainAxisMargin', mainAxisMargin, defaultValue: null));
properties.add(DiagnosticsProperty<double>('minThumbLength', minThumbLength, defaultValue: null));
}
static MaterialStateProperty<T>? _lerpProperties<T>(
MaterialStateProperty<T>? a,
MaterialStateProperty<T>? b,
double t,
T Function(T?, T?, double) lerpFunction,
) {
// Avoid creating a _LerpProperties object for a common case.
if (a == null && b == null)
return null;
return _LerpProperties<T>(a, b, t, lerpFunction);
}
}
class _LerpProperties<T> implements MaterialStateProperty<T> {
const _LerpProperties(this.a, this.b, this.t, this.lerpFunction);
final MaterialStateProperty<T>? a;
final MaterialStateProperty<T>? b;
final double t;
final T Function(T?, T?, double) lerpFunction;
@override
T resolve(Set<MaterialState> states) {
final T? resolvedA = a?.resolve(states);
final T? resolvedB = b?.resolve(states);
return lerpFunction(resolvedA, resolvedB, t);
}
}
/// Applies a scrollbar theme to descendant [Scrollbar] widgets.
///
/// Descendant widgets obtain the current theme's [ScrollbarThemeData] using
/// [ScrollbarTheme.of]. When a widget uses [ScrollbarTheme.of], it is
/// automatically rebuilt if the theme later changes.
///
/// A scrollbar theme can be specified as part of the overall Material theme
/// using [ThemeData.scrollbarTheme].
///
/// See also:
///
/// * [ScrollbarThemeData], which describes the configuration of a
/// scrollbar theme.
class ScrollbarTheme extends InheritedWidget {
/// Constructs a scrollbar theme that configures all descendant [Scrollbar]
/// widgets.
const ScrollbarTheme({
Key? key,
required this.data,
required Widget child,
}) : super(key: key, child: child);
/// The properties used for all descendant [Scrollbar] widgets.
final ScrollbarThemeData data;
/// Returns the configuration [data] from the closest [ScrollbarTheme]
/// ancestor. If there is no ancestor, it returns [ThemeData.scrollbarTheme].
///
/// Typical usage is as follows:
///
/// ```dart
/// ScrollbarThemeData theme = ScrollbarTheme.of(context);
/// ```
static ScrollbarThemeData of(BuildContext context) {
final ScrollbarTheme? scrollbarTheme = context.dependOnInheritedWidgetOfExactType<ScrollbarTheme>();
return scrollbarTheme?.data ?? Theme.of(context).scrollbarTheme;
}
@override
bool updateShouldNotify(ScrollbarTheme oldWidget) => data != oldWidget.data;
}

View file

@ -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));

View file

@ -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();

View file

@ -0,0 +1,557 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart';
void main() {
test('ScrollbarThemeData copyWith, ==, hashCode basics', () {
expect(const ScrollbarThemeData(), const ScrollbarThemeData().copyWith());
expect(const ScrollbarThemeData().hashCode, const ScrollbarThemeData().copyWith().hashCode);
});
testWidgets('Passing no ScrollbarTheme returns defaults', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(
MaterialApp(
home: Scrollbar(
isAlwaysShown: true,
showTrackOnHover: true,
controller: scrollController,
child: SingleChildScrollView(
controller: scrollController,
child: const SizedBox(width: 4000.0, height: 4000.0)
),
),
),
);
await tester.pumpAndSettle();
// Idle scrollbar behavior
expect(
find.byType(Scrollbar),
paints..rrect(
rrect: RRect.fromRectAndRadius(
const Rect.fromLTRB(790.0, 0.0, 798.0, 90.0),
const Radius.circular(8.0),
),
color: const Color(0x1a000000),
),
);
// Drag scrollbar behavior
const double scrollAmount = 10.0;
final TestGesture dragScrollbarGesture = await tester.startGesture(const Offset(797.0, 45.0));
await tester.pumpAndSettle();
expect(
find.byType(Scrollbar),
paints..rrect(
rrect: RRect.fromRectAndRadius(
const Rect.fromLTRB(790.0, 0.0, 798.0, 90.0),
const Radius.circular(8.0),
),
// Drag color
color: const Color(0x99000000),
),
);
await dragScrollbarGesture.moveBy(const Offset(0.0, scrollAmount));
await tester.pumpAndSettle();
await dragScrollbarGesture.up();
await tester.pumpAndSettle();
// Hover scrollbar behavior
final TestGesture gesture = await tester.createGesture(kind: ui.PointerDeviceKind.mouse);
await gesture.addPointer();
addTearDown(gesture.removePointer);
await gesture.moveTo(const Offset(794.0, 5.0));
await tester.pumpAndSettle();
expect(
find.byType(Scrollbar),
paints
..rect(
rect: const Rect.fromLTRB(784.0, 0.0, 800.0, 600.0),
color: const Color(0x08000000),
)
..line(
p1: const Offset(784.0, 0.0),
p2: const Offset(784.0, 600.0),
strokeWidth: 1.0,
color: const Color(0x1a000000),
)
..rrect(
rrect: RRect.fromRectAndRadius(
// Scrollbar thumb is larger
const Rect.fromLTRB(786.0, 10.0, 798.0, 100.0),
const Radius.circular(8.0),
),
// Hover color
color: const Color(0x80000000),
),
);
}, variant: const TargetPlatformVariant(<TargetPlatform>{
TargetPlatform.linux,
TargetPlatform.macOS,
TargetPlatform.windows,
TargetPlatform.fuchsia,
}),
);
testWidgets('Scrollbar uses values from ScrollbarTheme', (WidgetTester tester) async {
final ScrollbarThemeData scrollbarTheme = _scrollbarTheme();
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(MaterialApp(
theme: ThemeData(scrollbarTheme: scrollbarTheme),
home: Scrollbar(
isAlwaysShown: true,
controller: scrollController,
child: SingleChildScrollView(
controller: scrollController,
child: const SizedBox(width: 4000.0, height: 4000.0)
),
),
));
await tester.pumpAndSettle();
// Idle scrollbar behavior
expect(
find.byType(Scrollbar),
paints..rrect(
rrect: RRect.fromRectAndRadius(
const Rect.fromLTRB(785.0, 10.0, 795.0, 97.0),
const Radius.circular(6.0),
),
color: const Color(0xff4caf50),
),
);
// Drag scrollbar behavior
const double scrollAmount = 10.0;
final TestGesture dragScrollbarGesture = await tester.startGesture(const Offset(797.0, 45.0));
await tester.pumpAndSettle();
expect(
find.byType(Scrollbar),
paints..rrect(
rrect: RRect.fromRectAndRadius(
const Rect.fromLTRB(785.0, 10.0, 795.0, 97.0),
const Radius.circular(6.0),
),
// Drag color
color: const Color(0xfff44336),
),
);
await dragScrollbarGesture.moveBy(const Offset(0.0, scrollAmount));
await tester.pumpAndSettle();
await dragScrollbarGesture.up();
await tester.pumpAndSettle();
// Hover scrollbar behavior
final TestGesture gesture = await tester.createGesture(kind: ui.PointerDeviceKind.mouse);
await gesture.addPointer();
addTearDown(gesture.removePointer);
await gesture.moveTo(const Offset(794.0, 5.0));
await tester.pumpAndSettle();
expect(
find.byType(Scrollbar),
paints
..rect(
rect: const Rect.fromLTRB(770.0, 0.0, 800.0, 580.0),
color: const Color(0xff000000),
)
..line(
p1: const Offset(770.0, 0.0),
p2: const Offset(770.0, 580.0),
strokeWidth: 1.0,
color: const Color(0xffffeb3b),
)
..rrect(
rrect: RRect.fromRectAndRadius(
// Scrollbar thumb is larger
const Rect.fromLTRB(775.0, 20.0, 795.0, 107.0),
const Radius.circular(6.0),
),
// Hover color
color: const Color(0xff2196f3),
),
);
}, variant: const TargetPlatformVariant(<TargetPlatform>{
TargetPlatform.linux,
TargetPlatform.macOS,
TargetPlatform.windows,
TargetPlatform.fuchsia,
}),
);
testWidgets('Scrollbar widget properties take priority over theme', (WidgetTester tester) async {
const double thickness = 4.0;
const double hoverThickness = 4.0;
const bool showTrackOnHover = true;
const Radius radius = Radius.circular(3.0);
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(colorScheme: const ColorScheme.light()),
home: Scrollbar(
thickness: thickness,
hoverThickness: hoverThickness,
isAlwaysShown: true,
showTrackOnHover: showTrackOnHover,
radius: radius,
controller: scrollController,
child: SingleChildScrollView(
controller: scrollController,
child: const SizedBox(width: 4000.0, height: 4000.0)
),
),
),
);
await tester.pumpAndSettle();
// Idle scrollbar behavior
expect(
find.byType(Scrollbar),
paints..rrect(
rrect: RRect.fromRectAndRadius(
const Rect.fromLTRB(794.0, 0.0, 798.0, 90.0),
const Radius.circular(3.0),
),
color: const Color(0x1a000000),
),
);
// Drag scrollbar behavior
const double scrollAmount = 10.0;
final TestGesture dragScrollbarGesture = await tester.startGesture(const Offset(797.0, 45.0));
await tester.pumpAndSettle();
expect(
find.byType(Scrollbar),
paints..rrect(
rrect: RRect.fromRectAndRadius(
const Rect.fromLTRB(794.0, 0.0, 798.0, 90.0),
const Radius.circular(3.0),
),
// Drag color
color: const Color(0x99000000),
),
);
await dragScrollbarGesture.moveBy(const Offset(0.0, scrollAmount));
await tester.pumpAndSettle();
await dragScrollbarGesture.up();
await tester.pumpAndSettle();
// Hover scrollbar behavior
final TestGesture gesture = await tester.createGesture(kind: ui.PointerDeviceKind.mouse);
await gesture.addPointer();
addTearDown(gesture.removePointer);
await gesture.moveTo(const Offset(794.0, 5.0));
await tester.pumpAndSettle();
expect(
find.byType(Scrollbar),
paints
..rect(
rect: const Rect.fromLTRB(792.0, 0.0, 800.0, 600.0),
color: const Color(0x08000000),
)
..line(
p1: const Offset(792.0, 0.0),
p2: const Offset(792.0, 600.0),
strokeWidth: 1.0,
color: const Color(0x1a000000),
)
..rrect(
rrect: RRect.fromRectAndRadius(
// Scrollbar thumb is larger
const Rect.fromLTRB(794.0, 10.0, 798.0, 100.0),
const Radius.circular(3.0),
),
// Hover color
color: const Color(0x80000000),
),
);
}, variant: const TargetPlatformVariant(<TargetPlatform>{
TargetPlatform.linux,
TargetPlatform.macOS,
TargetPlatform.windows,
TargetPlatform.fuchsia,
}),
);
testWidgets('ThemeData colorScheme is used when no ScrollbarTheme is set', (WidgetTester tester) async {
Widget buildFrame(ThemeData appTheme) {
final ScrollController scrollController = ScrollController();
return MaterialApp(
theme: appTheme,
home: Scrollbar(
isAlwaysShown: true,
showTrackOnHover: true,
controller: scrollController,
child: SingleChildScrollView(
controller: scrollController,
child: const SizedBox(width: 4000.0, height: 4000.0)
),
)
);
}
// Scrollbar defaults for light themes:
// - coloring based on ColorScheme.onSurface
await tester.pumpWidget(buildFrame(ThemeData.from(colorScheme: const ColorScheme.light())));
await tester.pumpAndSettle();
// Idle scrollbar behavior
expect(
find.byType(Scrollbar),
paints..rrect(
rrect: RRect.fromRectAndRadius(
const Rect.fromLTRB(790.0, 0.0, 798.0, 90.0),
const Radius.circular(8.0),
),
color: const Color(0x1a000000),
),
);
// Drag scrollbar behavior
const double scrollAmount = 10.0;
TestGesture dragScrollbarGesture = await tester.startGesture(const Offset(797.0, 45.0));
await tester.pumpAndSettle();
expect(
find.byType(Scrollbar),
paints..rrect(
rrect: RRect.fromRectAndRadius(
const Rect.fromLTRB(790.0, 0.0, 798.0, 90.0),
const Radius.circular(8.0),
),
// Drag color
color: const Color(0x99000000),
),
);
await dragScrollbarGesture.moveBy(const Offset(0.0, scrollAmount));
await tester.pumpAndSettle();
await dragScrollbarGesture.up();
await tester.pumpAndSettle();
// Hover scrollbar behavior
final TestGesture hoverGesture = await tester.createGesture(kind: ui.PointerDeviceKind.mouse);
await hoverGesture.addPointer();
addTearDown(hoverGesture.removePointer);
await hoverGesture.moveTo(const Offset(794.0, 5.0));
await tester.pumpAndSettle();
expect(
find.byType(Scrollbar),
paints
..rect(
rect: const Rect.fromLTRB(784.0, 0.0, 800.0, 600.0),
color: const Color(0x08000000),
)
..line(
p1: const Offset(784.0, 0.0),
p2: const Offset(784.0, 600.0),
strokeWidth: 1.0,
color: const Color(0x1a000000),
)
..rrect(
rrect: RRect.fromRectAndRadius(
// Scrollbar thumb is larger
const Rect.fromLTRB(786.0, 10.0, 798.0, 100.0),
const Radius.circular(8.0),
),
// Hover color
color: const Color(0x80000000),
),
);
await hoverGesture.moveTo(const Offset(0.0, 0.0));
// Scrollbar defaults for dark themes:
// - coloring slightly different based on ColorScheme.onSurface
await tester.pumpWidget(buildFrame(ThemeData.from(colorScheme: const ColorScheme.dark())));
await tester.pumpAndSettle(); // Theme change animation
// Idle scrollbar behavior
expect(
find.byType(Scrollbar),
paints..rrect(
rrect: RRect.fromRectAndRadius(
const Rect.fromLTRB(790.0, 10.0, 798.0, 100.0),
const Radius.circular(8.0),
),
color: const Color(0x4dffffff),
),
);
// Drag scrollbar behavior
dragScrollbarGesture = await tester.startGesture(const Offset(797.0, 45.0));
await tester.pumpAndSettle();
expect(
find.byType(Scrollbar),
paints..rrect(
rrect: RRect.fromRectAndRadius(
const Rect.fromLTRB(790.0, 10.0, 798.0, 100.0),
const Radius.circular(8.0),
),
// Drag color
color: const Color(0xbfffffff),
),
);
await dragScrollbarGesture.moveBy(const Offset(0.0, scrollAmount));
await tester.pumpAndSettle();
await dragScrollbarGesture.up();
await tester.pumpAndSettle();
// Hover scrollbar behavior
await hoverGesture.moveTo(const Offset(794.0, 5.0));
await tester.pumpAndSettle();
expect(
find.byType(Scrollbar),
paints
..rect(
rect: const Rect.fromLTRB(784.0, 0.0, 800.0, 600.0),
color: const Color(0x0dffffff),
)
..line(
p1: const Offset(784.0, 0.0),
p2: const Offset(784.0, 600.0),
strokeWidth: 1.0,
color: const Color(0x40ffffff),
)
..rrect(
rrect: RRect.fromRectAndRadius(
// Scrollbar thumb is larger
const Rect.fromLTRB(786.0, 20.0, 798.0, 110.0),
const Radius.circular(8.0),
),
// Hover color
color: const Color(0xa6ffffff),
),
);
}, variant: const TargetPlatformVariant(<TargetPlatform>{
TargetPlatform.linux,
TargetPlatform.macOS,
TargetPlatform.windows,
TargetPlatform.fuchsia,
}),
);
testWidgets('Default ScrollbarTheme debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
const ScrollbarThemeData().debugFillProperties(builder);
final List<String> description = builder.properties
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
.map((DiagnosticsNode node) => node.toString())
.toList();
expect(description, <String>[]);
});
testWidgets('ScrollbarTheme implements debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
ScrollbarThemeData(
thickness: MaterialStateProperty.resolveWith(_getThickness),
showTrackOnHover: true,
isAlwaysShown: true,
radius: const Radius.circular(3.0),
thumbColor: MaterialStateProperty.resolveWith(_getThumbColor),
trackColor: MaterialStateProperty.resolveWith(_getTrackColor),
trackBorderColor: MaterialStateProperty.resolveWith(_getTrackBorderColor),
crossAxisMargin: 3.0,
mainAxisMargin: 6.0,
minThumbLength: 120.0,
).debugFillProperties(builder);
final List<String> description = builder.properties
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
.map((DiagnosticsNode node) => node.toString())
.toList();
expect(description, <String>[
'thickness: Instance of \'_MaterialStatePropertyWith<double?>\'',
'showTrackOnHover: true',
'isAlwaysShown: true',
'radius: Radius.circular(3.0)',
'thumbColor: Instance of \'_MaterialStatePropertyWith<Color?>\'',
'trackColor: Instance of \'_MaterialStatePropertyWith<Color?>\'',
'trackBorderColor: Instance of \'_MaterialStatePropertyWith<Color?>\'',
'crossAxisMargin: 3.0',
'mainAxisMargin: 6.0',
'minThumbLength: 120.0'
]);
// On the web, Dart doubles and ints are backed by the same kind of object because
// JavaScript does not support integers. So, the Dart double "4.0" is identical
// to "4", which results in the web evaluating to the value "4" regardless of which
// one is used. This results in a difference for doubles in debugFillProperties between
// the web and the rest of Flutter's target platforms.
}, skip: kIsWeb);
}
ScrollbarThemeData _scrollbarTheme({
MaterialStateProperty<double?>? thickness,
bool showTrackOnHover = true,
bool isAlwaysShown = true,
Radius radius = const Radius.circular(6.0),
MaterialStateProperty<Color?>? thumbColor,
MaterialStateProperty<Color?>? trackColor,
MaterialStateProperty<Color?>? trackBorderColor,
double crossAxisMargin = 5.0,
double mainAxisMargin = 10.0,
double minThumbLength = 50.0,
}) {
return ScrollbarThemeData(
thickness: thickness ?? MaterialStateProperty.resolveWith(_getThickness),
showTrackOnHover: showTrackOnHover,
isAlwaysShown: isAlwaysShown,
radius: radius,
thumbColor: thumbColor ?? MaterialStateProperty.resolveWith(_getThumbColor),
trackColor: trackColor ?? MaterialStateProperty.resolveWith(_getTrackColor),
trackBorderColor: trackBorderColor ?? MaterialStateProperty.resolveWith(_getTrackBorderColor),
crossAxisMargin: crossAxisMargin,
mainAxisMargin: mainAxisMargin,
minThumbLength: minThumbLength,
);
}
double? _getThickness(Set<MaterialState> states) {
if (states.contains(MaterialState.hovered))
return 20.0;
return 10.0;
}
Color? _getThumbColor(Set<MaterialState> states) {
if (states.contains(MaterialState.dragged))
return Colors.red;
if (states.contains(MaterialState.hovered))
return Colors.blue;
return Colors.green;
}
Color? _getTrackColor(Set<MaterialState> states) {
if (states.contains(MaterialState.hovered))
return Colors.black;
return null;
}
Color? _getTrackBorderColor(Set<MaterialState> states) {
if (states.contains(MaterialState.hovered))
return Colors.yellow;
return null;
}

View file

@ -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),

View file

@ -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),