Unverified Commit 2a64fdbc authored by Kate Lovett's avatar Kate Lovett Committed by GitHub

Restore adaptive nature to new Scrollbar (#73899)

parent 527a2611
...@@ -26,6 +26,9 @@ const Duration _kScrollbarTimeToFade = Duration(milliseconds: 600); ...@@ -26,6 +26,9 @@ const Duration _kScrollbarTimeToFade = Duration(milliseconds: 600);
/// ///
/// {@macro flutter.widgets.Scrollbar} /// {@macro flutter.widgets.Scrollbar}
/// ///
/// Dynamically changes to an iOS style scrollbar that looks like
/// [CupertinoScrollbar] on the iOS platform.
///
/// The color of the Scrollbar will change when dragged. A hover animation is /// 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 /// 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 /// can also been drawn when triggered by a hover event, which is controlled by
...@@ -42,7 +45,7 @@ const Duration _kScrollbarTimeToFade = Duration(milliseconds: 600); ...@@ -42,7 +45,7 @@ const Duration _kScrollbarTimeToFade = Duration(milliseconds: 600);
/// * [CupertinoScrollbar], an iOS style scrollbar. /// * [CupertinoScrollbar], an iOS style scrollbar.
/// * [ListView], which displays a linear, scrollable list of children. /// * [ListView], which displays a linear, scrollable list of children.
/// * [GridView], which displays a 2 dimensional, scrollable array of children. /// * [GridView], which displays a 2 dimensional, scrollable array of children.
class Scrollbar extends RawScrollbar { class Scrollbar extends StatefulWidget {
/// Creates a material design scrollbar that by default will connect to the /// Creates a material design scrollbar that by default will connect to the
/// closest Scrollable descendant of [child]. /// closest Scrollable descendant of [child].
/// ///
...@@ -58,6 +61,90 @@ class Scrollbar extends RawScrollbar { ...@@ -58,6 +61,90 @@ class Scrollbar extends RawScrollbar {
/// except for when executing on [TargetPlatform.android], which will render the /// except for when executing on [TargetPlatform.android], which will render the
/// thumb without a radius. /// thumb without a radius.
const Scrollbar({ const Scrollbar({
Key? key,
required this.child,
this.controller,
this.isAlwaysShown,
this.showTrackOnHover,
this.hoverThickness,
this.thickness,
this.radius,
}) : super(key: key);
/// {@macro flutter.widgets.Scrollbar.child}
final Widget child;
/// {@macro flutter.widgets.Scrollbar.controller}
final ScrollController? controller;
/// {@macro flutter.widgets.Scrollbar.isAlwaysShown}
final bool? isAlwaysShown;
/// 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.
final bool? showTrackOnHover;
/// The thickness of the scrollbar when a hover state is active and
/// [showTrackOnHover] is true.
///
/// 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;
/// 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 color of the scrollbar thumb.
///
/// 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;
@override
_ScrollbarState createState() => _ScrollbarState();
}
class _ScrollbarState extends State<Scrollbar> {
bool get _useCupertinoScrollbar => Theme.of(context).platform == TargetPlatform.iOS;
@override
Widget build(BuildContext context) {
if (_useCupertinoScrollbar) {
return CupertinoScrollbar(
child: widget.child,
isAlwaysShown: widget.isAlwaysShown ?? false,
thickness: widget.thickness ?? CupertinoScrollbar.defaultThickness,
thicknessWhileDragging: widget.thickness ?? CupertinoScrollbar.defaultThicknessWhileDragging,
radius: widget.radius ?? CupertinoScrollbar.defaultRadius,
radiusWhileDragging: widget.radius ?? CupertinoScrollbar.defaultRadiusWhileDragging,
controller: widget.controller,
);
}
return _MaterialScrollbar(
child: widget.child,
controller: widget.controller,
isAlwaysShown: widget.isAlwaysShown,
showTrackOnHover: widget.showTrackOnHover,
hoverThickness: widget.hoverThickness,
thickness: widget.thickness,
radius: widget.radius,
);
}
}
class _MaterialScrollbar extends RawScrollbar {
const _MaterialScrollbar({
Key? key, Key? key,
required Widget child, required Widget child,
ScrollController? controller, ScrollController? controller,
...@@ -78,26 +165,14 @@ class Scrollbar extends RawScrollbar { ...@@ -78,26 +165,14 @@ class Scrollbar extends RawScrollbar {
pressDuration: Duration.zero, pressDuration: Duration.zero,
); );
/// 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.
final bool? showTrackOnHover; final bool? showTrackOnHover;
/// The thickness of the scrollbar when a hover state is active and
/// [showTrackOnHover] is true.
///
/// 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; final double? hoverThickness;
@override @override
_ScrollbarState createState() => _ScrollbarState(); _MaterialScrollbarState createState() => _MaterialScrollbarState();
} }
class _ScrollbarState extends RawScrollbarState<Scrollbar> { class _MaterialScrollbarState extends RawScrollbarState<_MaterialScrollbar> {
late AnimationController _hoverAnimationController; late AnimationController _hoverAnimationController;
bool _dragIsActive = false; bool _dragIsActive = false;
bool _hoverIsActive = false; bool _hoverIsActive = false;
......
...@@ -613,14 +613,17 @@ class RawScrollbar extends StatefulWidget { ...@@ -613,14 +613,17 @@ class RawScrollbar extends StatefulWidget {
assert(pressDuration != null), assert(pressDuration != null),
super(key: key); super(key: key);
/// {@template flutter.widgets.Scrollbar.child}
/// The widget below this widget in the tree. /// The widget below this widget in the tree.
/// ///
/// The scrollbar will be stacked on top of this child. This child (and its /// The scrollbar will be stacked on top of this child. This child (and its
/// subtree) should include a source of [ScrollNotification] notifications. /// subtree) should include a source of [ScrollNotification] notifications.
/// ///
/// Typically a [ListView] or [CustomScrollView]. /// Typically a [ListView] or [CustomScrollView].
/// {@endtemplate}
final Widget child; final Widget child;
/// {@template flutter.widgets.Scrollbar.controller}
/// The [ScrollController] used to implement Scrollbar dragging. /// The [ScrollController] used to implement Scrollbar dragging.
/// ///
/// If nothing is passed to controller, the default behavior is to automatically /// If nothing is passed to controller, the default behavior is to automatically
...@@ -670,8 +673,10 @@ class RawScrollbar extends StatefulWidget { ...@@ -670,8 +673,10 @@ class RawScrollbar extends StatefulWidget {
/// } /// }
/// ``` /// ```
/// {@end-tool} /// {@end-tool}
/// {@endtemplate}
final ScrollController? controller; final ScrollController? controller;
/// {@template flutter.widgets.Scrollbar.isAlwaysShown}
/// Indicates that the scrollbar should be visible, even when a scroll is not /// Indicates that the scrollbar should be visible, even when a scroll is not
/// underway. /// underway.
/// ///
...@@ -727,6 +732,7 @@ class RawScrollbar extends StatefulWidget { ...@@ -727,6 +732,7 @@ class RawScrollbar extends StatefulWidget {
/// } /// }
/// ``` /// ```
/// {@end-tool} /// {@end-tool}
/// {@endtemplate}
final bool? isAlwaysShown; final bool? isAlwaysShown;
/// The [Radius] of the scrollbar thumb's rounded rectangle corners. /// The [Radius] of the scrollbar thumb's rounded rectangle corners.
......
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -919,7 +921,7 @@ void main() { ...@@ -919,7 +921,7 @@ void main() {
}, },
variant: const TargetPlatformVariant(<TargetPlatform>{ variant: const TargetPlatformVariant(<TargetPlatform>{
TargetPlatform.iOS, TargetPlatform.linux,
}), }),
); );
...@@ -989,4 +991,72 @@ void main() { ...@@ -989,4 +991,72 @@ void main() {
TargetPlatform.fuchsia, TargetPlatform.fuchsia,
}), }),
); );
testWidgets('Adaptive scrollbar', (WidgetTester tester) async {
Widget viewWithScroll(TargetPlatform platform) {
return _buildBoilerplate(
child: Theme(
data: ThemeData(
platform: platform
),
child: const Scrollbar(
child: SingleChildScrollView(
child: SizedBox(width: 4000.0, height: 4000.0),
),
),
),
);
}
await tester.pumpWidget(viewWithScroll(TargetPlatform.android));
await tester.drag(find.byType(SingleChildScrollView), const Offset(0.0, -10.0));
await tester.pump();
// Scrollbar fully showing
await tester.pump(const Duration(milliseconds: 500));
expect(find.byType(Scrollbar), paints..rect());
await tester.pumpWidget(viewWithScroll(TargetPlatform.iOS));
final TestGesture gesture = await tester.startGesture(
tester.getCenter(find.byType(SingleChildScrollView))
);
await gesture.moveBy(const Offset(0.0, -10.0));
await tester.drag(find.byType(SingleChildScrollView), const Offset(0.0, -10.0));
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
expect(find.byType(Scrollbar), paints..rrect());
expect(find.byType(CupertinoScrollbar), paints..rrect());
await gesture.up();
await tester.pumpAndSettle();
});
testWidgets('Scrollbar passes controller to CupertinoScrollbar', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
Widget viewWithScroll(TargetPlatform? platform) {
return _buildBoilerplate(
child: Theme(
data: ThemeData(
platform: platform
),
child: Scrollbar(
controller: controller,
child: const SingleChildScrollView(
child: SizedBox(width: 4000.0, height: 4000.0),
),
),
),
);
}
await tester.pumpWidget(viewWithScroll(debugDefaultTargetPlatformOverride));
final TestGesture gesture = await tester.startGesture(
tester.getCenter(find.byType(SingleChildScrollView))
);
await gesture.moveBy(const Offset(0.0, -10.0));
await tester.drag(find.byType(SingleChildScrollView), const Offset(0.0, -10.0));
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
expect(find.byType(CupertinoScrollbar), paints..rrect());
final CupertinoScrollbar scrollbar = tester.widget<CupertinoScrollbar>(find.byType(CupertinoScrollbar));
expect(scrollbar.controller, isNotNull);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }));
} }
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