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() {
|
||||
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,32 +742,71 @@ 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);
|
||||
}
|
||||
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;
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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);
|
||||
|
||||
|
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(
|
||||
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'
|
||||
|
Loading…
x
Reference in New Issue
Block a user