Skip to content

Commit e7ab3b0

Browse files
OverlayPortal (#105335)
`OverlayPortal`
1 parent 60de2aa commit e7ab3b0

File tree

10 files changed

+3066
-132
lines changed

10 files changed

+3066
-132
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
// Flutter code sample for OverlayPortal
6+
7+
import 'package:flutter/material.dart';
8+
9+
void main() => runApp(const MyApp());
10+
11+
class MyApp extends StatelessWidget {
12+
const MyApp({super.key});
13+
14+
@override
15+
Widget build(BuildContext context) {
16+
return MaterialApp(
17+
title: 'Flutter Code Sample',
18+
home: Scaffold(
19+
appBar: AppBar(title: const Text('OverlayPortal Example')),
20+
body: const Center(child: ClickableTooltipWidget()),
21+
),
22+
);
23+
}
24+
}
25+
26+
class ClickableTooltipWidget extends StatefulWidget {
27+
const ClickableTooltipWidget({super.key});
28+
29+
@override
30+
State<StatefulWidget> createState() => ClickableTooltipWidgetState();
31+
}
32+
33+
class ClickableTooltipWidgetState extends State<ClickableTooltipWidget> {
34+
final OverlayPortalController _tooltipController = OverlayPortalController();
35+
36+
@override
37+
Widget build(BuildContext context) {
38+
return TextButton(
39+
onPressed: _tooltipController.toggle,
40+
child: DefaultTextStyle(
41+
style: DefaultTextStyle.of(context).style.copyWith(fontSize: 50),
42+
child: OverlayPortal(
43+
controller: _tooltipController,
44+
overlayChildBuilder: (BuildContext context) {
45+
return const Positioned(
46+
right: 50,
47+
bottom: 50,
48+
child: ColoredBox(
49+
color: Colors.amberAccent,
50+
child: Text('tooltip'),
51+
),
52+
);
53+
},
54+
child: const Text('Press to show/hide tooltip'),
55+
),
56+
),
57+
);
58+
}
59+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/rendering.dart';
6+
import 'package:flutter_api_samples/widgets/overlay/overlay_portal.0.dart' as example;
7+
import 'package:flutter_test/flutter_test.dart';
8+
9+
void main() {
10+
const String tooltipText = 'tooltip';
11+
testWidgets('Tooltip is shown on press', (WidgetTester tester) async {
12+
await tester.pumpWidget(const example.MyApp());
13+
expect(find.text(tooltipText), findsNothing);
14+
15+
await tester.tap(find.byType(example.ClickableTooltipWidget));
16+
await tester.pump();
17+
expect(find.text(tooltipText), findsOneWidget);
18+
19+
await tester.tap(find.byType(example.ClickableTooltipWidget));
20+
await tester.pump();
21+
expect(find.text(tooltipText), findsNothing);
22+
});
23+
24+
testWidgets('Tooltip is shown at the right location', (WidgetTester tester) async {
25+
await tester.pumpWidget(const example.MyApp());
26+
await tester.tap(find.byType(example.ClickableTooltipWidget));
27+
await tester.pump();
28+
29+
final Size canvasSize = tester.getSize(find.byType(example.MyApp));
30+
expect(
31+
tester.getBottomRight(find.text(tooltipText)),
32+
canvasSize - const Size(50, 50),
33+
);
34+
});
35+
36+
testWidgets('Tooltip is shown with the right font size', (WidgetTester tester) async {
37+
await tester.pumpWidget(const example.MyApp());
38+
await tester.tap(find.byType(example.ClickableTooltipWidget));
39+
await tester.pump();
40+
41+
final TextSpan textSpan = tester.renderObject<RenderParagraph>(find.text(tooltipText)).text as TextSpan;
42+
expect(textSpan.style?.fontSize, 50);
43+
});
44+
}

packages/flutter/lib/src/material/material.dart

Lines changed: 65 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -641,7 +641,7 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController
641641
}
642642

643643
void _didChangeLayout() {
644-
if (_inkFeatures != null && _inkFeatures!.isNotEmpty) {
644+
if (_inkFeatures?.isNotEmpty ?? false) {
645645
markNeedsPaint();
646646
}
647647
}
@@ -651,16 +651,18 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController
651651

652652
@override
653653
void paint(PaintingContext context, Offset offset) {
654-
if (_inkFeatures != null && _inkFeatures!.isNotEmpty) {
654+
final List<InkFeature>? inkFeatures = _inkFeatures;
655+
if (inkFeatures != null && inkFeatures.isNotEmpty) {
655656
final Canvas canvas = context.canvas;
656657
canvas.save();
657658
canvas.translate(offset.dx, offset.dy);
658659
canvas.clipRect(Offset.zero & size);
659-
for (final InkFeature inkFeature in _inkFeatures!) {
660+
for (final InkFeature inkFeature in inkFeatures) {
660661
inkFeature._paint(canvas);
661662
}
662663
canvas.restore();
663664
}
665+
assert(inkFeatures == _inkFeatures);
664666
super.paint(context, offset);
665667
}
666668
}
@@ -740,32 +742,71 @@ abstract class InkFeature {
740742
onRemoved?.call();
741743
}
742744

745+
// Returns the paint transform that allows `fromRenderObject` to perform paint
746+
// in `toRenderObject`'s coordinate space.
747+
//
748+
// Returns null if either `fromRenderObject` or `toRenderObject` is not in the
749+
// same render tree, or either of them is in an offscreen subtree (see
750+
// RenderObject.paintsChild).
751+
static Matrix4? _getPaintTransform(
752+
RenderObject fromRenderObject,
753+
RenderObject toRenderObject,
754+
) {
755+
// The paths to fromRenderObject and toRenderObject's common ancestor.
756+
final List<RenderObject> fromPath = <RenderObject>[fromRenderObject];
757+
final List<RenderObject> toPath = <RenderObject>[toRenderObject];
758+
759+
RenderObject from = fromRenderObject;
760+
RenderObject to = toRenderObject;
761+
762+
while (!identical(from, to)) {
763+
final int fromDepth = from.depth;
764+
final int toDepth = to.depth;
765+
766+
if (fromDepth >= toDepth) {
767+
final AbstractNode? fromParent = from.parent;
768+
// Return early if the 2 render objects are not in the same render tree,
769+
// or either of them is offscreen and thus won't get painted.
770+
if (fromParent is! RenderObject || !fromParent.paintsChild(from)) {
771+
return null;
772+
}
773+
fromPath.add(fromParent);
774+
from = fromParent;
775+
}
776+
777+
if (fromDepth <= toDepth) {
778+
final AbstractNode? toParent = to.parent;
779+
if (toParent is! RenderObject || !toParent.paintsChild(to)) {
780+
return null;
781+
}
782+
toPath.add(toParent);
783+
to = toParent;
784+
}
785+
}
786+
assert(identical(from, to));
787+
788+
final Matrix4 transform = Matrix4.identity();
789+
final Matrix4 inverseTransform = Matrix4.identity();
790+
791+
for (int index = toPath.length - 1; index > 0; index -= 1) {
792+
toPath[index].applyPaintTransform(toPath[index - 1], transform);
793+
}
794+
for (int index = fromPath.length - 1; index > 0; index -= 1) {
795+
fromPath[index].applyPaintTransform(fromPath[index - 1], inverseTransform);
796+
}
797+
798+
final double det = inverseTransform.invert();
799+
return det != 0 ? (inverseTransform..multiply(transform)) : null;
800+
}
801+
743802
void _paint(Canvas canvas) {
744803
assert(referenceBox.attached);
745804
assert(!_debugDisposed);
746-
// find the chain of renderers from us to the feature's referenceBox
747-
final List<RenderObject> descendants = <RenderObject>[referenceBox];
748-
RenderObject node = referenceBox;
749-
while (node != _controller) {
750-
final RenderObject childNode = node;
751-
node = node.parent! as RenderObject;
752-
if (!node.paintsChild(childNode)) {
753-
// Some node between the reference box and this would skip painting on
754-
// the reference box, so bail out early and avoid unnecessary painting.
755-
// Some cases where this can happen are the reference box being
756-
// offstage, in a fully transparent opacity node, or in a keep alive
757-
// bucket.
758-
return;
759-
}
760-
descendants.add(node);
761-
}
762805
// determine the transform that gets our coordinate system to be like theirs
763-
final Matrix4 transform = Matrix4.identity();
764-
assert(descendants.length >= 2);
765-
for (int index = descendants.length - 1; index > 0; index -= 1) {
766-
descendants[index].applyPaintTransform(descendants[index - 1], transform);
806+
final Matrix4? transform = _getPaintTransform(_controller, referenceBox);
807+
if (transform != null) {
808+
paintFeature(canvas, transform);
767809
}
768-
paintFeature(canvas, transform);
769810
}
770811

771812
/// Override this method to paint the ink feature.

packages/flutter/lib/src/rendering/object.dart

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1486,7 +1486,6 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
14861486
/// in other cases will lead to an inconsistent tree and probably cause crashes.
14871487
@override
14881488
void adoptChild(RenderObject child) {
1489-
assert(_debugCanPerformMutations);
14901489
setupParentData(child);
14911490
markNeedsLayout();
14921491
markNeedsCompositingBitsUpdate();
@@ -1500,7 +1499,6 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
15001499
/// in other cases will lead to an inconsistent tree and probably cause crashes.
15011500
@override
15021501
void dropChild(RenderObject child) {
1503-
assert(_debugCanPerformMutations);
15041502
assert(child.parentData != null);
15051503
child._cleanRelayoutBoundary();
15061504
child.parentData!.detach();
@@ -1643,7 +1641,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
16431641
}
16441642

16451643
if (!activeLayoutRoot._debugMutationsLocked) {
1646-
final AbstractNode? p = activeLayoutRoot.parent;
1644+
final AbstractNode? p = activeLayoutRoot.debugLayoutParent;
16471645
activeLayoutRoot = p is RenderObject ? p : null;
16481646
} else {
16491647
// activeLayoutRoot found.
@@ -1722,6 +1720,29 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
17221720
return result;
17231721
}
17241722

1723+
/// The [RenderObject] that's expected to call [layout] on this [RenderObject]
1724+
/// in its [performLayout] implementation.
1725+
///
1726+
/// This method is used to implement an assert that ensures the render subtree
1727+
/// actively performing layout can not get accidently mutated. It's only
1728+
/// implemented in debug mode and always returns null in release mode.
1729+
///
1730+
/// The default implementation returns [parent] and overriding is rarely
1731+
/// needed. A [RenderObject] subclass that expects its
1732+
/// [RenderObject.performLayout] to be called from a different [RenderObject]
1733+
/// that's not its [parent] should override this property to return the actual
1734+
/// layout parent.
1735+
@protected
1736+
RenderObject? get debugLayoutParent {
1737+
RenderObject? layoutParent;
1738+
assert(() {
1739+
final AbstractNode? parent = this.parent;
1740+
layoutParent = parent is RenderObject? ? parent : null;
1741+
return true;
1742+
}());
1743+
return layoutParent;
1744+
}
1745+
17251746
@override
17261747
PipelineOwner? get owner => super.owner as PipelineOwner?;
17271748

@@ -3636,17 +3657,13 @@ mixin RenderObjectWithChildMixin<ChildType extends RenderObject> on RenderObject
36363657
@override
36373658
void attach(PipelineOwner owner) {
36383659
super.attach(owner);
3639-
if (_child != null) {
3640-
_child!.attach(owner);
3641-
}
3660+
_child?.attach(owner);
36423661
}
36433662

36443663
@override
36453664
void detach() {
36463665
super.detach();
3647-
if (_child != null) {
3648-
_child!.detach();
3649-
}
3666+
_child?.detach();
36503667
}
36513668

36523669
@override

packages/flutter/lib/src/widgets/framework.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4701,6 +4701,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
47014701
performRebuild();
47024702
} finally {
47034703
assert(() {
4704+
owner!._debugElementWasRebuilt(this);
47044705
assert(owner!._debugCurrentBuildTarget == this);
47054706
owner!._debugCurrentBuildTarget = debugPreviousBuildTarget;
47064707
return true;

0 commit comments

Comments
 (0)