diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index d58b99de28..1037cd4a70 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -2566,6 +2566,7 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior { void _updateAnnotations() { bool changed = false; + final bool hadHoverAnnotation = _hoverAnnotation != null; if (_hoverAnnotation != null && attached) { RendererBinding.instance.mouseTracker.detachAnnotation(_hoverAnnotation); changed = true; @@ -2587,6 +2588,10 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior { if (changed) { markNeedsPaint(); } + final bool hasHoverAnnotation = _hoverAnnotation != null; + if (hadHoverAnnotation != hasHoverAnnotation) { + markNeedsCompositingBitsUpdate(); + } } @override @@ -2608,6 +2613,9 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior { super.detach(); } + @override + bool get needsCompositing => _hoverAnnotation != null; + @override void paint(PaintingContext context, Offset offset) { if (_hoverAnnotation != null) { diff --git a/packages/flutter/test/widgets/listener_test.dart b/packages/flutter/test/widgets/listener_test.dart index a4691e3084..b07e49912c 100644 --- a/packages/flutter/test/widgets/listener_test.dart +++ b/packages/flutter/test/widgets/listener_test.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/widgets.dart'; @@ -232,5 +233,120 @@ void main() { expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isFalse); expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isFalse); }); + + testWidgets('works with transform', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/31986. + final Key key = UniqueKey(); + const double scaleFactor = 2.0; + const double localWidth = 150.0; + const double localHeight = 100.0; + final List events = []; + + await tester.pumpWidget( + MaterialApp( + home: Center( + child: Transform.scale( + scale: scaleFactor, + child: Listener( + onPointerEnter: (PointerEnterEvent event) { + events.add(event); + }, + onPointerHover: (PointerHoverEvent event) { + events.add(event); + }, + onPointerExit: (PointerExitEvent event) { + events.add(event); + }, + child: Container( + key: key, + color: Colors.blue, + height: localHeight, + width: localWidth, + child: const Text('Hi'), + ), + ), + ), + ), + ), + ); + + final Offset topLeft = tester.getTopLeft(find.byKey(key)); + final Offset topRight = tester.getTopRight(find.byKey(key)); + final Offset bottomLeft = tester.getBottomLeft(find.byKey(key)); + expect(topRight.dx - topLeft.dx, scaleFactor * localWidth); + expect(bottomLeft.dy - topLeft.dy, scaleFactor * localHeight); + + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.moveTo(topLeft - const Offset(1, 1)); + await tester.pump(); + expect(events, isEmpty); + + await gesture.moveTo(topLeft + const Offset(1, 1)); + await tester.pump(); + expect(events, hasLength(2)); + expect(events.first, isA()); + expect(events.last, isA()); + events.clear(); + + await gesture.moveTo(bottomLeft + const Offset(1, -1)); + await tester.pump(); + expect(events.single, isA()); + expect(events.single.delta, const Offset(0.0, scaleFactor * localHeight - 2)); + events.clear(); + + await gesture.moveTo(bottomLeft + const Offset(1, 1)); + await tester.pump(); + expect(events.single, isA()); + events.clear(); + }); + + testWidgets('needsCompositing updates correctly and is respected', (WidgetTester tester) async { + // Pretend that we have a mouse connected. + final TestGesture gesture = await tester.startGesture(Offset.zero, kind: PointerDeviceKind.mouse); + await gesture.up(); + + await tester.pumpWidget( + Transform.scale( + scale: 2.0, + child: Listener( + onPointerDown: (PointerDownEvent _) { }, + ), + ), + ); + final RenderPointerListener listener = tester.renderObject(find.byType(Listener)); + expect(listener.needsCompositing, isFalse); + // No TransformLayer for `Transform.scale` is added because composting is + // not required and therefore the transform is executed on the canvas + // directly. (One TransformLayer is always present for the root + // transform.) + expect(tester.layers.whereType(), hasLength(1)); + + await tester.pumpWidget( + Transform.scale( + scale: 2.0, + child: Listener( + onPointerDown: (PointerDownEvent _) { }, + onPointerHover: (PointerHoverEvent _) { }, + ), + ), + ); + expect(listener.needsCompositing, isTrue); + // Composting is required, therefore a dedicated TransformLayer for + // `Transform.scale` is added. + expect(tester.layers.whereType(), hasLength(2)); + + await tester.pumpWidget( + Transform.scale( + scale: 2.0, + child: Listener( + onPointerDown: (PointerDownEvent _) { }, + ), + ), + ); + expect(listener.needsCompositing, isFalse); + // TransformLayer for `Transform.scale` is removed again as transform is + // executed directly on the canvas. + expect(tester.layers.whereType(), hasLength(1)); + }); }); }