InteractiveViewer should call onInteractionUpdate even when gesture is disabled (#78990)

This commit is contained in:
Justin McCandless 2021-03-31 14:14:04 -07:00 committed by GitHub
parent 97a75b9c46
commit 4982a7f185
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 167 additions and 21 deletions

View file

@ -253,11 +253,12 @@ class InteractiveViewer extends StatefulWidget {
/// Called when the user ends a pan or scale gesture on the widget.
///
/// At the time this is called, the [TransformationController] will have
/// already been updated to reflect the change caused by the interaction.
/// already been updated to reflect the change caused by the interaction,
/// though a pan may cause an inertia animation after this is called as well.
///
/// {@template flutter.widgets.InteractiveViewer.onInteractionEnd}
/// Will be called even if the interaction is disabled with
/// [panEnabled] or [scaleEnabled].
/// Will be called even if the interaction is disabled with [panEnabled] or
/// [scaleEnabled] for both touch gestures and mouse interactions.
///
/// A [GestureDetector] wrapping the InteractiveViewer will not respond to
/// [GestureDetector.onScaleStart], [GestureDetector.onScaleUpdate], and
@ -294,7 +295,8 @@ class InteractiveViewer extends StatefulWidget {
/// Called when the user updates a pan or scale gesture on the widget.
///
/// At the time this is called, the [TransformationController] will have
/// already been updated to reflect the change caused by the interaction.
/// already been updated to reflect the change caused by the interaction, if
/// the interation caused the matrix to change.
///
/// {@macro flutter.widgets.InteractiveViewer.onInteractionEnd}
///
@ -796,6 +798,7 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid
_gestureType ??= _getGestureType(details);
}
if (!_gestureIsSupported(_gestureType)) {
widget.onInteractionUpdate?.call(details);
return;
}
@ -839,6 +842,7 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid
case _GestureType.rotate:
if (details.rotation == 0.0) {
widget.onInteractionUpdate?.call(details);
return;
}
final double desiredRotation = _rotationStart! + details.rotation;
@ -856,6 +860,7 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid
// In an effort to keep the behavior similar whether or not scaleEnabled
// is true, these gestures are thrown away.
if (details.scale != 1.0) {
widget.onInteractionUpdate?.call(details);
return;
}
_panAxis ??= _getPanAxis(_referenceFocalPoint!, focalPointScene);
@ -871,12 +876,7 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid
);
break;
}
widget.onInteractionUpdate?.call(ScaleUpdateDetails(
focalPoint: details.focalPoint,
localFocalPoint: details.localFocalPoint,
scale: details.scale,
rotation: details.rotation,
));
widget.onInteractionUpdate?.call(details);
}
// Handle the end of a gesture of _GestureType. All of pan, scale, and rotate
@ -932,25 +932,36 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid
// Handle mousewheel scroll events.
void _receivedPointerSignal(PointerSignalEvent event) {
if (event is PointerScrollEvent) {
// Ignore left and right scroll.
if (event.scrollDelta.dy == 0.0) {
return;
}
widget.onInteractionStart?.call(
ScaleStartDetails(
focalPoint: event.position,
localFocalPoint: event.localPosition,
),
);
// In the Flutter engine, the mousewheel scrollDelta is hardcoded to 20
// per scroll, while a trackpad scroll can be any amount. The calculation
// for scaleChange here was arbitrarily chosen to feel natural for both
// trackpads and mousewheels on all platforms.
final double scaleChange = math.exp(-event.scrollDelta.dy / 200);
if (!_gestureIsSupported(_GestureType.scale)) {
widget.onInteractionUpdate?.call(ScaleUpdateDetails(
focalPoint: event.position,
localFocalPoint: event.localPosition,
rotation: 0.0,
scale: scaleChange,
horizontalScale: 1.0,
verticalScale: 1.0,
));
widget.onInteractionEnd?.call(ScaleEndDetails());
return;
}
// Ignore left and right scroll.
if (event.scrollDelta.dy == 0.0) {
return;
}
// In the Flutter engine, the mousewheel scrollDelta is hardcoded to 20 per scroll, while a trackpad scroll can be any amount.
// The calculation for scaleChange here was arbitrarily chosen to feel natural for both trackpads and mousewheels on all platforms.
final double scaleChange = math.exp(-event.scrollDelta.dy / 200);
final Offset focalPointScene = _transformationController!.toScene(
event.localPosition,
);

View file

@ -4,6 +4,7 @@
import 'dart:math' as math;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:vector_math/vector_math_64.dart' show Quad, Vector3, Matrix4;
@ -717,15 +718,15 @@ void main() {
body: Center(
child: InteractiveViewer(
transformationController: transformationController,
onInteractionStart: (ScaleStartDetails details){
onInteractionStart: (ScaleStartDetails details) {
calledStart = true;
},
onInteractionUpdate: (ScaleUpdateDetails details){
onInteractionUpdate: (ScaleUpdateDetails details) {
scaleChange = details.scale;
focalPoint = details.focalPoint;
localFocalPoint = details.localFocalPoint;
},
onInteractionEnd: (ScaleEndDetails details){
onInteractionEnd: (ScaleEndDetails details) {
currentVelocity = details.velocity;
},
child: const SizedBox(width: 200.0, height: 200.0),
@ -758,6 +759,140 @@ void main() {
expect(scenePoint.dy, greaterThan(0.0));
});
testWidgets('onInteraction is called even when disabled (touch)', (WidgetTester tester) async {
final TransformationController transformationController = TransformationController();
bool calledStart = false;
bool calledUpdate = false;
bool calledEnd = false;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: InteractiveViewer(
transformationController: transformationController,
scaleEnabled: false,
onInteractionStart: (ScaleStartDetails details) {
calledStart = true;
},
onInteractionUpdate: (ScaleUpdateDetails details) {
calledUpdate = true;
},
onInteractionEnd: (ScaleEndDetails details) {
calledEnd = true;
},
child: const SizedBox(width: 200.0, height: 200.0),
),
),
),
),
);
final Offset childOffset = tester.getTopLeft(find.byType(SizedBox));
final Offset childInterior = Offset(
childOffset.dx + 20.0,
childOffset.dy + 20.0,
);
TestGesture gesture = await tester.startGesture(childOffset);
// Attempting to pan doesn't work because it's disabled, but the
// interaction methods are still called.
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.moveTo(childInterior);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(transformationController.value, equals(Matrix4.identity()));
expect(calledStart, isTrue);
expect(calledUpdate, isTrue);
expect(calledEnd, isTrue);
// Attempting to pinch to zoom doesn't work because it's disabled, but the
// interaction methods are still called.
calledStart = false;
calledUpdate = false;
calledEnd = false;
final Offset scaleStart1 = childInterior;
final Offset scaleStart2 = Offset(childInterior.dx + 10.0, childInterior.dy);
final Offset scaleEnd1 = Offset(childInterior.dx - 10.0, childInterior.dy);
final Offset scaleEnd2 = Offset(childInterior.dx + 20.0, childInterior.dy);
gesture = await tester.startGesture(scaleStart1);
final TestGesture gesture2 = await tester.startGesture(scaleStart2);
addTearDown(gesture2.removePointer);
await tester.pump();
await gesture.moveTo(scaleEnd1);
await gesture2.moveTo(scaleEnd2);
await tester.pump();
await gesture.up();
await gesture2.up();
await tester.pumpAndSettle();
expect(transformationController.value, equals(Matrix4.identity()));
expect(calledStart, isTrue);
expect(calledUpdate, isTrue);
expect(calledEnd, isTrue);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.iOS }));
testWidgets('onInteraction is called even when disabled (mouse)', (WidgetTester tester) async {
final TransformationController transformationController = TransformationController();
bool calledStart = false;
bool calledUpdate = false;
bool calledEnd = false;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: InteractiveViewer(
transformationController: transformationController,
scaleEnabled: false,
onInteractionStart: (ScaleStartDetails details) {
calledStart = true;
},
onInteractionUpdate: (ScaleUpdateDetails details) {
calledUpdate = true;
},
onInteractionEnd: (ScaleEndDetails details) {
calledEnd = true;
},
child: const SizedBox(width: 200.0, height: 200.0),
),
),
),
),
);
final Offset childOffset = tester.getTopLeft(find.byType(SizedBox));
final Offset childInterior = Offset(
childOffset.dx + 20.0,
childOffset.dy + 20.0,
);
final TestGesture gesture = await tester.startGesture(childOffset, kind: PointerDeviceKind.mouse);
// Attempting to pan doesn't work because it's disabled, but the
// interaction methods are still called.
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.moveTo(childInterior);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(transformationController.value, equals(Matrix4.identity()));
expect(calledStart, isTrue);
expect(calledUpdate, isTrue);
expect(calledEnd, isTrue);
// Attempting to scroll with a mouse to zoom doesn't work because it's
// disabled, but the interaction methods are still called.
calledStart = false;
calledUpdate = false;
calledEnd = false;
await scrollAt(childInterior, tester, const Offset(0.0, -20.0));
await tester.pumpAndSettle();
expect(transformationController.value, equals(Matrix4.identity()));
expect(calledStart, isTrue);
expect(calledUpdate, isTrue);
expect(calledEnd, isTrue);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS, TargetPlatform.linux, TargetPlatform.windows }));
testWidgets('viewport changes size', (WidgetTester tester) async {
final TransformationController transformationController = TransformationController();
await tester.pumpWidget(