| // 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 'package:flutter/cupertino.dart'; |
| import 'package:flutter/gestures.dart'; |
| |
| import 'color_scheme.dart'; |
| import 'material_state.dart'; |
| import 'scrollbar_theme.dart'; |
| import 'theme.dart'; |
| |
| const double _kScrollbarThickness = 8.0; |
| const double _kScrollbarThicknessWithTrack = 12.0; |
| const double _kScrollbarMargin = 2.0; |
| const double _kScrollbarMinLength = 48.0; |
| const Radius _kScrollbarRadius = Radius.circular(8.0); |
| const Duration _kScrollbarFadeDuration = Duration(milliseconds: 300); |
| const Duration _kScrollbarTimeToFade = Duration(milliseconds: 600); |
| |
| /// A Material Design scrollbar. |
| /// |
| /// To add a scrollbar to a [ScrollView], wrap the scroll view |
| /// widget in a [Scrollbar] widget. |
| /// |
| /// {@youtube 560 315 https://www.youtube.com/watch?v=DbkIQSvwnZc} |
| /// |
| /// {@macro flutter.widgets.Scrollbar} |
| /// |
| /// Dynamically changes to a [CupertinoScrollbar], an iOS style scrollbar, by |
| /// default on the iOS platform. |
| /// |
| /// The color of the Scrollbar thumb will change when [MaterialState.dragged], |
| /// or [MaterialState.hovered] on desktop and web platforms. These stateful |
| /// color choices can be changed using [ScrollbarThemeData.thumbColor]. |
| /// |
| /// {@tool dartpad} |
| /// This sample shows a [Scrollbar] that executes a fade animation as scrolling |
| /// occurs. The Scrollbar will fade into view as the user scrolls, and fade out |
| /// when scrolling stops. |
| /// |
| /// ** See code in examples/api/lib/material/scrollbar/scrollbar.0.dart ** |
| /// {@end-tool} |
| /// |
| /// {@tool dartpad} |
| /// When [thumbVisibility] is true, the scrollbar thumb will remain visible |
| /// without the fade animation. This requires that a [ScrollController] is |
| /// provided to controller, or that the [PrimaryScrollController] is available. |
| /// |
| /// When a [ScrollView.scrollDirection] is [Axis.horizontal], it is recommended |
| /// that the [Scrollbar] is always visible, since scrolling in the horizontal |
| /// axis is less discoverable. |
| /// |
| /// ** See code in examples/api/lib/material/scrollbar/scrollbar.1.dart ** |
| /// {@end-tool} |
| /// |
| /// A scrollbar track can be added using [trackVisibility]. This can also be |
| /// drawn when triggered by a hover event, or based on any [MaterialState] by |
| /// using [ScrollbarThemeData.trackVisibility]. |
| /// |
| /// The [thickness] of the track and scrollbar thumb can be changed dynamically |
| /// in response to [MaterialState]s using [ScrollbarThemeData.thickness]. |
| /// |
| /// See also: |
| /// |
| /// * [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. |
| class Scrollbar extends StatelessWidget { |
| /// Creates a Material Design scrollbar that by default will connect to the |
| /// closest Scrollable descendant of [child]. |
| /// |
| /// The [child] should be a source of [ScrollNotification] notifications, |
| /// typically a [Scrollable] widget. |
| /// |
| /// If the [controller] is null, the default behavior is to |
| /// enable scrollbar dragging using the [PrimaryScrollController]. |
| /// |
| /// When null, [thickness] defaults to 8.0 pixels on desktop and web, and 4.0 |
| /// pixels when on mobile platforms. A null [radius] will result in a default |
| /// of an 8.0 pixel circular radius about the corners of the scrollbar thumb, |
| /// except for when executing on [TargetPlatform.android], which will render the |
| /// thumb without a radius. |
| const Scrollbar({ |
| super.key, |
| required this.child, |
| this.controller, |
| this.thumbVisibility, |
| this.trackVisibility, |
| this.thickness, |
| this.radius, |
| this.notificationPredicate, |
| this.interactive, |
| this.scrollbarOrientation, |
| @Deprecated( |
| 'Use ScrollbarThemeData.trackVisibility to resolve based on the current state instead. ' |
| 'This feature was deprecated after v3.4.0-19.0.pre.', |
| ) |
| this.showTrackOnHover, |
| }); |
| |
| /// {@macro flutter.widgets.Scrollbar.child} |
| final Widget child; |
| |
| /// {@macro flutter.widgets.Scrollbar.controller} |
| final ScrollController? controller; |
| |
| /// {@macro flutter.widgets.Scrollbar.thumbVisibility} |
| /// |
| /// If this property is null, then [ScrollbarThemeData.thumbVisibility] of |
| /// [ThemeData.scrollbarTheme] is used. If that is also null, the default value |
| /// is false. |
| /// |
| /// If the thumb visibility is related to the scrollbar's material state, |
| /// use the global [ScrollbarThemeData.thumbVisibility] or override the |
| /// sub-tree's theme data. |
| final bool? thumbVisibility; |
| |
| /// {@macro flutter.widgets.Scrollbar.trackVisibility} |
| /// |
| /// 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. |
| /// |
| /// Replaces deprecated [showTrackOnHover]. |
| 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 is deprecated, [trackVisibility] or [ScrollbarThemeData.trackVisibility] |
| /// should be used instead. |
| @Deprecated( |
| 'Use ScrollbarThemeData.trackVisibility to resolve based on the current state instead. ' |
| 'This feature was deprecated after v3.4.0-19.0.pre.', |
| ) |
| final bool? showTrackOnHover; |
| |
| /// The thickness of the scrollbar in the cross axis of the scrollable. |
| /// |
| /// If null, the default value is platform dependent. On [TargetPlatform.android], |
| /// the default thickness is 4.0 pixels. On [TargetPlatform.iOS], |
| /// [CupertinoScrollbar.defaultThickness] is used. The remaining platforms have a |
| /// default thickness of 8.0 pixels. |
| final double? thickness; |
| |
| /// The [Radius] of the scrollbar thumb's rounded rectangle corners. |
| /// |
| /// If null, the default value is platform dependent. On [TargetPlatform.android], |
| /// no radius is applied to the scrollbar thumb. On [TargetPlatform.iOS], |
| /// [CupertinoScrollbar.defaultRadius] is used. The remaining platforms have a |
| /// default [Radius.circular] of 8.0 pixels. |
| final Radius? radius; |
| |
| /// {@macro flutter.widgets.Scrollbar.interactive} |
| final bool? interactive; |
| |
| /// {@macro flutter.widgets.Scrollbar.notificationPredicate} |
| final ScrollNotificationPredicate? notificationPredicate; |
| |
| /// {@macro flutter.widgets.Scrollbar.scrollbarOrientation} |
| final ScrollbarOrientation? scrollbarOrientation; |
| |
| @override |
| Widget build(BuildContext context) { |
| if (Theme.of(context).platform == TargetPlatform.iOS) { |
| return CupertinoScrollbar( |
| thumbVisibility: thumbVisibility ?? false, |
| thickness: thickness ?? CupertinoScrollbar.defaultThickness, |
| thicknessWhileDragging: thickness ?? CupertinoScrollbar.defaultThicknessWhileDragging, |
| radius: radius ?? CupertinoScrollbar.defaultRadius, |
| radiusWhileDragging: radius ?? CupertinoScrollbar.defaultRadiusWhileDragging, |
| controller: controller, |
| notificationPredicate: notificationPredicate, |
| scrollbarOrientation: scrollbarOrientation, |
| child: child, |
| ); |
| } |
| return _MaterialScrollbar( |
| controller: controller, |
| thumbVisibility: thumbVisibility, |
| trackVisibility: trackVisibility, |
| showTrackOnHover: showTrackOnHover, |
| thickness: thickness, |
| radius: radius, |
| notificationPredicate: notificationPredicate, |
| interactive: interactive, |
| scrollbarOrientation: scrollbarOrientation, |
| child: child, |
| ); |
| } |
| } |
| |
| class _MaterialScrollbar extends RawScrollbar { |
| const _MaterialScrollbar({ |
| required super.child, |
| super.controller, |
| super.thumbVisibility, |
| super.trackVisibility, |
| this.showTrackOnHover, |
| super.thickness, |
| super.radius, |
| ScrollNotificationPredicate? notificationPredicate, |
| super.interactive, |
| super.scrollbarOrientation, |
| }) : super( |
| fadeDuration: _kScrollbarFadeDuration, |
| timeToFade: _kScrollbarTimeToFade, |
| pressDuration: Duration.zero, |
| notificationPredicate: notificationPredicate ?? defaultScrollNotificationPredicate, |
| ); |
| |
| final bool? showTrackOnHover; |
| |
| @override |
| _MaterialScrollbarState createState() => _MaterialScrollbarState(); |
| } |
| |
| class _MaterialScrollbarState extends RawScrollbarState<_MaterialScrollbar> { |
| late AnimationController _hoverAnimationController; |
| 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.thumbVisibility ?? _scrollbarTheme.thumbVisibility?.resolve(_states) ?? false; |
| |
| @override |
| bool get enableGestures => widget.interactive ?? _scrollbarTheme.interactive ?? !_useAndroidScrollbar; |
| |
| bool get _showTrackOnHover => widget.showTrackOnHover ?? _scrollbarTheme.showTrackOnHover ?? false; |
| |
| MaterialStateProperty<bool> get _trackVisibility => MaterialStateProperty.resolveWith((Set<MaterialState> states) { |
| if (states.contains(MaterialState.hovered) && _showTrackOnHover) { |
| return true; |
| } |
| return widget.trackVisibility ?? _scrollbarTheme.trackVisibility?.resolve(states) ?? false; |
| }); |
| |
| Set<MaterialState> get _states => <MaterialState>{ |
| if (_dragIsActive) MaterialState.dragged, |
| if (_hoverIsActive) MaterialState.hovered, |
| }; |
| |
| MaterialStateProperty<Color> get _thumbColor { |
| final Color onSurface = _colorScheme.onSurface; |
| final Brightness brightness = _colorScheme.brightness; |
| late Color dragColor; |
| late Color hoverColor; |
| late Color idleColor; |
| switch (brightness) { |
| case Brightness.light: |
| dragColor = onSurface.withOpacity(0.6); |
| hoverColor = onSurface.withOpacity(0.5); |
| idleColor = _useAndroidScrollbar |
| ? Theme.of(context).highlightColor.withOpacity(1.0) |
| : onSurface.withOpacity(0.1); |
| case Brightness.dark: |
| dragColor = onSurface.withOpacity(0.75); |
| hoverColor = onSurface.withOpacity(0.65); |
| idleColor = _useAndroidScrollbar |
| ? Theme.of(context).highlightColor.withOpacity(1.0) |
| : onSurface.withOpacity(0.3); |
| } |
| |
| return MaterialStateProperty.resolveWith((Set<MaterialState> states) { |
| if (states.contains(MaterialState.dragged)) { |
| return _scrollbarTheme.thumbColor?.resolve(states) ?? dragColor; |
| } |
| |
| // If the track is visible, the thumb color hover animation is ignored and |
| // changes immediately. |
| if (_trackVisibility.resolve(states)) { |
| return _scrollbarTheme.thumbColor?.resolve(states) ?? hoverColor; |
| } |
| |
| return Color.lerp( |
| _scrollbarTheme.thumbColor?.resolve(states) ?? idleColor, |
| _scrollbarTheme.thumbColor?.resolve(states) ?? hoverColor, |
| _hoverAnimationController.value, |
| )!; |
| }); |
| } |
| |
| MaterialStateProperty<Color> get _trackColor { |
| final Color onSurface = _colorScheme.onSurface; |
| final Brightness brightness = _colorScheme.brightness; |
| return MaterialStateProperty.resolveWith((Set<MaterialState> states) { |
| if (showScrollbar && _trackVisibility.resolve(states)) { |
| return _scrollbarTheme.trackColor?.resolve(states) |
| ?? (brightness == Brightness.light |
| ? onSurface.withOpacity(0.03) |
| : onSurface.withOpacity(0.05)); |
| } |
| return const Color(0x00000000); |
| }); |
| } |
| |
| MaterialStateProperty<Color> get _trackBorderColor { |
| final Color onSurface = _colorScheme.onSurface; |
| final Brightness brightness = _colorScheme.brightness; |
| return MaterialStateProperty.resolveWith((Set<MaterialState> states) { |
| if (showScrollbar && _trackVisibility.resolve(states)) { |
| return _scrollbarTheme.trackBorderColor?.resolve(states) |
| ?? (brightness == Brightness.light |
| ? onSurface.withOpacity(0.1) |
| : onSurface.withOpacity(0.25)); |
| } |
| return const Color(0x00000000); |
| }); |
| } |
| |
| MaterialStateProperty<double> get _thickness { |
| return MaterialStateProperty.resolveWith((Set<MaterialState> states) { |
| if (states.contains(MaterialState.hovered) && _trackVisibility.resolve(states)) { |
| return _scrollbarTheme.thickness?.resolve(states) |
| ?? _kScrollbarThicknessWithTrack; |
| } |
| // The default scrollbar thickness is smaller on mobile. |
| return widget.thickness |
| ?? _scrollbarTheme.thickness?.resolve(states) |
| ?? (_kScrollbarThickness / (_useAndroidScrollbar ? 2 : 1)); |
| }); |
| } |
| |
| @override |
| void initState() { |
| super.initState(); |
| _hoverAnimationController = AnimationController( |
| vsync: this, |
| duration: const Duration(milliseconds: 200), |
| ); |
| _hoverAnimationController.addListener(() { |
| updateScrollbarPainter(); |
| }); |
| } |
| |
| @override |
| void didChangeDependencies() { |
| final ThemeData theme = Theme.of(context); |
| _colorScheme = theme.colorScheme; |
| _scrollbarTheme = ScrollbarTheme.of(context); |
| switch (theme.platform) { |
| case TargetPlatform.android: |
| _useAndroidScrollbar = true; |
| case TargetPlatform.iOS: |
| case TargetPlatform.linux: |
| case TargetPlatform.fuchsia: |
| case TargetPlatform.macOS: |
| case TargetPlatform.windows: |
| _useAndroidScrollbar = false; |
| } |
| super.didChangeDependencies(); |
| } |
| |
| @override |
| void updateScrollbarPainter() { |
| scrollbarPainter |
| ..color = _thumbColor.resolve(_states) |
| ..trackColor = _trackColor.resolve(_states) |
| ..trackBorderColor = _trackBorderColor.resolve(_states) |
| ..textDirection = Directionality.of(context) |
| ..thickness = _thickness.resolve(_states) |
| ..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.paddingOf(context) |
| ..scrollbarOrientation = widget.scrollbarOrientation |
| ..ignorePointer = !enableGestures; |
| } |
| |
| @override |
| void handleThumbPressStart(Offset localPosition) { |
| super.handleThumbPressStart(localPosition); |
| setState(() { _dragIsActive = true; }); |
| } |
| |
| @override |
| void handleThumbPressEnd(Offset localPosition, Velocity velocity) { |
| super.handleThumbPressEnd(localPosition, velocity); |
| setState(() { _dragIsActive = false; }); |
| } |
| |
| @override |
| void handleHover(PointerHoverEvent event) { |
| super.handleHover(event); |
| // Check if the position of the pointer falls over the painted scrollbar |
| if (isPointerOverScrollbar(event.position, event.kind, forHover: true)) { |
| // Pointer is hovering over the scrollbar |
| setState(() { _hoverIsActive = true; }); |
| _hoverAnimationController.forward(); |
| } else if (_hoverIsActive) { |
| // Pointer was, but is no longer over painted scrollbar. |
| setState(() { _hoverIsActive = false; }); |
| _hoverAnimationController.reverse(); |
| } |
| } |
| |
| @override |
| void handleHoverExit(PointerExitEvent event) { |
| super.handleHoverExit(event); |
| setState(() { _hoverIsActive = false; }); |
| _hoverAnimationController.reverse(); |
| } |
| |
| @override |
| void dispose() { |
| _hoverAnimationController.dispose(); |
| super.dispose(); |
| } |
| } |