Unverified Commit a45727d8 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Add MediaQuery to View (#118004)

* Add MediaQuery to View

* unify API

* fix test

* add test

* comment

* better doc

* Apply suggestions from code review
Co-authored-by: 's avatarGreg Spencer <gspencergoog@users.noreply.github.com>
Co-authored-by: 's avatarGreg Spencer <gspencergoog@users.noreply.github.com>
parent 3be330aa
......@@ -1730,8 +1730,7 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
child: title,
);
final MediaQueryData? data = MediaQuery.maybeOf(context);
if (!widget.useInheritedMediaQuery || data == null) {
if (!widget.useInheritedMediaQuery || MediaQuery.maybeOf(context) == null) {
child = MediaQuery.fromWindow(
child: child,
);
......
......@@ -6,10 +6,14 @@ import 'dart:ui' show FlutterView;
import 'framework.dart';
import 'lookup_boundary.dart';
import 'media_query.dart';
/// Injects a [FlutterView] into the tree and makes it available to descendants
/// within the same [LookupBoundary] via [View.of] and [View.maybeOf].
///
/// The provided [child] is wrapped in a [MediaQuery] constructed from the given
/// [view].
///
/// In a future version of Flutter, the functionality of this widget will be
/// extended to actually bootstrap the render tree that is going to be rendered
/// into the provided [view]. This will enable rendering content into multiple
......@@ -20,15 +24,26 @@ import 'lookup_boundary.dart';
/// [FlutterView] must never exist within the same widget tree at the same time.
/// Internally, this limitation is enforced by a [GlobalObjectKey] that derives
/// its identity from the [view] provided to this widget.
class View extends InheritedWidget {
class View extends StatelessWidget {
/// Injects the provided [view] into the widget tree.
View({required this.view, required super.child}) : super(key: GlobalObjectKey(view));
View({required this.view, required this.child}) : super(key: GlobalObjectKey(view));
/// The [FlutterView] to be injected into the tree.
final FlutterView view;
/// {@macro flutter.widgets.ProxyWidget.child}
final Widget child;
@override
bool updateShouldNotify(View oldWidget) => view != oldWidget.view;
Widget build(BuildContext context) {
return _ViewScope(
view: view,
child: MediaQuery.fromView(
view: view,
child: child,
),
);
}
/// Returns the [FlutterView] that the provided `context` will render into.
///
......@@ -47,7 +62,7 @@ class View extends InheritedWidget {
/// * [View.of], which throws instead of returning null if no [FlutterView]
/// is found.
static FlutterView? maybeOf(BuildContext context) {
return LookupBoundary.dependOnInheritedWidgetOfExactType<View>(context)?.view;
return LookupBoundary.dependOnInheritedWidgetOfExactType<_ViewScope>(context)?.view;
}
/// Returns the [FlutterView] that the provided `context` will render into.
......@@ -70,7 +85,7 @@ class View extends InheritedWidget {
final FlutterView? result = maybeOf(context);
assert(() {
if (result == null) {
final bool hiddenByBoundary = LookupBoundary.debugIsHidingAncestorWidgetOfExactType<View>(context);
final bool hiddenByBoundary = LookupBoundary.debugIsHidingAncestorWidgetOfExactType<_ViewScope>(context);
final List<DiagnosticsNode> information = <DiagnosticsNode>[
if (hiddenByBoundary) ...<DiagnosticsNode>[
ErrorSummary('View.of() was called with a context that does not have access to a View widget.'),
......@@ -92,3 +107,12 @@ class View extends InheritedWidget {
return result!;
}
}
class _ViewScope extends InheritedWidget {
const _ViewScope({required this.view, required super.child});
final FlutterView view;
@override
bool updateShouldNotify(_ViewScope oldWidget) => view != oldWidget.view;
}
......@@ -193,7 +193,7 @@ void main() {
' Semantics\n'
' Localizations\n'
' MediaQuery\n'
' _MediaQueryFromWindow\n'
' _MediaQueryFromView\n'
' Semantics\n'
' _FocusInheritedScope\n'
' Focus\n'
......@@ -235,6 +235,9 @@ void main() {
' HeroControllerScope\n'
' ScrollConfiguration\n'
' MaterialApp\n'
' MediaQuery\n'
' _MediaQueryFromView\n'
' _ViewScope\n'
' View-[GlobalObjectKey TestWindow#00000]\n'
' [root]\n'
' Typically, the Scaffold widget is introduced by the MaterialApp\n'
......@@ -251,18 +254,15 @@ void main() {
);
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: ScaffoldMessenger(
key: scaffoldMessengerKey,
child: Builder(
builder: (BuildContext context) {
return Scaffold(
key: scaffoldKey,
body: Container(),
);
},
),
child: ScaffoldMessenger(
key: scaffoldMessengerKey,
child: Builder(
builder: (BuildContext context) {
return Scaffold(
key: scaffoldKey,
body: Container(),
);
},
),
),
));
......@@ -278,12 +278,9 @@ void main() {
// Pump widget to rebuild without ScaffoldMessenger
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: Scaffold(
key: scaffoldKey,
body: Container(),
),
child: Scaffold(
key: scaffoldKey,
body: Container(),
),
));
// Tap SnackBarAction to dismiss.
......@@ -378,8 +375,10 @@ void main() {
' ScrollNotificationObserver\n'
' _ScaffoldScope\n'
' Scaffold-[LabeledGlobalKey<ScaffoldState>#00000]\n'
' MediaQuery\n'
' Directionality\n'
' MediaQuery\n'
' _MediaQueryFromView\n'
' _ViewScope\n'
' View-[GlobalObjectKey TestWindow#00000]\n'
' [root]\n'
' Typically, the ScaffoldMessenger widget is introduced by the\n'
......
......@@ -136,12 +136,6 @@ void main() {
),
);
}
await tester.pumpWidget(boilerplate(Scaffold(
appBar: AppBar(title: const Text('Title')),
body: Container(key: bodyKey),
),
));
expect(tester.takeException(), isFlutterError);
await tester.pumpWidget(MaterialApp(
home: Scaffold(
......@@ -2391,24 +2385,21 @@ void main() {
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: Scaffold(
body: Builder(
builder: (BuildContext context) {
return GestureDetector(
key: tapTarget,
onTap: () {
ScaffoldMessenger.of(context);
},
behavior: HitTestBehavior.opaque,
child: const SizedBox(
height: 100.0,
width: 100.0,
),
);
},
),
child: Scaffold(
body: Builder(
builder: (BuildContext context) {
return GestureDetector(
key: tapTarget,
onTap: () {
ScaffoldMessenger.of(context);
},
behavior: HitTestBehavior.opaque,
child: const SizedBox(
height: 100.0,
width: 100.0,
),
);
},
),
),
));
......@@ -2459,8 +2450,10 @@ void main() {
' ScrollNotificationObserver\n'
' _ScaffoldScope\n'
' Scaffold\n'
' MediaQuery\n'
' Directionality\n'
' MediaQuery\n'
' _MediaQueryFromView\n'
' _ViewScope\n'
' View-[GlobalObjectKey TestWindow#e6136]\n'
' [root]\n'
' Typically, the ScaffoldMessenger widget is introduced by the\n'
......
......@@ -77,7 +77,7 @@ void main() {
' │ decoration: BoxDecoration:\n'
' │ color: Color(0xff0000ff)\n'
' │ configuration: ImageConfiguration(bundle:\n'
' │ PlatformAssetBundle#00000(), devicePixelRatio: 1.0, platform:\n'
' │ PlatformAssetBundle#00000(), devicePixelRatio: 3.0, platform:\n'
' │ android)\n'
' │\n'
' └─child: RenderLimitedBox#00000\n'
......
......@@ -644,6 +644,32 @@ void main() {
expect(MediaQuery.of(capturedContext), isNotNull);
});
testWidgets("WidgetsApp doesn't have dependency on MediaQuery", (WidgetTester tester) async {
int routeBuildCount = 0;
final Widget widget = WidgetsApp(
color: const Color.fromARGB(255, 255, 255, 255),
onGenerateRoute: (_) {
return PageRouteBuilder<void>(pageBuilder: (_, __, ___) {
routeBuildCount++;
return const Placeholder();
});
},
);
await tester.pumpWidget(
MediaQuery(data: const MediaQueryData(textScaleFactor: 10), child: widget),
);
expect(routeBuildCount, equals(1));
await tester.pumpWidget(
MediaQuery(data: const MediaQueryData(textScaleFactor: 20), child: widget),
);
expect(routeBuildCount, equals(1));
});
testWidgets('WidgetsApp provides meta based shortcuts for iOS and macOS', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
final SelectAllSpy selectAllSpy = SelectAllSpy();
......
......@@ -372,7 +372,8 @@ void main() {
' in its parent data.\n'
' The following child has no ID: RenderConstrainedBox#00000 NEEDS-LAYOUT NEEDS-PAINT:\n'
' creator: ConstrainedBox ← Container ← LayoutWithMissingId ←\n'
' CustomMultiChildLayout ← Center ← View-[GlobalObjectKey\n'
' CustomMultiChildLayout ← Center ← MediaQuery ←\n'
' _MediaQueryFromView ← _ViewScope ← View-[GlobalObjectKey\n'
' TestWindow#00000] ← [root]\n'
' parentData: offset=Offset(0.0, 0.0); id=null\n'
' constraints: MISSING\n'
......
......@@ -96,8 +96,11 @@ void main() {
});
testWidgets('debugCheckHasMediaQuery control test', (WidgetTester tester) async {
await tester.pumpWidget(
Builder(
// Cannot use tester.pumpWidget here because it wraps the widget in a View,
// which introduces a MediaQuery ancestor.
await pumpWidgetWithoutViewWrapper(
tester: tester,
widget: Builder(
builder: (BuildContext context) {
late FlutterError error;
try {
......@@ -339,3 +342,9 @@ void main() {
expect(renderObject.debugLayer?.debugCreator, isNotNull);
});
}
Future<void> pumpWidgetWithoutViewWrapper({required WidgetTester tester, required Widget widget}) {
tester.binding.attachRootWidget(widget);
tester.binding.scheduleFrame();
return tester.binding.pump();
}
......@@ -1228,8 +1228,9 @@ void main() {
equalsIgnoringHashCodes(
'FocusManager#00000\n'
' │ primaryFocus: FocusNode#00000(Child 4 [PRIMARY FOCUS])\n'
' │ primaryFocusCreator: Container-[GlobalKey#00000] ←\n'
' │ View-[GlobalObjectKey TestWindow#00000] ← [root]\n'
' │ primaryFocusCreator: Container-[GlobalKey#00000] ← MediaQuery ←\n'
' │ _MediaQueryFromView ← _ViewScope ← View-[GlobalObjectKey\n'
' │ TestWindow#00000] ← [root]\n'
' │\n'
' └─rootScope: FocusScopeNode#00000(Root Focus Scope [IN FOCUS PATH])\n'
' │ IN FOCUS PATH\n'
......
......@@ -30,7 +30,7 @@ class TestWidgetState extends State<TestWidget> {
void main() {
testWidgets('initState() is called when we are in the tree', (WidgetTester tester) async {
await tester.pumpWidget(const Parent(child: TestWidget()));
expect(ancestors, equals(<String>['Parent', 'View', 'RenderObjectToWidgetAdapter<RenderBox>']));
expect(ancestors, containsAllInOrder(<String>['Parent', 'View', 'RenderObjectToWidgetAdapter<RenderBox>']));
});
}
......
......@@ -326,7 +326,7 @@ void main() {
expect(
list.toStringDeep(minLevel: DiagnosticLevel.info),
equalsIgnoringHashCodes(
'RenderSliverList#00000 relayoutBoundary=up1\n'
'RenderSliverList#00000 relayoutBoundary=up2\n'
' │ needs compositing\n'
' │ parentData: paintOffset=Offset(0.0, 0.0) (can use size)\n'
' │ constraints: SliverConstraints(AxisDirection.down,\n'
......@@ -339,7 +339,7 @@ void main() {
' │ maxPaintExtent: 300.0, cacheExtent: 300.0)\n'
' │ currently live children: 0 to 2\n'
' │\n'
' ├─child with index 0: RenderRepaintBoundary#00000 relayoutBoundary=up2\n'
' ├─child with index 0: RenderRepaintBoundary#00000 relayoutBoundary=up3\n'
' │ │ needs compositing\n'
' │ │ parentData: index=0; layoutOffset=0.0 (can use size)\n'
' │ │ constraints: BoxConstraints(w=800.0, 0.0<=h<=Infinity)\n'
......@@ -349,7 +349,7 @@ void main() {
' │ │ diagnosis: insufficient data to draw conclusion (less than five\n'
' │ │ repaints)\n'
' │ │\n'
' │ └─child: RenderConstrainedBox#00000 relayoutBoundary=up3\n'
' │ └─child: RenderConstrainedBox#00000 relayoutBoundary=up4\n'
' │ │ parentData: <none> (can use size)\n'
' │ │ constraints: BoxConstraints(w=800.0, 0.0<=h<=Infinity)\n'
' │ │ size: Size(800.0, 100.0)\n'
......@@ -368,7 +368,7 @@ void main() {
' │ size: Size(800.0, 100.0)\n'
' │ additionalConstraints: BoxConstraints(biggest)\n'
' │\n'
' ├─child with index 1: RenderRepaintBoundary#00000 relayoutBoundary=up2\n'
' ├─child with index 1: RenderRepaintBoundary#00000 relayoutBoundary=up3\n'
' │ │ needs compositing\n'
' │ │ parentData: index=1; layoutOffset=100.0 (can use size)\n'
' │ │ constraints: BoxConstraints(w=800.0, 0.0<=h<=Infinity)\n'
......@@ -378,7 +378,7 @@ void main() {
' │ │ diagnosis: insufficient data to draw conclusion (less than five\n'
' │ │ repaints)\n'
' │ │\n'
' │ └─child: RenderConstrainedBox#00000 relayoutBoundary=up3\n'
' │ └─child: RenderConstrainedBox#00000 relayoutBoundary=up4\n'
' │ │ parentData: <none> (can use size)\n'
' │ │ constraints: BoxConstraints(w=800.0, 0.0<=h<=Infinity)\n'
' │ │ size: Size(800.0, 100.0)\n'
......@@ -397,7 +397,7 @@ void main() {
' │ size: Size(800.0, 100.0)\n'
' │ additionalConstraints: BoxConstraints(biggest)\n'
' │\n'
' └─child with index 2: RenderRepaintBoundary#00000 relayoutBoundary=up2\n'
' └─child with index 2: RenderRepaintBoundary#00000 relayoutBoundary=up3\n'
' │ needs compositing\n'
' │ parentData: index=2; layoutOffset=200.0 (can use size)\n'
' │ constraints: BoxConstraints(w=800.0, 0.0<=h<=Infinity)\n'
......@@ -407,7 +407,7 @@ void main() {
' │ diagnosis: insufficient data to draw conclusion (less than five\n'
' │ repaints)\n'
' │\n'
' └─child: RenderConstrainedBox#00000 relayoutBoundary=up3\n'
' └─child: RenderConstrainedBox#00000 relayoutBoundary=up4\n'
' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=800.0, 0.0<=h<=Infinity)\n'
' │ size: Size(800.0, 100.0)\n'
......
......@@ -2707,14 +2707,6 @@ void main() {
semantics.dispose();
});
testWidgets('SelectableText throws when not descended from a MediaQuery widget', (WidgetTester tester) async {
const Widget selectableText = SelectableText('something');
await tester.pumpWidget(selectableText);
final dynamic exception = tester.takeException();
expect(exception, isFlutterError);
expect(exception.toString(), startsWith('No MediaQuery widget ancestor found.\nSelectableText widgets require a MediaQuery widget ancestor.'));
});
testWidgets('onTap is called upon tap', (WidgetTester tester) async {
int tapCount = 0;
await tester.pumpWidget(
......
......@@ -221,14 +221,16 @@ void main() {
tester.renderObject(find.byType(_Diagonal)).toStringDeep(),
equalsIgnoringHashCodes(
'_RenderDiagonal#00000 relayoutBoundary=up1\n'
' │ creator: _Diagonal ← Align ← Directionality ←\n'
' │ View-[GlobalObjectKey TestWindow#00000] ← [root]\n'
' │ creator: _Diagonal ← Align ← Directionality ← MediaQuery ←\n'
' │ _MediaQueryFromView ← _ViewScope ← View-[GlobalObjectKey\n'
' │ TestWindow#00000] ← [root]\n'
' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n'
' │ constraints: BoxConstraints(0.0<=w<=800.0, 0.0<=h<=600.0)\n'
' │ size: Size(190.0, 220.0)\n'
' │\n'
' ├─topLeft: RenderConstrainedBox#00000 relayoutBoundary=up2\n'
' │ creator: SizedBox ← _Diagonal ← Align ← Directionality ←\n'
' │ MediaQuery ← _MediaQueryFromView ← _ViewScope ←\n'
' │ View-[GlobalObjectKey TestWindow#00000] ← [root]\n'
' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n'
' │ constraints: BoxConstraints(unconstrained)\n'
......@@ -237,6 +239,7 @@ void main() {
' │\n'
' └─bottomRight: RenderConstrainedBox#00000 relayoutBoundary=up2\n'
' creator: SizedBox ← _Diagonal ← Align ← Directionality ←\n'
' MediaQuery ← _MediaQueryFromView ← _ViewScope ←\n'
' View-[GlobalObjectKey TestWindow#00000] ← [root]\n'
' parentData: offset=Offset(80.0, 100.0) (can use size)\n'
' constraints: BoxConstraints(unconstrained)\n'
......
......@@ -879,24 +879,24 @@ void main() {
expect(
element.toStringDeep(minLevel: DiagnosticLevel.info),
equalsIgnoringHashCodes(
'Table-[GlobalKey#00000](dependencies: [Directionality], renderObject: RenderTable#00000)\n'
'├Text("A")\n'
'Table-[GlobalKey#00000](dependencies: [Directionality, MediaQuery], renderObject: RenderTable#00000)\n'
'├Text("A", dependencies: [MediaQuery])\n'
'│└RichText(softWrap: wrapping at box width, maxLines: unlimited, text: "A", dependencies: [Directionality], renderObject: RenderParagraph#00000 relayoutBoundary=up1)\n'
'├Text("B")\n'
'├Text("B", dependencies: [MediaQuery])\n'
'│└RichText(softWrap: wrapping at box width, maxLines: unlimited, text: "B", dependencies: [Directionality], renderObject: RenderParagraph#00000 relayoutBoundary=up1)\n'
'├Text("C")\n'
'├Text("C", dependencies: [MediaQuery])\n'
'│└RichText(softWrap: wrapping at box width, maxLines: unlimited, text: "C", dependencies: [Directionality], renderObject: RenderParagraph#00000 relayoutBoundary=up1)\n'
'├Text("D")\n'
'├Text("D", dependencies: [MediaQuery])\n'
'│└RichText(softWrap: wrapping at box width, maxLines: unlimited, text: "D", dependencies: [Directionality], renderObject: RenderParagraph#00000 relayoutBoundary=up1)\n'
'├Text("EEE")\n'
'├Text("EEE", dependencies: [MediaQuery])\n'
'│└RichText(softWrap: wrapping at box width, maxLines: unlimited, text: "EEE", dependencies: [Directionality], renderObject: RenderParagraph#00000 relayoutBoundary=up1)\n'
'├Text("F")\n'
'├Text("F", dependencies: [MediaQuery])\n'
'│└RichText(softWrap: wrapping at box width, maxLines: unlimited, text: "F", dependencies: [Directionality], renderObject: RenderParagraph#00000 relayoutBoundary=up1)\n'
'├Text("G")\n'
'├Text("G", dependencies: [MediaQuery])\n'
'│└RichText(softWrap: wrapping at box width, maxLines: unlimited, text: "G", dependencies: [Directionality], renderObject: RenderParagraph#00000 relayoutBoundary=up1)\n'
'├Text("H")\n'
'├Text("H", dependencies: [MediaQuery])\n'
'│└RichText(softWrap: wrapping at box width, maxLines: unlimited, text: "H", dependencies: [Directionality], renderObject: RenderParagraph#00000 relayoutBoundary=up1)\n'
'└Text("III")\n'
'└Text("III", dependencies: [MediaQuery])\n'
' └RichText(softWrap: wrapping at box width, maxLines: unlimited, text: "III", dependencies: [Directionality], renderObject: RenderParagraph#00000 relayoutBoundary=up1)\n',
),
);
......
......@@ -99,7 +99,7 @@ void main() {
final String? message = failure.message;
expect(message, contains('Expected: no matching nodes in the widget tree\n'));
expect(message, contains('Actual: _TextFinder:<exactly one widget with text "foo": Text("foo", textDirection: ltr)>\n'));
expect(message, contains('Actual: _TextFinder:<exactly one widget with text "foo": Text("foo", textDirection: ltr, dependencies: [MediaQuery])>\n'));
expect(message, contains('Which: means one was found but none were expected\n'));
});
......@@ -117,7 +117,7 @@ void main() {
final String? message = failure.message;
expect(message, contains('Expected: no matching nodes in the widget tree\n'));
expect(message, contains('Actual: _TextFinder:<exactly one widget with text "foo" (ignoring offstage widgets): Text("foo", textDirection: ltr)>\n'));
expect(message, contains('Actual: _TextFinder:<exactly one widget with text "foo" (ignoring offstage widgets): Text("foo", textDirection: ltr, dependencies: [MediaQuery])>\n'));
expect(message, contains('Which: means one was found but none were expected\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