Commit 0120c414 authored by Pieter van Loon's avatar Pieter van Loon Committed by Justin McCandless

Improved ios 13 scrollbar fidelity (#41799)

Drag from the right is no more
Longpress is now only 100ms instead of 500
Added optional duration field to longpressgesturerecognizer
Added controller field to material scrollbar api
Haptic feedback only triggers when scrollbar is fully expanded
Added haptic feedback when releasing the scrollbar after dragging it
parent 2cedd559
...@@ -15,7 +15,7 @@ const double _kScrollbarMinLength = 36.0; ...@@ -15,7 +15,7 @@ const double _kScrollbarMinLength = 36.0;
const double _kScrollbarMinOverscrollLength = 8.0; const double _kScrollbarMinOverscrollLength = 8.0;
const Duration _kScrollbarTimeToFade = Duration(milliseconds: 1200); const Duration _kScrollbarTimeToFade = Duration(milliseconds: 1200);
const Duration _kScrollbarFadeDuration = Duration(milliseconds: 250); const Duration _kScrollbarFadeDuration = Duration(milliseconds: 250);
const Duration _kScrollbarResizeDuration = Duration(milliseconds: 150); const Duration _kScrollbarResizeDuration = Duration(milliseconds: 100);
// Extracted from iOS 13.1 beta using Debug View Hierarchy. // Extracted from iOS 13.1 beta using Debug View Hierarchy.
const Color _kScrollbarColor = CupertinoDynamicColor.withBrightness( const Color _kScrollbarColor = CupertinoDynamicColor.withBrightness(
...@@ -42,6 +42,10 @@ const double _kScrollbarCrossAxisMargin = 3.0; ...@@ -42,6 +42,10 @@ const double _kScrollbarCrossAxisMargin = 3.0;
/// To add a scrollbar to a [ScrollView], simply wrap the scroll view widget in /// To add a scrollbar to a [ScrollView], simply wrap the scroll view widget in
/// a [CupertinoScrollbar] widget. /// a [CupertinoScrollbar] widget.
/// ///
/// By default, the CupertinoScrollbar will be draggable (a feature introduced
/// in iOS 13), it uses the PrimaryScrollController. For multiple scrollbars, or
/// other more complicated situations, see the [controller] parameter.
///
/// See also: /// See also:
/// ///
/// * [ListView], which display a linear, scrollable list of children. /// * [ListView], which display a linear, scrollable list of children.
...@@ -65,39 +69,60 @@ class CupertinoScrollbar extends StatefulWidget { ...@@ -65,39 +69,60 @@ class CupertinoScrollbar extends StatefulWidget {
/// typically a [Scrollable] widget. /// typically a [Scrollable] widget.
final Widget child; final Widget child;
/// {@template flutter.cupertino.cupertinoScrollbar.controller}
/// The [ScrollController] used to implement Scrollbar dragging. /// The [ScrollController] used to implement Scrollbar dragging.
/// ///
/// Scrollbar dragging is started with a long press or a drag in from the side
/// on top of the scrollbar thumb, which enlarges the thumb and makes it
/// interactive. Dragging it then causes the view to scroll. This feature was
/// introduced in iOS 13. /// introduced in iOS 13.
/// ///
/// In order to enable this feature, pass an active ScrollController to this /// If nothing is passed to controller, the default behavior is to automatically
/// parameter. A stateful ancestor of this CupertinoScrollbar needs to /// enable scrollbar dragging on the nearest ScrollController using
/// manage the ScrollController and either pass it to a scrollable descendant /// [PrimaryScrollController.of].
/// or use a PrimaryScrollController to share it. ///
/// If a ScrollController is passed, then scrollbar dragging will be enabled on
/// the given ScrollController. A stateful ancestor of this CupertinoScrollbar
/// needs to manage the ScrollController and either pass it to a scrollable
/// descendant or use a PrimaryScrollController to share it.
/// ///
/// Here is an example of using PrimaryScrollController to enable scrollbar /// Here is an example of using the `controller` parameter to enable
/// dragging: /// scrollbar dragging for multiple independent ListViews:
/// ///
/// {@tool sample} /// {@tool sample}
/// ///
/// ```dart /// ```dart
/// final ScrollController _controllerOne = ScrollController();
/// final ScrollController _controllerTwo = ScrollController();
///
/// build(BuildContext context) { /// build(BuildContext context) {
/// final ScrollController controller = ScrollController(); /// return Column(
/// return PrimaryScrollController( /// children: <Widget>[
/// controller: controller, /// Container(
/// child: CupertinoScrollbar( /// height: 200,
/// controller: controller, /// child: CupertinoScrollbar(
/// child: ListView.builder( /// controller: _controllerOne,
/// itemCount: 150, /// child: ListView.builder(
/// itemBuilder: (BuildContext context, int index) => Text('item $index'), /// controller: _controllerOne,
/// ), /// itemCount: 120,
/// ), /// itemBuilder: (BuildContext context, int index) => Text('item $index'),
/// ),
/// ),
/// ),
/// Container(
/// height: 200,
/// child: CupertinoScrollbar(
/// controller: _controllerTwo,
/// child: ListView.builder(
/// controller: _controllerTwo,
/// itemCount: 120,
/// itemBuilder: (BuildContext context, int index) => Text('list 2 item $index'),
/// ),
/// ),
/// ),
/// ],
/// ); /// );
/// } /// }
/// ``` /// ```
/// {@end-tool} /// {@end-tool}
/// {@endtemplate}
final ScrollController controller; final ScrollController controller;
@override @override
...@@ -123,6 +148,10 @@ class _CupertinoScrollbarState extends State<CupertinoScrollbar> with TickerProv ...@@ -123,6 +148,10 @@ class _CupertinoScrollbarState extends State<CupertinoScrollbar> with TickerProv
return Radius.lerp(_kScrollbarRadius, _kScrollbarRadiusDragging, _thicknessAnimationController.value); return Radius.lerp(_kScrollbarRadius, _kScrollbarRadiusDragging, _thicknessAnimationController.value);
} }
ScrollController _currentController;
ScrollController get _controller =>
widget.controller ?? PrimaryScrollController.of(context);
@override @override
void initState() { void initState() {
super.initState(); super.initState();
...@@ -148,8 +177,7 @@ class _CupertinoScrollbarState extends State<CupertinoScrollbar> with TickerProv ...@@ -148,8 +177,7 @@ class _CupertinoScrollbarState extends State<CupertinoScrollbar> with TickerProv
super.didChangeDependencies(); super.didChangeDependencies();
if (_painter == null) { if (_painter == null) {
_painter = _buildCupertinoScrollbarPainter(context); _painter = _buildCupertinoScrollbarPainter(context);
} } else {
else {
_painter _painter
..textDirection = Directionality.of(context) ..textDirection = Directionality.of(context)
..color = CupertinoDynamicColor.resolve(_kScrollbarColor, context) ..color = CupertinoDynamicColor.resolve(_kScrollbarColor, context)
...@@ -175,16 +203,16 @@ class _CupertinoScrollbarState extends State<CupertinoScrollbar> with TickerProv ...@@ -175,16 +203,16 @@ class _CupertinoScrollbarState extends State<CupertinoScrollbar> with TickerProv
// Handle a gesture that drags the scrollbar by the given amount. // Handle a gesture that drags the scrollbar by the given amount.
void _dragScrollbar(double primaryDelta) { void _dragScrollbar(double primaryDelta) {
assert(widget.controller != null); assert(_currentController != null);
// Convert primaryDelta, the amount that the scrollbar moved since the last // Convert primaryDelta, the amount that the scrollbar moved since the last
// time _dragScrollbar was called, into the coordinate space of the scroll // time _dragScrollbar was called, into the coordinate space of the scroll
// position, and create/update the drag event with that position. // position, and create/update the drag event with that position.
final double scrollOffsetLocal = _painter.getTrackToScroll(primaryDelta); final double scrollOffsetLocal = _painter.getTrackToScroll(primaryDelta);
final double scrollOffsetGlobal = scrollOffsetLocal + widget.controller.position.pixels; final double scrollOffsetGlobal = scrollOffsetLocal + _currentController.position.pixels;
if (_drag == null) { if (_drag == null) {
_drag = widget.controller.position.drag( _drag = _currentController.position.drag(
DragStartDetails( DragStartDetails(
globalPosition: Offset(0.0, scrollOffsetGlobal), globalPosition: Offset(0.0, scrollOffsetGlobal),
), ),
...@@ -207,64 +235,62 @@ class _CupertinoScrollbarState extends State<CupertinoScrollbar> with TickerProv ...@@ -207,64 +235,62 @@ class _CupertinoScrollbarState extends State<CupertinoScrollbar> with TickerProv
}); });
} }
void _assertVertical() { bool _checkVertical() {
assert( try {
widget.controller.position.axis == Axis.vertical, return _currentController.position.axis == Axis.vertical;
'Scrollbar dragging is only supported for vertical scrolling. Don\'t pass the controller param to a horizontal scrollbar.', } catch (_) {
); // Ignore the gesture if we cannot determine the direction.
return false;
}
} }
double _pressStartY = 0.0;
// Long press event callbacks handle the gesture where the user long presses // Long press event callbacks handle the gesture where the user long presses
// on the scrollbar thumb and then drags the scrollbar without releasing. // on the scrollbar thumb and then drags the scrollbar without releasing.
void _handleLongPressStart(LongPressStartDetails details) { void _handleLongPressStart(LongPressStartDetails details) {
_assertVertical(); _currentController = _controller;
if (!_checkVertical()) {
return;
}
_pressStartY = details.localPosition.dy;
_fadeoutTimer?.cancel(); _fadeoutTimer?.cancel();
_fadeoutAnimationController.forward(); _fadeoutAnimationController.forward();
HapticFeedback.mediumImpact();
_dragScrollbar(details.localPosition.dy); _dragScrollbar(details.localPosition.dy);
_dragScrollbarPositionY = details.localPosition.dy; _dragScrollbarPositionY = details.localPosition.dy;
} }
void _handleLongPress() { void _handleLongPress() {
_assertVertical(); if (!_checkVertical()) {
return;
}
_fadeoutTimer?.cancel(); _fadeoutTimer?.cancel();
_thicknessAnimationController.forward(); _thicknessAnimationController.forward().then<void>(
(_) => HapticFeedback.mediumImpact(),
);
} }
void _handleLongPressMoveUpdate(LongPressMoveUpdateDetails details) { void _handleLongPressMoveUpdate(LongPressMoveUpdateDetails details) {
_assertVertical(); if (!_checkVertical()) {
return;
}
_dragScrollbar(details.localPosition.dy - _dragScrollbarPositionY); _dragScrollbar(details.localPosition.dy - _dragScrollbarPositionY);
_dragScrollbarPositionY = details.localPosition.dy; _dragScrollbarPositionY = details.localPosition.dy;
} }
void _handleLongPressEnd(LongPressEndDetails details) { void _handleLongPressEnd(LongPressEndDetails details) {
if (!_checkVertical()) {
return;
}
_handleDragScrollEnd(details.velocity.pixelsPerSecond.dy); _handleDragScrollEnd(details.velocity.pixelsPerSecond.dy);
} if (details.velocity.pixelsPerSecond.dy.abs() < 10 &&
(details.localPosition.dy - _pressStartY).abs() > 0) {
// Horizontal drag event callbacks handle the gesture where the user swipes in HapticFeedback.mediumImpact();
// from the right on top of the scrollbar thumb and then drags the scrollbar }
// without releasing. _currentController = null;
void _handleHorizontalDragStart(DragStartDetails details) {
_assertVertical();
_fadeoutTimer?.cancel();
_thicknessAnimationController.forward();
HapticFeedback.mediumImpact();
_dragScrollbar(details.localPosition.dy);
_dragScrollbarPositionY = details.localPosition.dy;
}
void _handleHorizontalDragUpdate(DragUpdateDetails details) {
_assertVertical();
_dragScrollbar(details.localPosition.dy - _dragScrollbarPositionY);
_dragScrollbarPositionY = details.localPosition.dy;
}
void _handleHorizontalDragEnd(DragEndDetails details) {
_handleDragScrollEnd(details.velocity.pixelsPerSecond.dy);
} }
void _handleDragScrollEnd(double trackVelocityY) { void _handleDragScrollEnd(double trackVelocityY) {
_assertVertical();
_startFadeoutTimer(); _startFadeoutTimer();
_thicknessAnimationController.reverse(); _thicknessAnimationController.reverse();
_dragScrollbarPositionY = null; _dragScrollbarPositionY = null;
...@@ -308,40 +334,23 @@ class _CupertinoScrollbarState extends State<CupertinoScrollbar> with TickerProv ...@@ -308,40 +334,23 @@ class _CupertinoScrollbarState extends State<CupertinoScrollbar> with TickerProv
// Get the GestureRecognizerFactories used to detect gestures on the scrollbar // Get the GestureRecognizerFactories used to detect gestures on the scrollbar
// thumb. // thumb.
Map<Type, GestureRecognizerFactory> get _gestures { Map<Type, GestureRecognizerFactory> get _gestures {
final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{}; final Map<Type, GestureRecognizerFactory> gestures =
if (widget.controller == null) { <Type, GestureRecognizerFactory>{};
return gestures;
} gestures[_ThumbPressGestureRecognizer] =
GestureRecognizerFactoryWithHandlers<_ThumbPressGestureRecognizer>(
gestures[_ThumbLongPressGestureRecognizer] = () => _ThumbPressGestureRecognizer(
GestureRecognizerFactoryWithHandlers<_ThumbLongPressGestureRecognizer>( debugOwner: this,
() => _ThumbLongPressGestureRecognizer( customPaintKey: _customPaintKey,
debugOwner: this, ),
kind: PointerDeviceKind.touch, (_ThumbPressGestureRecognizer instance) {
customPaintKey: _customPaintKey, instance
), ..onLongPressStart = _handleLongPressStart
(_ThumbLongPressGestureRecognizer instance) { ..onLongPress = _handleLongPress
instance ..onLongPressMoveUpdate = _handleLongPressMoveUpdate
..onLongPressStart = _handleLongPressStart ..onLongPressEnd = _handleLongPressEnd;
..onLongPress = _handleLongPress },
..onLongPressMoveUpdate = _handleLongPressMoveUpdate );
..onLongPressEnd = _handleLongPressEnd;
},
);
gestures[_ThumbHorizontalDragGestureRecognizer] =
GestureRecognizerFactoryWithHandlers<_ThumbHorizontalDragGestureRecognizer>(
() => _ThumbHorizontalDragGestureRecognizer(
debugOwner: this,
kind: PointerDeviceKind.touch,
customPaintKey: _customPaintKey,
),
(_ThumbHorizontalDragGestureRecognizer instance) {
instance
..onStart = _handleHorizontalDragStart
..onUpdate = _handleHorizontalDragUpdate
..onEnd = _handleHorizontalDragEnd;
},
);
return gestures; return gestures;
} }
...@@ -375,8 +384,8 @@ class _CupertinoScrollbarState extends State<CupertinoScrollbar> with TickerProv ...@@ -375,8 +384,8 @@ class _CupertinoScrollbarState extends State<CupertinoScrollbar> with TickerProv
// A longpress gesture detector that only responds to events on the scrollbar's // A longpress gesture detector that only responds to events on the scrollbar's
// thumb and ignores everything else. // thumb and ignores everything else.
class _ThumbLongPressGestureRecognizer extends LongPressGestureRecognizer { class _ThumbPressGestureRecognizer extends LongPressGestureRecognizer {
_ThumbLongPressGestureRecognizer({ _ThumbPressGestureRecognizer({
double postAcceptSlopTolerance, double postAcceptSlopTolerance,
PointerDeviceKind kind, PointerDeviceKind kind,
Object debugOwner, Object debugOwner,
...@@ -386,6 +395,7 @@ class _ThumbLongPressGestureRecognizer extends LongPressGestureRecognizer { ...@@ -386,6 +395,7 @@ class _ThumbLongPressGestureRecognizer extends LongPressGestureRecognizer {
postAcceptSlopTolerance: postAcceptSlopTolerance, postAcceptSlopTolerance: postAcceptSlopTolerance,
kind: kind, kind: kind,
debugOwner: debugOwner, debugOwner: debugOwner,
duration: const Duration(milliseconds: 100),
); );
final GlobalKey _customPaintKey; final GlobalKey _customPaintKey;
...@@ -399,39 +409,6 @@ class _ThumbLongPressGestureRecognizer extends LongPressGestureRecognizer { ...@@ -399,39 +409,6 @@ class _ThumbLongPressGestureRecognizer extends LongPressGestureRecognizer {
} }
} }
// A horizontal drag gesture detector that only responds to events on the
// scrollbar's thumb and ignores everything else.
class _ThumbHorizontalDragGestureRecognizer extends HorizontalDragGestureRecognizer {
_ThumbHorizontalDragGestureRecognizer({
PointerDeviceKind kind,
Object debugOwner,
GlobalKey customPaintKey,
}) : _customPaintKey = customPaintKey,
super(
kind: kind,
debugOwner: debugOwner,
);
final GlobalKey _customPaintKey;
@override
bool isPointerAllowed(PointerEvent event) {
if (!_hitTestInteractive(_customPaintKey, event.position)) {
return false;
}
return super.isPointerAllowed(event);
}
// Flings are actually in the vertical direction. Even though the event starts
// horizontal, the scrolling is tracked vertically.
@override
bool isFlingGesture(VelocityEstimate estimate) {
final double minVelocity = minFlingVelocity ?? kMinFlingVelocity;
final double minDistance = minFlingDistance ?? kTouchSlop;
return estimate.pixelsPerSecond.dy.abs() > minVelocity && estimate.offset.dy.abs() > minDistance;
}
}
// foregroundPainter also hit tests its children by default, but the // foregroundPainter also hit tests its children by default, but the
// scrollbar should only respond to a gesture directly on its thumb, so // scrollbar should only respond to a gesture directly on its thumb, so
// manually check for a hit on the thumb here. // manually check for a hit on the thumb here.
......
...@@ -156,16 +156,20 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer { ...@@ -156,16 +156,20 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
/// subsequent callbacks ([onLongPressMoveUpdate], [onLongPressUp], /// subsequent callbacks ([onLongPressMoveUpdate], [onLongPressUp],
/// [onLongPressEnd]) will stop. Defaults to null, which means the gesture /// [onLongPressEnd]) will stop. Defaults to null, which means the gesture
/// can be moved without limit once the long press is accepted. /// can be moved without limit once the long press is accepted.
///
/// The [duration] argument can be used to overwrite the default duration
/// after which the long press will be recognized.
LongPressGestureRecognizer({ LongPressGestureRecognizer({
Duration duration,
double postAcceptSlopTolerance, double postAcceptSlopTolerance,
PointerDeviceKind kind, PointerDeviceKind kind,
Object debugOwner, Object debugOwner,
}) : super( }) : super(
deadline: kLongPressTimeout, deadline: duration ?? kLongPressTimeout,
postAcceptSlopTolerance: postAcceptSlopTolerance, postAcceptSlopTolerance: postAcceptSlopTolerance,
kind: kind, kind: kind,
debugOwner: debugOwner, debugOwner: debugOwner,
); );
bool _longPressAccepted = false; bool _longPressAccepted = false;
OffsetPair _longPressOrigin; OffsetPair _longPressOrigin;
......
...@@ -36,6 +36,7 @@ class Scrollbar extends StatefulWidget { ...@@ -36,6 +36,7 @@ class Scrollbar extends StatefulWidget {
const Scrollbar({ const Scrollbar({
Key key, Key key,
@required this.child, @required this.child,
this.controller,
}) : super(key: key); }) : super(key: key);
/// The widget below this widget in the tree. /// The widget below this widget in the tree.
...@@ -46,11 +47,13 @@ class Scrollbar extends StatefulWidget { ...@@ -46,11 +47,13 @@ class Scrollbar extends StatefulWidget {
/// Typically a [ListView] or [CustomScrollView]. /// Typically a [ListView] or [CustomScrollView].
final Widget child; final Widget child;
/// {@macro flutter.cupertino.cupertinoScrollbar.controller}
final ScrollController controller;
@override @override
_ScrollbarState createState() => _ScrollbarState(); _ScrollbarState createState() => _ScrollbarState();
} }
class _ScrollbarState extends State<Scrollbar> with TickerProviderStateMixin { class _ScrollbarState extends State<Scrollbar> with TickerProviderStateMixin {
ScrollbarPainter _materialPainter; ScrollbarPainter _materialPainter;
TextDirection _textDirection; TextDirection _textDirection;
...@@ -148,6 +151,7 @@ class _ScrollbarState extends State<Scrollbar> with TickerProviderStateMixin { ...@@ -148,6 +151,7 @@ class _ScrollbarState extends State<Scrollbar> with TickerProviderStateMixin {
if (_useCupertinoScrollbar) { if (_useCupertinoScrollbar) {
return CupertinoScrollbar( return CupertinoScrollbar(
child: widget.child, child: widget.child,
controller: widget.controller,
); );
} }
return NotificationListener<ScrollNotification>( return NotificationListener<ScrollNotification>(
......
...@@ -10,13 +10,13 @@ import '../rendering/mock_canvas.dart'; ...@@ -10,13 +10,13 @@ import '../rendering/mock_canvas.dart';
const CupertinoDynamicColor _kScrollbarColor = CupertinoDynamicColor.withBrightness( const CupertinoDynamicColor _kScrollbarColor = CupertinoDynamicColor.withBrightness(
color: Color(0x59000000), color: Color(0x59000000),
darkColor:Color(0x80FFFFFF), darkColor: Color(0x80FFFFFF),
); );
void main() { void main() {
const Duration _kScrollbarTimeToFade = Duration(milliseconds: 1200); const Duration _kScrollbarTimeToFade = Duration(milliseconds: 1200);
const Duration _kScrollbarFadeDuration = Duration(milliseconds: 250); const Duration _kScrollbarFadeDuration = Duration(milliseconds: 250);
const Duration _kScrollbarResizeDuration = Duration(milliseconds: 150); const Duration _kScrollbarResizeDuration = Duration(milliseconds: 100);
testWidgets('Scrollbar never goes away until finger lift', (WidgetTester tester) async { testWidgets('Scrollbar never goes away until finger lift', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -102,9 +102,8 @@ void main() { ...@@ -102,9 +102,8 @@ void main() {
data: const MediaQueryData(), data: const MediaQueryData(),
child: PrimaryScrollController( child: PrimaryScrollController(
controller: scrollController, controller: scrollController,
child: CupertinoScrollbar( child: const CupertinoScrollbar(
controller: scrollController, child: SingleChildScrollView(child: SizedBox(width: 4000.0, height: 4000.0)),
child: const SingleChildScrollView(child: SizedBox(width: 4000.0, height: 4000.0)),
), ),
), ),
), ),
...@@ -136,86 +135,19 @@ void main() { ...@@ -136,86 +135,19 @@ void main() {
} }
}); });
// Longpress on the scrollbar thumb and expect a vibration. // Longpress on the scrollbar thumb and expect a vibration after it resizes.
expect(hapticFeedbackCalls, 0); expect(hapticFeedbackCalls, 0);
final TestGesture dragScrollbarGesture = await tester.startGesture(const Offset(796.0, 50.0)); final TestGesture dragScrollbarGesture = await tester.startGesture(const Offset(796.0, 50.0));
await tester.pump(const Duration(milliseconds: 500)); await tester.pump(const Duration(milliseconds: 100));
expect(hapticFeedbackCalls, 1);
// Drag the thumb down to scroll down.
await dragScrollbarGesture.moveBy(const Offset(0.0, scrollAmount));
await tester.pump(const Duration(milliseconds: 500));
await dragScrollbarGesture.up();
await tester.pumpAndSettle();
// The view has scrolled more than it would have by a swipe gesture of the
// same distance.
expect(scrollController.offset, greaterThan(scrollAmount * 2));
// The scrollbar thumb is still fully visible.
expect(find.byType(CupertinoScrollbar), paints..rrect(
color: _kScrollbarColor.color,
));
// Let the thumb fade out so all timers have resolved.
await tester.pump(_kScrollbarTimeToFade);
await tester.pump(_kScrollbarFadeDuration);
});
testWidgets('Scrollbar thumb can be dragged by swiping in from right', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: PrimaryScrollController(
controller: scrollController,
child: CupertinoScrollbar(
controller: scrollController,
child: const SingleChildScrollView(child: SizedBox(width: 4000.0, height: 4000.0)),
),
),
),
),
);
expect(scrollController.offset, 0.0);
// Scroll a bit.
const double scrollAmount = 10.0;
final TestGesture scrollGesture = await tester.startGesture(tester.getCenter(find.byType(SingleChildScrollView)));
// Scroll down by swiping up.
await scrollGesture.moveBy(const Offset(0.0, -scrollAmount));
await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
// Scrollbar thumb is fully showing and scroll offset has moved by
// scrollAmount.
expect(find.byType(CupertinoScrollbar), paints..rrect(
color: _kScrollbarColor.color,
));
expect(scrollController.offset, scrollAmount);
await scrollGesture.up();
await tester.pump();
int hapticFeedbackCalls = 0;
SystemChannels.platform.setMockMethodCallHandler((MethodCall methodCall) async {
if (methodCall.method == 'HapticFeedback.vibrate') {
hapticFeedbackCalls++;
}
});
// Drag in from the right side on top of the scrollbar thumb and expect a
// vibration.
expect(hapticFeedbackCalls, 0); expect(hapticFeedbackCalls, 0);
final TestGesture dragScrollbarGesture = await tester.startGesture(const Offset(796.0, 50.0));
await tester.pump();
await dragScrollbarGesture.moveBy(const Offset(-50.0, 0.0));
await tester.pump(_kScrollbarResizeDuration); await tester.pump(_kScrollbarResizeDuration);
// Allow the haptic feedback some slack.
await tester.pump(const Duration(milliseconds: 1));
expect(hapticFeedbackCalls, 1); expect(hapticFeedbackCalls, 1);
// Drag the thumb down to scroll down. // Drag the thumb down to scroll down.
await dragScrollbarGesture.moveBy(const Offset(0.0, scrollAmount)); await dragScrollbarGesture.moveBy(const Offset(0.0, scrollAmount));
await tester.pump(const Duration(milliseconds: 500)); await tester.pump(const Duration(milliseconds: 100));
await dragScrollbarGesture.up(); await dragScrollbarGesture.up();
await tester.pumpAndSettle(); await tester.pumpAndSettle();
......
...@@ -80,6 +80,29 @@ void main() { ...@@ -80,6 +80,29 @@ void main() {
longPress.dispose(); longPress.dispose();
}); });
testGesture('Should recognize long press with altered duration', (GestureTester tester) {
longPress = LongPressGestureRecognizer(duration: const Duration(milliseconds: 100));
longPressDown = false;
longPress.onLongPress = () {
longPressDown = true;
};
longPressUp = false;
longPress.onLongPressUp = () {
longPressUp = true;
};
longPress.addPointer(down);
tester.closeArena(5);
expect(longPressDown, isFalse);
tester.route(down);
expect(longPressDown, isFalse);
tester.async.elapse(const Duration(milliseconds: 50));
expect(longPressDown, isFalse);
tester.async.elapse(const Duration(milliseconds: 50));
expect(longPressDown, isTrue);
longPress.dispose();
});
testGesture('Up cancels long press', (GestureTester tester) { testGesture('Up cancels long press', (GestureTester tester) {
longPress.addPointer(down); longPress.addPointer(down);
tester.closeArena(5); tester.closeArena(5);
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/cupertino.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';
...@@ -151,6 +152,38 @@ void main() { ...@@ -151,6 +152,38 @@ void main() {
await tester.drag(find.byType(SingleChildScrollView), const Offset(0.0, -10.0)); await tester.drag(find.byType(SingleChildScrollView), const Offset(0.0, -10.0));
await tester.pump(); await tester.pump();
await tester.pump(const Duration(milliseconds: 200)); await tester.pump(const Duration(milliseconds: 200));
expect(find.byType(Scrollbar), paints..rrect()); expect(find.byType(CupertinoScrollbar), paints..rrect());
}); });
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(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(CupertinoScrollbar), paints..rrect());
final CupertinoScrollbar scrollbar = find.byType(CupertinoScrollbar).evaluate().first.widget;
expect(scrollbar.controller, isNotNull);
});
} }
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