Unverified Commit e7ab3b07 authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

`OverlayPortal` (#105335)

`OverlayPortal` 
parent 60de2aa9
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flutter code sample for OverlayPortal
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Code Sample',
home: Scaffold(
appBar: AppBar(title: const Text('OverlayPortal Example')),
body: const Center(child: ClickableTooltipWidget()),
),
);
}
}
class ClickableTooltipWidget extends StatefulWidget {
const ClickableTooltipWidget({super.key});
@override
State<StatefulWidget> createState() => ClickableTooltipWidgetState();
}
class ClickableTooltipWidgetState extends State<ClickableTooltipWidget> {
final OverlayPortalController _tooltipController = OverlayPortalController();
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: _tooltipController.toggle,
child: DefaultTextStyle(
style: DefaultTextStyle.of(context).style.copyWith(fontSize: 50),
child: OverlayPortal(
controller: _tooltipController,
overlayChildBuilder: (BuildContext context) {
return const Positioned(
right: 50,
bottom: 50,
child: ColoredBox(
color: Colors.amberAccent,
child: Text('tooltip'),
),
);
},
child: const Text('Press to show/hide tooltip'),
),
),
);
}
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/rendering.dart';
import 'package:flutter_api_samples/widgets/overlay/overlay_portal.0.dart' as example;
import 'package:flutter_test/flutter_test.dart';
void main() {
const String tooltipText = 'tooltip';
testWidgets('Tooltip is shown on press', (WidgetTester tester) async {
await tester.pumpWidget(const example.MyApp());
expect(find.text(tooltipText), findsNothing);
await tester.tap(find.byType(example.ClickableTooltipWidget));
await tester.pump();
expect(find.text(tooltipText), findsOneWidget);
await tester.tap(find.byType(example.ClickableTooltipWidget));
await tester.pump();
expect(find.text(tooltipText), findsNothing);
});
testWidgets('Tooltip is shown at the right location', (WidgetTester tester) async {
await tester.pumpWidget(const example.MyApp());
await tester.tap(find.byType(example.ClickableTooltipWidget));
await tester.pump();
final Size canvasSize = tester.getSize(find.byType(example.MyApp));
expect(
tester.getBottomRight(find.text(tooltipText)),
canvasSize - const Size(50, 50),
);
});
testWidgets('Tooltip is shown with the right font size', (WidgetTester tester) async {
await tester.pumpWidget(const example.MyApp());
await tester.tap(find.byType(example.ClickableTooltipWidget));
await tester.pump();
final TextSpan textSpan = tester.renderObject<RenderParagraph>(find.text(tooltipText)).text as TextSpan;
expect(textSpan.style?.fontSize, 50);
});
}
......@@ -641,7 +641,7 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController
}
void _didChangeLayout() {
if (_inkFeatures != null && _inkFeatures!.isNotEmpty) {
if (_inkFeatures?.isNotEmpty ?? false) {
markNeedsPaint();
}
}
......@@ -651,16 +651,18 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController
@override
void paint(PaintingContext context, Offset offset) {
if (_inkFeatures != null && _inkFeatures!.isNotEmpty) {
final List<InkFeature>? inkFeatures = _inkFeatures;
if (inkFeatures != null && inkFeatures.isNotEmpty) {
final Canvas canvas = context.canvas;
canvas.save();
canvas.translate(offset.dx, offset.dy);
canvas.clipRect(Offset.zero & size);
for (final InkFeature inkFeature in _inkFeatures!) {
for (final InkFeature inkFeature in inkFeatures) {
inkFeature._paint(canvas);
}
canvas.restore();
}
assert(inkFeatures == _inkFeatures);
super.paint(context, offset);
}
}
......@@ -740,33 +742,72 @@ abstract class InkFeature {
onRemoved?.call();
}
// Returns the paint transform that allows `fromRenderObject` to perform paint
// in `toRenderObject`'s coordinate space.
//
// Returns null if either `fromRenderObject` or `toRenderObject` is not in the
// same render tree, or either of them is in an offscreen subtree (see
// RenderObject.paintsChild).
static Matrix4? _getPaintTransform(
RenderObject fromRenderObject,
RenderObject toRenderObject,
) {
// The paths to fromRenderObject and toRenderObject's common ancestor.
final List<RenderObject> fromPath = <RenderObject>[fromRenderObject];
final List<RenderObject> toPath = <RenderObject>[toRenderObject];
RenderObject from = fromRenderObject;
RenderObject to = toRenderObject;
while (!identical(from, to)) {
final int fromDepth = from.depth;
final int toDepth = to.depth;
if (fromDepth >= toDepth) {
final AbstractNode? fromParent = from.parent;
// Return early if the 2 render objects are not in the same render tree,
// or either of them is offscreen and thus won't get painted.
if (fromParent is! RenderObject || !fromParent.paintsChild(from)) {
return null;
}
fromPath.add(fromParent);
from = fromParent;
}
if (fromDepth <= toDepth) {
final AbstractNode? toParent = to.parent;
if (toParent is! RenderObject || !toParent.paintsChild(to)) {
return null;
}
toPath.add(toParent);
to = toParent;
}
}
assert(identical(from, to));
final Matrix4 transform = Matrix4.identity();
final Matrix4 inverseTransform = Matrix4.identity();
for (int index = toPath.length - 1; index > 0; index -= 1) {
toPath[index].applyPaintTransform(toPath[index - 1], transform);
}
for (int index = fromPath.length - 1; index > 0; index -= 1) {
fromPath[index].applyPaintTransform(fromPath[index - 1], inverseTransform);
}
final double det = inverseTransform.invert();
return det != 0 ? (inverseTransform..multiply(transform)) : null;
}
void _paint(Canvas canvas) {
assert(referenceBox.attached);
assert(!_debugDisposed);
// find the chain of renderers from us to the feature's referenceBox
final List<RenderObject> descendants = <RenderObject>[referenceBox];
RenderObject node = referenceBox;
while (node != _controller) {
final RenderObject childNode = node;
node = node.parent! as RenderObject;
if (!node.paintsChild(childNode)) {
// Some node between the reference box and this would skip painting on
// the reference box, so bail out early and avoid unnecessary painting.
// Some cases where this can happen are the reference box being
// offstage, in a fully transparent opacity node, or in a keep alive
// bucket.
return;
}
descendants.add(node);
}
// determine the transform that gets our coordinate system to be like theirs
final Matrix4 transform = Matrix4.identity();
assert(descendants.length >= 2);
for (int index = descendants.length - 1; index > 0; index -= 1) {
descendants[index].applyPaintTransform(descendants[index - 1], transform);
}
final Matrix4? transform = _getPaintTransform(_controller, referenceBox);
if (transform != null) {
paintFeature(canvas, transform);
}
}
/// Override this method to paint the ink feature.
///
......
......@@ -1486,7 +1486,6 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
/// in other cases will lead to an inconsistent tree and probably cause crashes.
@override
void adoptChild(RenderObject child) {
assert(_debugCanPerformMutations);
setupParentData(child);
markNeedsLayout();
markNeedsCompositingBitsUpdate();
......@@ -1500,7 +1499,6 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
/// in other cases will lead to an inconsistent tree and probably cause crashes.
@override
void dropChild(RenderObject child) {
assert(_debugCanPerformMutations);
assert(child.parentData != null);
child._cleanRelayoutBoundary();
child.parentData!.detach();
......@@ -1643,7 +1641,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
}
if (!activeLayoutRoot._debugMutationsLocked) {
final AbstractNode? p = activeLayoutRoot.parent;
final AbstractNode? p = activeLayoutRoot.debugLayoutParent;
activeLayoutRoot = p is RenderObject ? p : null;
} else {
// activeLayoutRoot found.
......@@ -1722,6 +1720,29 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
return result;
}
/// The [RenderObject] that's expected to call [layout] on this [RenderObject]
/// in its [performLayout] implementation.
///
/// This method is used to implement an assert that ensures the render subtree
/// actively performing layout can not get accidently mutated. It's only
/// implemented in debug mode and always returns null in release mode.
///
/// The default implementation returns [parent] and overriding is rarely
/// needed. A [RenderObject] subclass that expects its
/// [RenderObject.performLayout] to be called from a different [RenderObject]
/// that's not its [parent] should override this property to return the actual
/// layout parent.
@protected
RenderObject? get debugLayoutParent {
RenderObject? layoutParent;
assert(() {
final AbstractNode? parent = this.parent;
layoutParent = parent is RenderObject? ? parent : null;
return true;
}());
return layoutParent;
}
@override
PipelineOwner? get owner => super.owner as PipelineOwner?;
......@@ -3636,17 +3657,13 @@ mixin RenderObjectWithChildMixin<ChildType extends RenderObject> on RenderObject
@override
void attach(PipelineOwner owner) {
super.attach(owner);
if (_child != null) {
_child!.attach(owner);
}
_child?.attach(owner);
}
@override
void detach() {
super.detach();
if (_child != null) {
_child!.detach();
}
_child?.detach();
}
@override
......
......@@ -4701,6 +4701,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
performRebuild();
} finally {
assert(() {
owner!._debugElementWasRebuilt(this);
assert(owner!._debugCurrentBuildTarget == this);
owner!._debugCurrentBuildTarget = debugPreviousBuildTarget;
return true;
......
......@@ -152,10 +152,11 @@ void main() {
' AnimatedBuilder\n'
' _ModalScope<dynamic>-[LabeledGlobalKey<_ModalScopeState<dynamic>>#00000]\n'
' Semantics\n'
' _RenderTheaterMarker\n'
' _EffectiveTickerMode\n'
' TickerMode\n'
' _OverlayEntryWidget-[LabeledGlobalKey<_OverlayEntryWidgetState>#00000]\n'
' _Theatre\n'
' _Theater\n'
' Overlay-[LabeledGlobalKey<OverlayState>#00000]\n'
' UnmanagedRestorationScope\n'
' _FocusInheritedScope\n'
......
......@@ -454,6 +454,66 @@ void main() {
}));
});
testWidgets('The InkWell widget on OverlayPortal does not throw', (WidgetTester tester) async {
final OverlayPortalController controller = OverlayPortalController();
controller.show();
await tester.pumpWidget(
Center(
child: RepaintBoundary(
child: SizedBox.square(
dimension: 200,
child: Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return Center(
child: SizedBox.square(
dimension: 100,
// The material partially overlaps the overlayChild.
// This is to verify that the `overlayChild`'s ink
// features aren't clipped by it.
child: Material(
color: Colors.black,
child: OverlayPortal(
controller: controller,
overlayChildBuilder: (BuildContext context) {
return Positioned(
right: 0,
bottom: 0,
child: InkWell(
splashColor: Colors.red,
onTap: () {},
child: const SizedBox.square(dimension: 100),
),
);
},
),
),
),
);
},
),
],
),
),
),
),
),
);
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(InkWell)));
addTearDown(() async {
await gesture.up();
});
await tester.pump(); // start gesture
await tester.pump(const Duration(seconds: 2));
expect(tester.takeException(), isNull);
});
testWidgets('Custom rectCallback renders an ink splash from its center', (WidgetTester tester) async {
const Color splashColor = Color(0xff00ff00);
......
This diff is collapsed.
......@@ -41,7 +41,7 @@ void main() {
expect(
theater.toStringDeep(minLevel: DiagnosticLevel.info),
equalsIgnoringHashCodes(
'_RenderTheatre#744c9\n'
'_RenderTheater#744c9\n'
' │ parentData: <none>\n'
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' │ size: Size(800.0, 600.0)\n'
......@@ -114,7 +114,7 @@ void main() {
expect(
theater.toStringDeep(minLevel: DiagnosticLevel.info),
equalsIgnoringHashCodes(
'_RenderTheatre#385b3\n'
'_RenderTheater#385b3\n'
' │ parentData: <none>\n'
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' │ size: Size(800.0, 600.0)\n'
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment