Unverified Commit 26ccbd9f authored by Kate Lovett's avatar Kate Lovett Committed by GitHub

Update Scrollbar behavior for mobile devices (#72531)

parent ff8203dc
......@@ -18,18 +18,18 @@ const Radius _kScrollbarRadius = Radius.circular(8.0);
const Duration _kScrollbarFadeDuration = Duration(milliseconds: 300);
const Duration _kScrollbarTimeToFade = Duration(milliseconds: 600);
/// A material design scrollbar.
/// A Material Design scrollbar.
///
/// To add a scrollbar thumb to a [ScrollView], simply wrap the scroll view
/// To add a scrollbar to a [ScrollView], wrap the scroll view
/// widget in a [Scrollbar] widget.
///
/// {@macro flutter.widgets.Scrollbar}
///
/// The color of the Scrollbar will change when dragged, as well as when
/// hovered over. A scrollbar track can also been drawn when triggered by a
/// hover event, which is controlled by [showTrackOnHover]. The thickness of the
/// track and scrollbar thumb will become larger when hovering, unless
/// overridden by [hoverThickness].
/// The color of the Scrollbar will change when dragged. A hover animation is
/// also triggered when used on web and desktop platforms. A scrollbar track
/// can also been drawn when triggered by a hover event, which is controlled by
/// [showTrackOnHover]. The thickness of the track and scrollbar thumb will
/// become larger when hovering, unless overridden by [hoverThickness].
///
// TODO(Piinks): Add code sample
///
......@@ -50,8 +50,11 @@ class Scrollbar extends RawScrollbar {
/// If the [controller] is null, the default behavior is to
/// enable scrollbar dragging using the [PrimaryScrollController].
///
/// When null, [thickness] and [radius] defaults will result in a rounded
/// rectangular thumb that is 8.0 dp wide with a radius of 8.0 pixels.
/// 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({
Key? key,
required Widget child,
......@@ -66,7 +69,7 @@ class Scrollbar extends RawScrollbar {
child: child,
controller: controller,
isAlwaysShown: isAlwaysShown,
thickness: thickness ?? _kScrollbarThickness,
thickness: thickness,
radius: radius,
fadeDuration: _kScrollbarFadeDuration,
timeToFade: _kScrollbarTimeToFade,
......@@ -93,6 +96,11 @@ class _ScrollbarState extends RawScrollbarState<Scrollbar> {
bool _dragIsActive = false;
bool _hoverIsActive = false;
late ColorScheme _colorScheme;
// On Android, scrollbars should match native appearance.
late bool _useAndroidScrollbar;
// Hover events should be ignored on mobile, the exit event cannot be
// triggered, but the enter event can on tap.
late bool _isMobile;
Set<MaterialState> get _states => <MaterialState>{
if (_dragIsActive) MaterialState.dragged,
......@@ -165,7 +173,8 @@ class _ScrollbarState extends RawScrollbarState<Scrollbar> {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.hovered) && widget.showTrackOnHover)
return widget.hoverThickness ?? _kScrollbarThicknessWithTrack;
return widget.thickness ?? _kScrollbarThickness;
// The default scrollbar thickness is smaller on mobile.
return widget.thickness ?? (_kScrollbarThickness / (_isMobile ? 2 : 1));
});
}
......@@ -181,6 +190,29 @@ class _ScrollbarState extends RawScrollbarState<Scrollbar> {
});
}
@override
void didChangeDependencies() {
final ThemeData theme = Theme.of(context);
switch (theme.platform) {
case TargetPlatform.android:
_useAndroidScrollbar = true;
_isMobile = true;
break;
case TargetPlatform.iOS:
_useAndroidScrollbar = false;
_isMobile = true;
break;
case TargetPlatform.linux:
case TargetPlatform.fuchsia:
case TargetPlatform.macOS:
case TargetPlatform.windows:
_useAndroidScrollbar = false;
_isMobile = false;
break;
}
super.didChangeDependencies();
}
@override
void updateScrollbarPainter() {
_colorScheme = Theme.of(context).colorScheme;
......@@ -190,8 +222,8 @@ class _ScrollbarState extends RawScrollbarState<Scrollbar> {
..trackBorderColor = _trackBorderColor.resolve(_states)
..textDirection = Directionality.of(context)
..thickness = _thickness.resolve(_states)
..radius = widget.radius ?? _kScrollbarRadius
..crossAxisMargin = _kScrollbarMargin
..radius = widget.radius ?? (_useAndroidScrollbar ? null : _kScrollbarRadius)
..crossAxisMargin = (_useAndroidScrollbar ? 0.0 : _kScrollbarMargin)
..minLength = _kScrollbarMinLength
..padding = MediaQuery.of(context).padding;
}
......@@ -210,6 +242,8 @@ class _ScrollbarState extends RawScrollbarState<Scrollbar> {
@override
void handleHover(PointerHoverEvent event) {
// Hover events should not be triggered on mobile.
assert(!_isMobile);
super.handleHover(event);
// Check if the position of the pointer falls over the painted scrollbar
if (isPointerOverScrollbar(event.position)) {
......@@ -225,6 +259,8 @@ class _ScrollbarState extends RawScrollbarState<Scrollbar> {
@override
void handleHoverExit(PointerExitEvent event) {
// Hover events should not be triggered on mobile.
assert(!_isMobile);
super.handleHoverExit(event);
setState(() { _hoverIsActive = false; });
_hoverAnimationController.reverse();
......
......@@ -781,6 +781,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
late Animation<double> _fadeoutOpacityAnimation;
final GlobalKey _scrollbarPainterKey = GlobalKey();
bool _hoverIsActive = false;
late bool _isMobile;
/// Used to paint the scrollbar.
......@@ -811,6 +812,18 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
@override
void didChangeDependencies() {
super.didChangeDependencies();
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.iOS:
_isMobile = true;
break;
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
_isMobile = false;
break;
}
_maybeTriggerScrollbar();
}
......@@ -1145,20 +1158,28 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
@override
Widget build(BuildContext context) {
updateScrollbarPainter();
Widget child = CustomPaint(
key: _scrollbarPainterKey,
foregroundPainter: scrollbarPainter,
child: RepaintBoundary(child: widget.child),
);
if (!_isMobile) {
// Hover events not supported on mobile.
child = MouseRegion(
onExit: handleHoverExit,
onHover: handleHover,
child: child
);
}
return NotificationListener<ScrollNotification>(
onNotification: _handleScrollNotification,
child: RepaintBoundary(
child: RawGestureDetector(
gestures: _gestures,
child: MouseRegion(
onExit: handleHoverExit,
onHover: handleHover,
child: CustomPaint(
key: _scrollbarPainterKey,
foregroundPainter: scrollbarPainter,
child: RepaintBoundary(child: widget.child),
),
),
child: child,
),
),
);
......
......@@ -30,7 +30,24 @@ void main() {
));
expect(find.byType(Scrollbar), isNot(paints..rect()));
await tester.fling(find.byType(SingleChildScrollView), const Offset(0.0, -10.0), 10.0);
expect(find.byType(Scrollbar), paints..rect(rect: const Rect.fromLTRB(800.0 - 12.0, 0.0, 800.0, 600.0)));
expect(
find.byType(Scrollbar),
paints
..rect(
rect: const Rect.fromLTRB(796.0, 0.0, 800.0, 600.0),
color: const Color(0x00000000),
)
..line(
p1: const Offset(796.0, 0.0),
p2: const Offset(796.0, 600.0),
strokeWidth: 1.0,
color: const Color(0x00000000),
)
..rect(
rect: const Rect.fromLTRB(796.0, 1.5, 800.0, 91.5),
color: const Color(0x1a000000),
),
);
});
testWidgets('Viewport basic test (RTL)', (WidgetTester tester) async {
......@@ -40,7 +57,24 @@ void main() {
));
expect(find.byType(Scrollbar), isNot(paints..rect()));
await tester.fling(find.byType(SingleChildScrollView), const Offset(0.0, -10.0), 10.0);
expect(find.byType(Scrollbar), paints..rect(rect: const Rect.fromLTRB(0.0, 0.0, 12.0, 600.0)));
expect(
find.byType(Scrollbar),
paints
..rect(
rect: const Rect.fromLTRB(0.0, 0.0, 4.0, 600.0),
color: const Color(0x00000000),
)
..line(
p1: const Offset(0.0, 0.0),
p2: const Offset(0.0, 600.0),
strokeWidth: 1.0,
color: const Color(0x00000000),
)
..rect(
rect: const Rect.fromLTRB(0.0, 1.5, 4.0, 91.5),
color: const Color(0x1a000000),
),
);
});
testWidgets('works with MaterialApp and Scaffold', (WidgetTester tester) async {
......@@ -67,15 +101,24 @@ void main() {
await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
expect(find.byType(Scrollbar), paints..rect(
rect: const Rect.fromLTWH(
800.0 - 12, // screen width - default thickness and margin
0, // the paint area starts from the bottom of the app bar
12, // thickness
// 56 being the height of the app bar
600.0 - 56 - 34 - 20,
),
));
expect(
find.byType(Scrollbar),
paints
..rect(
rect: const Rect.fromLTRB(796.0, 0.0, 800.0, 490.0),
color: const Color(0x00000000),
)
..line(
p1: const Offset(796.0, 0.0),
p2: const Offset(796.0, 490.0),
strokeWidth: 1.0,
color: const Color(0x00000000),
)
..rect(
rect: const Rect.fromLTWH(796.0, 0.0, 4.0, (600.0 - 56 - 34 - 20) / 4000 * (600 - 56 - 34 - 20)),
color: const Color(0x1a000000),
),
);
});
testWidgets("should not paint when there isn't enough space", (WidgetTester tester) async {
......
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