Reland: MouseRegion enter/exit event can be triggered with button pressed (#83253)

* Revert "Revert "MouseRegion enter/exit event can be triggered with button pressed (#81148)" (#81557)"
This commit is contained in:
Tong Mu 2021-06-11 20:31:20 -07:00 committed by GitHub
parent e1825c5c4c
commit e708aa64bd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 125 additions and 12 deletions

View file

@ -278,12 +278,14 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
@override // from GestureBinding
void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
if (hitTestResult != null ||
event is PointerAddedEvent ||
event is PointerRemovedEvent) {
assert(event.position != null);
_mouseTracker!.updateWithEvent(event, () => hitTestResult ?? renderView.hitTestMouseTrackers(event.position));
}
_mouseTracker!.updateWithEvent(
event,
// Enter and exit events should be triggered with or without buttons
// pressed. When the button is pressed, normal hit test uses a cached
// result, but MouseTracker requires that the hit test is re-executed to
// update the hovering events.
() => (hitTestResult == null || event is PointerMoveEvent) ? renderView.hitTestMouseTrackers(event.position) : hitTestResult,
);
super.dispatchEvent(event, hitTestResult);
}

View file

@ -289,17 +289,20 @@ class MouseTracker extends ChangeNotifier {
/// Trigger a device update with a new event and its corresponding hit test
/// result.
///
/// The [updateWithEvent] indicates that an event has been observed, and
/// is called during the handler of the event. The `getResult` should return
/// the hit test result at the position of the event.
/// The [updateWithEvent] indicates that an event has been observed, and is
/// called during the handler of the event. It is typically called by
/// [RendererBinding], and should be called with all events received, and let
/// [MouseTracker] filter which to react to.
///
/// The `getResult` is a function to return the hit test result at the
/// position of the event. It should not simply return cached hit test
/// result, because the cache does not change throughout a tap sequence.
void updateWithEvent(PointerEvent event, ValueGetter<HitTestResult> getResult) {
assert(event != null);
final HitTestResult result = event is PointerRemovedEvent ? HitTestResult() : getResult();
assert(result != null);
if (event.kind != PointerDeviceKind.mouse)
return;
if (event is PointerSignalEvent)
return;
final HitTestResult result = event is PointerRemovedEvent ? HitTestResult() : getResult();
final int device = event.device;
final _MouseState? existingState = _mouseStates[device];
if (!_shouldMarkStateDirty(existingState, event))

View file

@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
@ -13,4 +14,75 @@ void main() {
expect(result, hasOneLineDescription);
expect(result.path.first, hasOneLineDescription);
});
testWidgets('A mouse click should only cause one hit test', (WidgetTester tester) async {
int hitCount = 0;
await tester.pumpWidget(
_HitTestCounter(
onHitTestCallback: () { hitCount += 1; },
child: Container(),
),
);
final TestGesture gesture =
await tester.startGesture(tester.getCenter(find.byType(_HitTestCounter)), kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await gesture.up();
expect(hitCount, 1);
});
testWidgets('Non-mouse events should not cause movement hit tests', (WidgetTester tester) async {
int hitCount = 0;
await tester.pumpWidget(
_HitTestCounter(
onHitTestCallback: () { hitCount += 1; },
child: Container(),
),
);
final TestGesture gesture =
await tester.startGesture(tester.getCenter(find.byType(_HitTestCounter)), kind: PointerDeviceKind.touch);
await gesture.moveBy(const Offset(1, 1));
await gesture.up();
expect(hitCount, 1);
});
}
// The [_HitTestCounter] invokes [onHitTestCallback] every time
// [hitTestChildren] is called.
class _HitTestCounter extends SingleChildRenderObjectWidget {
const _HitTestCounter({
Key? key,
required Widget child,
required this.onHitTestCallback,
}) : super(key: key, child: child);
final VoidCallback? onHitTestCallback;
@override
_RenderHitTestCounter createRenderObject(BuildContext context) {
return _RenderHitTestCounter()
.._onHitTestCallback = onHitTestCallback;
}
@override
void updateRenderObject(
BuildContext context,
_RenderHitTestCounter renderObject,
) {
renderObject._onHitTestCallback = onHitTestCallback;
}
}
class _RenderHitTestCounter extends RenderProxyBox {
VoidCallback? _onHitTestCallback;
@override
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
_onHitTestCallback?.call();
return super.hitTestChildren(result, position: position);
}
}

View file

@ -76,6 +76,42 @@ class _HoverFeedbackState extends State<HoverFeedback> {
}
void main() {
testWidgets('onEnter and onExit can be triggered with mouse buttons pressed', (WidgetTester tester) async {
PointerEnterEvent? enter;
PointerExitEvent? exit;
await tester.pumpWidget(Center(
child: MouseRegion(
child: Container(
color: const Color.fromARGB(0xff, 0xff, 0x00, 0x00),
width: 100.0,
height: 100.0,
),
onEnter: (PointerEnterEvent details) => enter = details,
onExit: (PointerExitEvent details) => exit = details,
),
));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, buttons: kPrimaryMouseButton);
await gesture.addPointer(location: Offset.zero);
await gesture.down(Offset.zero); // Press the mouse button.
addTearDown(gesture.removePointer);
await tester.pump();
enter = null;
exit = null;
// Trigger the enter event.
await gesture.moveTo(const Offset(400.0, 300.0));
expect(enter, isNotNull);
expect(enter!.position, equals(const Offset(400.0, 300.0)));
expect(enter!.localPosition, equals(const Offset(50.0, 50.0)));
expect(exit, isNull);
// Trigger the exit event.
await gesture.moveTo(const Offset(1.0, 1.0));
expect(exit, isNotNull);
expect(exit!.position, equals(const Offset(1.0, 1.0)));
expect(exit!.localPosition, equals(const Offset(-349.0, -249.0)));
});
testWidgets('detects pointer enter', (WidgetTester tester) async {
PointerEnterEvent? enter;
PointerHoverEvent? move;