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
...@@ -156,12 +156,16 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer { ...@@ -156,12 +156,16 @@ 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,
......
...@@ -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