parent
60de2aa989
commit
e7ab3b07f8
59
examples/api/lib/widgets/overlay/overlay_portal.0.dart
Normal file
59
examples/api/lib/widgets/overlay/overlay_portal.0.dart
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
// 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'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
44
examples/api/test/widgets/overlay/overlay_portal.0_test.dart
Normal file
44
examples/api/test/widgets/overlay/overlay_portal.0_test.dart
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
// 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() {
|
void _didChangeLayout() {
|
||||||
if (_inkFeatures != null && _inkFeatures!.isNotEmpty) {
|
if (_inkFeatures?.isNotEmpty ?? false) {
|
||||||
markNeedsPaint();
|
markNeedsPaint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -651,16 +651,18 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void paint(PaintingContext context, Offset offset) {
|
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;
|
final Canvas canvas = context.canvas;
|
||||||
canvas.save();
|
canvas.save();
|
||||||
canvas.translate(offset.dx, offset.dy);
|
canvas.translate(offset.dx, offset.dy);
|
||||||
canvas.clipRect(Offset.zero & size);
|
canvas.clipRect(Offset.zero & size);
|
||||||
for (final InkFeature inkFeature in _inkFeatures!) {
|
for (final InkFeature inkFeature in inkFeatures) {
|
||||||
inkFeature._paint(canvas);
|
inkFeature._paint(canvas);
|
||||||
}
|
}
|
||||||
canvas.restore();
|
canvas.restore();
|
||||||
}
|
}
|
||||||
|
assert(inkFeatures == _inkFeatures);
|
||||||
super.paint(context, offset);
|
super.paint(context, offset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -740,32 +742,71 @@ abstract class InkFeature {
|
|||||||
onRemoved?.call();
|
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) {
|
void _paint(Canvas canvas) {
|
||||||
assert(referenceBox.attached);
|
assert(referenceBox.attached);
|
||||||
assert(!_debugDisposed);
|
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
|
// determine the transform that gets our coordinate system to be like theirs
|
||||||
final Matrix4 transform = Matrix4.identity();
|
final Matrix4? transform = _getPaintTransform(_controller, referenceBox);
|
||||||
assert(descendants.length >= 2);
|
if (transform != null) {
|
||||||
for (int index = descendants.length - 1; index > 0; index -= 1) {
|
paintFeature(canvas, transform);
|
||||||
descendants[index].applyPaintTransform(descendants[index - 1], transform);
|
|
||||||
}
|
}
|
||||||
paintFeature(canvas, transform);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Override this method to paint the ink feature.
|
/// 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.
|
/// in other cases will lead to an inconsistent tree and probably cause crashes.
|
||||||
@override
|
@override
|
||||||
void adoptChild(RenderObject child) {
|
void adoptChild(RenderObject child) {
|
||||||
assert(_debugCanPerformMutations);
|
|
||||||
setupParentData(child);
|
setupParentData(child);
|
||||||
markNeedsLayout();
|
markNeedsLayout();
|
||||||
markNeedsCompositingBitsUpdate();
|
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.
|
/// in other cases will lead to an inconsistent tree and probably cause crashes.
|
||||||
@override
|
@override
|
||||||
void dropChild(RenderObject child) {
|
void dropChild(RenderObject child) {
|
||||||
assert(_debugCanPerformMutations);
|
|
||||||
assert(child.parentData != null);
|
assert(child.parentData != null);
|
||||||
child._cleanRelayoutBoundary();
|
child._cleanRelayoutBoundary();
|
||||||
child.parentData!.detach();
|
child.parentData!.detach();
|
||||||
@ -1643,7 +1641,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!activeLayoutRoot._debugMutationsLocked) {
|
if (!activeLayoutRoot._debugMutationsLocked) {
|
||||||
final AbstractNode? p = activeLayoutRoot.parent;
|
final AbstractNode? p = activeLayoutRoot.debugLayoutParent;
|
||||||
activeLayoutRoot = p is RenderObject ? p : null;
|
activeLayoutRoot = p is RenderObject ? p : null;
|
||||||
} else {
|
} else {
|
||||||
// activeLayoutRoot found.
|
// activeLayoutRoot found.
|
||||||
@ -1722,6 +1720,29 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
|
|||||||
return result;
|
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
|
@override
|
||||||
PipelineOwner? get owner => super.owner as PipelineOwner?;
|
PipelineOwner? get owner => super.owner as PipelineOwner?;
|
||||||
|
|
||||||
@ -3636,17 +3657,13 @@ mixin RenderObjectWithChildMixin<ChildType extends RenderObject> on RenderObject
|
|||||||
@override
|
@override
|
||||||
void attach(PipelineOwner owner) {
|
void attach(PipelineOwner owner) {
|
||||||
super.attach(owner);
|
super.attach(owner);
|
||||||
if (_child != null) {
|
_child?.attach(owner);
|
||||||
_child!.attach(owner);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void detach() {
|
void detach() {
|
||||||
super.detach();
|
super.detach();
|
||||||
if (_child != null) {
|
_child?.detach();
|
||||||
_child!.detach();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -4701,6 +4701,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
|
|||||||
performRebuild();
|
performRebuild();
|
||||||
} finally {
|
} finally {
|
||||||
assert(() {
|
assert(() {
|
||||||
|
owner!._debugElementWasRebuilt(this);
|
||||||
assert(owner!._debugCurrentBuildTarget == this);
|
assert(owner!._debugCurrentBuildTarget == this);
|
||||||
owner!._debugCurrentBuildTarget = debugPreviousBuildTarget;
|
owner!._debugCurrentBuildTarget = debugPreviousBuildTarget;
|
||||||
return true;
|
return true;
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -152,10 +152,11 @@ void main() {
|
|||||||
' AnimatedBuilder\n'
|
' AnimatedBuilder\n'
|
||||||
' _ModalScope<dynamic>-[LabeledGlobalKey<_ModalScopeState<dynamic>>#00000]\n'
|
' _ModalScope<dynamic>-[LabeledGlobalKey<_ModalScopeState<dynamic>>#00000]\n'
|
||||||
' Semantics\n'
|
' Semantics\n'
|
||||||
|
' _RenderTheaterMarker\n'
|
||||||
' _EffectiveTickerMode\n'
|
' _EffectiveTickerMode\n'
|
||||||
' TickerMode\n'
|
' TickerMode\n'
|
||||||
' _OverlayEntryWidget-[LabeledGlobalKey<_OverlayEntryWidgetState>#00000]\n'
|
' _OverlayEntryWidget-[LabeledGlobalKey<_OverlayEntryWidgetState>#00000]\n'
|
||||||
' _Theatre\n'
|
' _Theater\n'
|
||||||
' Overlay-[LabeledGlobalKey<OverlayState>#00000]\n'
|
' Overlay-[LabeledGlobalKey<OverlayState>#00000]\n'
|
||||||
' UnmanagedRestorationScope\n'
|
' UnmanagedRestorationScope\n'
|
||||||
' _FocusInheritedScope\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 {
|
testWidgets('Custom rectCallback renders an ink splash from its center', (WidgetTester tester) async {
|
||||||
const Color splashColor = Color(0xff00ff00);
|
const Color splashColor = Color(0xff00ff00);
|
||||||
|
|
||||||
|
1587
packages/flutter/test/widgets/overlay_portal_test.dart
Normal file
1587
packages/flutter/test/widgets/overlay_portal_test.dart
Normal file
File diff suppressed because it is too large
Load Diff
@ -41,7 +41,7 @@ void main() {
|
|||||||
expect(
|
expect(
|
||||||
theater.toStringDeep(minLevel: DiagnosticLevel.info),
|
theater.toStringDeep(minLevel: DiagnosticLevel.info),
|
||||||
equalsIgnoringHashCodes(
|
equalsIgnoringHashCodes(
|
||||||
'_RenderTheatre#744c9\n'
|
'_RenderTheater#744c9\n'
|
||||||
' │ parentData: <none>\n'
|
' │ parentData: <none>\n'
|
||||||
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
|
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
|
||||||
' │ size: Size(800.0, 600.0)\n'
|
' │ size: Size(800.0, 600.0)\n'
|
||||||
@ -114,7 +114,7 @@ void main() {
|
|||||||
expect(
|
expect(
|
||||||
theater.toStringDeep(minLevel: DiagnosticLevel.info),
|
theater.toStringDeep(minLevel: DiagnosticLevel.info),
|
||||||
equalsIgnoringHashCodes(
|
equalsIgnoringHashCodes(
|
||||||
'_RenderTheatre#385b3\n'
|
'_RenderTheater#385b3\n'
|
||||||
' │ parentData: <none>\n'
|
' │ parentData: <none>\n'
|
||||||
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
|
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
|
||||||
' │ size: Size(800.0, 600.0)\n'
|
' │ size: Size(800.0, 600.0)\n'
|
||||||
|
Loading…
x
Reference in New Issue
Block a user