Use the correct Transform in the WidgetInspector overlay (#59566) (#60990)

This commit is contained in:
David Martos 2020-07-23 03:26:07 +02:00 committed by GitHub
parent 9da74f66ca
commit 6d303af97e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 157 additions and 5 deletions

View file

@ -2293,6 +2293,9 @@ class _WidgetInspectorState extends State<WidgetInspector>
@override
Widget build(BuildContext context) {
// Be careful changing this build method. The _InspectorOverlayLayer
// assumes the root RenderObject for the WidgetInspector will be
// a RenderStack with a _RenderInspectorOverlay as the last child.
return Stack(children: <Widget>[
GestureDetector(
onTap: _handleTap,
@ -2441,15 +2444,16 @@ class _RenderInspectorOverlay extends RenderBox {
context.addLayer(_InspectorOverlayLayer(
overlayRect: Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height),
selection: selection,
rootRenderObject: parent is RenderObject ? parent as RenderObject : null,
));
}
}
@immutable
class _TransformedRect {
_TransformedRect(RenderObject object)
_TransformedRect(RenderObject object, RenderObject ancestor)
: rect = object.semanticBounds,
transform = object.getTransformTo(null);
transform = object.getTransformTo(ancestor);
final Rect rect;
final Matrix4 transform;
@ -2517,6 +2521,7 @@ class _InspectorOverlayLayer extends Layer {
_InspectorOverlayLayer({
@required this.overlayRect,
@required this.selection,
@required this.rootRenderObject,
}) : assert(overlayRect != null),
assert(selection != null) {
bool inDebugMode = false;
@ -2543,6 +2548,10 @@ class _InspectorOverlayLayer extends Layer {
/// (as described at [Layer]).
final Rect overlayRect;
/// Widget inspector root render object. The selection overlay will be painted
/// with transforms relative to this render object.
final RenderObject rootRenderObject;
_InspectorOverlayRenderState _lastState;
/// Picture generated from _lastState.
@ -2557,16 +2566,21 @@ class _InspectorOverlayLayer extends Layer {
return;
final RenderObject selected = selection.current;
if (!_isInInspectorRenderObjectTree(selected))
return;
final List<_TransformedRect> candidates = <_TransformedRect>[];
for (final RenderObject candidate in selection.candidates) {
if (candidate == selected || !candidate.attached)
if (candidate == selected || !candidate.attached
|| !_isInInspectorRenderObjectTree(candidate))
continue;
candidates.add(_TransformedRect(candidate));
candidates.add(_TransformedRect(candidate, rootRenderObject));
}
final _InspectorOverlayRenderState state = _InspectorOverlayRenderState(
overlayRect: overlayRect,
selected: _TransformedRect(selected),
selected: _TransformedRect(selected, rootRenderObject),
tooltip: selection.currentElement.toStringShort(),
textDirection: TextDirection.ltr,
candidates: candidates,
@ -2583,6 +2597,9 @@ class _InspectorOverlayLayer extends Layer {
final ui.PictureRecorder recorder = ui.PictureRecorder();
final Canvas canvas = Canvas(recorder, state.overlayRect);
final Size size = state.overlayRect.size;
// The overlay rect could have an offset if the widget inspector does
// not take all the screen.
canvas.translate(state.overlayRect.left, state.overlayRect.top);
final Paint fillPaint = Paint()
..style = PaintingStyle.fill
@ -2695,6 +2712,24 @@ class _InspectorOverlayLayer extends Layer {
}) {
return false;
}
/// Return whether or not a render object belongs to this inspector widget
/// tree.
/// The inspector selection is static, so if there are multiple inspector
/// overlays in the same app (i.e. an storyboard), a selected or candidate
/// render object may not belong to this tree.
bool _isInInspectorRenderObjectTree(RenderObject child) {
RenderObject current = child.parent as RenderObject;
while (current != null) {
// We found the widget inspector render object.
if (current is RenderStack
&& current.lastChild is _RenderInspectorOverlay) {
return rootRenderObject == current;
}
current = current.parent as RenderObject;
}
return false;
}
}
const double _kScreenEdgeMargin = 10.0;

View file

@ -512,6 +512,123 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
expect(inspectorState.selection.candidates.where((RenderObject object) => object is RenderParagraph).length, equals(2));
});
testWidgets('WidgetInspector with Transform above', (WidgetTester tester) async {
final GlobalKey childKey = GlobalKey();
final GlobalKey repaintBoundaryKey = GlobalKey();
final Matrix4 mainTransform = Matrix4.identity()
..translate(50.0, 30.0)
..scale(0.8, 0.8)
..translate(100.0, 50.0);
await tester.pumpWidget(
RepaintBoundary(
key: repaintBoundaryKey,
child: Container(
color: Colors.grey,
child: Transform(
transform: mainTransform,
child: Directionality(
textDirection: TextDirection.ltr,
child: WidgetInspector(
selectButtonBuilder: null,
child: Container(
color: Colors.white,
child: Center(
child: Container(
key: childKey,
height: 100.0,
width: 50.0,
color: Colors.red,
),
),
),
),
),
),
),
),
);
await tester.tap(find.byKey(childKey));
await tester.pump();
await expectLater(
find.byKey(repaintBoundaryKey),
matchesGoldenFile('inspector.overlay_positioning_with_transform.png'),
);
});
testWidgets('Multiple widget inspectors', (WidgetTester tester) async {
// This test verifies that interacting with different inspectors
// works correctly. This use case may be an app that displays multiple
// apps inside (i.e. a storyboard).
final GlobalKey selectButton1Key = GlobalKey();
final GlobalKey selectButton2Key = GlobalKey();
final GlobalKey inspector1Key = GlobalKey();
final GlobalKey inspector2Key = GlobalKey();
final GlobalKey child1Key = GlobalKey();
final GlobalKey child2Key = GlobalKey();
InspectorSelectButtonBuilder selectButtonBuilder(Key key) {
return (BuildContext context, VoidCallback onPressed) {
return Material(child: RaisedButton(onPressed: onPressed, key: key));
};
}
// State type is private, hence using dynamic.
// The inspector state is static, so it's enough with reading one of them.
dynamic getInspectorState() => inspector1Key.currentState;
String paragraphText(RenderParagraph paragraph) {
final TextSpan textSpan = paragraph.text as TextSpan;
return textSpan.text;
}
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Row(
children: <Widget>[
Flexible(
child: WidgetInspector(
key: inspector1Key,
selectButtonBuilder: selectButtonBuilder(selectButton1Key),
child: Container(
key: child1Key,
child: const Text('Child 1'),
),
),
),
Flexible(
child: WidgetInspector(
key: inspector2Key,
selectButtonBuilder: selectButtonBuilder(selectButton2Key),
child: Container(
key: child2Key,
child: const Text('Child 2'),
),
),
),
],
),
),
);
final InspectorSelection selection = getInspectorState().selection as InspectorSelection;
// The selection is static, so it may be initialized from previous tests.
selection?.clear();
await tester.tap(find.text('Child 1'));
await tester.pump();
expect(paragraphText(selection.current as RenderParagraph), equals('Child 1'));
await tester.tap(find.text('Child 2'));
await tester.pump();
expect(paragraphText(selection.current as RenderParagraph), equals('Child 2'));
});
test('WidgetInspectorService null id', () {
service.disposeAllGroups();
expect(service.toObject(null), isNull);