From 4534a24c09c5919182fc080fe80d48db25c0b56a Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Tue, 9 Jan 2024 14:10:43 -0800 Subject: [PATCH] Reapply "Dynamic view sizing" (#140165) (#140918) This reverts commit https://github.com/flutter/flutter/commit/d24c01bd0c41331bd17165e0173b24c5d05d7c0a. The original change was reverted because it caused some apps to get stuck on the splash screen on some phones. An investigation determined that this was due to a rounding error. Example: The device reports a physical size of 1008.0 x 2198.0 with a dpr of 1.912500023841858. Flutter would translate that to a logical size of 527.0588169589221 x 1149.2810314243163 and use that as the input for its layout algorithm. Since the constraints here are tight, the layout algorithm would determine that the resulting logical size of the root render object must be 527.0588169589221 x 1149.2810314243163. Translating this back to physical pixels by applying the dpr resulted in a physical size of 1007.9999999999999 x 2198.0 for the frame. Android now rejected that frame because it didn't match the expected size of 1008.0 x 2198.0 and since no frame had been rendered would never take down the splash screen. Prior to dynamically sized views, this wasn't an issue because we would hard-code the frame size to whatever the requested size was. Changes in this PR over the original PR: * The issue has been fixed now by constraining the calculated physical size to the input physical constraints which makes sure that we always end up with a size that is acceptable to the operating system. * The `ViewConfiguration` was refactored to use the slightly more convenient `BoxConstraints` over the `ViewConstraints` to represent constraints. Both essentially represent the same thing, but `BoxConstraints` are more powerful and we avoid a couple of translations between the two by translating the` ViewConstraints` from the `FlutterView` to `BoxConstraints` directly when the `ViewConfiguration` is created. All changes over the original PR are contained in the second commit of this PR. Fixes b/316813075 Part of https://github.com/flutter/flutter/issues/134501. --- .../flutter/lib/src/rendering/binding.dart | 7 +- packages/flutter/lib/src/rendering/box.dart | 9 ++- packages/flutter/lib/src/rendering/view.dart | 81 +++++++++++++++---- .../foundation/service_extensions_test.dart | 3 +- .../test/rendering/box_constraints_test.dart | 15 ++++ .../rendering/independent_layout_test.dart | 4 +- .../flutter/test/rendering/layers_test.dart | 6 +- .../rendering/multi_view_binding_test.dart | 12 ++- .../test/rendering/view_constraints_test.dart | 43 ++++++++++ .../flutter/test/rendering/view_test.dart | 42 +++++++++- .../independent_widget_layout_test.dart | 4 +- .../flutter/test/widgets/keep_alive_test.dart | 6 +- packages/flutter/test/widgets/view_test.dart | 62 ++++++++++++++ .../test/widgets/widget_inspector_test.dart | 9 ++- packages/flutter_test/lib/src/binding.dart | 20 ++++- packages/flutter_test/lib/src/window.dart | 43 ++++++++-- packages/flutter_test/test/view_test.dart | 54 +++++++++++++ .../test/widget_tester_live_device_test.dart | 2 +- 18 files changed, 371 insertions(+), 51 deletions(-) create mode 100644 packages/flutter/test/rendering/view_constraints_test.dart diff --git a/packages/flutter/lib/src/rendering/binding.dart b/packages/flutter/lib/src/rendering/binding.dart index 82bb2bd933..6417185f8b 100644 --- a/packages/flutter/lib/src/rendering/binding.dart +++ b/packages/flutter/lib/src/rendering/binding.dart @@ -347,12 +347,7 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture /// using `flutter run`. @protected ViewConfiguration createViewConfigurationFor(RenderView renderView) { - final FlutterView view = renderView.flutterView; - final double devicePixelRatio = view.devicePixelRatio; - return ViewConfiguration( - size: view.physicalSize / devicePixelRatio, - devicePixelRatio: devicePixelRatio, - ); + return ViewConfiguration.fromView(renderView.flutterView); } /// Called when the system metrics change. diff --git a/packages/flutter/lib/src/rendering/box.dart b/packages/flutter/lib/src/rendering/box.dart index 1449d8b8fb..f01a45e621 100644 --- a/packages/flutter/lib/src/rendering/box.dart +++ b/packages/flutter/lib/src/rendering/box.dart @@ -3,7 +3,7 @@ // found in the LICENSE file. import 'dart:math' as math; -import 'dart:ui' as ui show lerpDouble; +import 'dart:ui' as ui show ViewConstraints, lerpDouble; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; @@ -153,6 +153,13 @@ class BoxConstraints extends Constraints { minHeight = height ?? double.infinity, maxHeight = height ?? double.infinity; + /// Creates box constraints that match the given view constraints. + BoxConstraints.fromViewConstraints(ui.ViewConstraints constraints) + : minWidth = constraints.minWidth, + maxWidth = constraints.maxWidth, + minHeight = constraints.minHeight, + maxHeight = constraints.maxHeight; + /// The minimum width that satisfies the constraints. final double minWidth; diff --git a/packages/flutter/lib/src/rendering/view.dart b/packages/flutter/lib/src/rendering/view.dart index 03db7f1d60..c4842cfc00 100644 --- a/packages/flutter/lib/src/rendering/view.dart +++ b/packages/flutter/lib/src/rendering/view.dart @@ -19,14 +19,40 @@ import 'object.dart'; class ViewConfiguration { /// Creates a view configuration. /// - /// By default, the view has zero [size] and a [devicePixelRatio] of 1.0. + /// By default, the view has [logicalConstraints] and [physicalConstraints] + /// with all dimensions set to zero (i.e. the view is forced to [Size.zero]) + /// and a [devicePixelRatio] of 1.0. + /// + /// [ViewConfiguration.fromView] is a more convenient way for deriving a + /// [ViewConfiguration] from a given [FlutterView]. const ViewConfiguration({ - this.size = Size.zero, + this.physicalConstraints = const BoxConstraints(maxWidth: 0, maxHeight: 0), + this.logicalConstraints = const BoxConstraints(maxWidth: 0, maxHeight: 0), this.devicePixelRatio = 1.0, }); - /// The size of the output surface. - final Size size; + /// Creates a view configuration for the provided [FlutterView]. + factory ViewConfiguration.fromView(ui.FlutterView view) { + final BoxConstraints physicalConstraints = BoxConstraints.fromViewConstraints(view.physicalConstraints); + final double devicePixelRatio = view.devicePixelRatio; + return ViewConfiguration( + physicalConstraints: physicalConstraints, + logicalConstraints: physicalConstraints / devicePixelRatio, + devicePixelRatio: devicePixelRatio, + ); + } + + /// The constraints of the output surface in logical pixel. + /// + /// The constraints are passed to the child of the root render object. + final BoxConstraints logicalConstraints; + + /// The constraints of the output surface in physical pixel. + /// + /// These constraints are enforced in [toPhysicalSize] when translating + /// the logical size of the root render object back to physical pixels for + /// the [FlutterView.render] method. + final BoxConstraints physicalConstraints; /// The pixel density of the output surface. final double devicePixelRatio; @@ -40,21 +66,36 @@ class ViewConfiguration { return Matrix4.diagonal3Values(devicePixelRatio, devicePixelRatio, 1.0); } + /// Transforms the provided [Size] in logical pixels to physical pixels. + /// + /// The [FlutterView.render] method accepts only sizes in physical pixels, but + /// the framework operates in logical pixels. This method is used to transform + /// the logical size calculated for a [RenderView] back to a physical size + /// suitable to be passed to [FlutterView.render]. + /// + /// By default, this method just multiplies the provided [Size] with the + /// [devicePixelRatio] and constraints the results to the + /// [physicalConstraints]. + Size toPhysicalSize(Size logicalSize) { + return physicalConstraints.constrain(logicalSize * devicePixelRatio); + } + @override bool operator ==(Object other) { if (other.runtimeType != runtimeType) { return false; } return other is ViewConfiguration - && other.size == size + && other.logicalConstraints == logicalConstraints + && other.physicalConstraints == physicalConstraints && other.devicePixelRatio == devicePixelRatio; } @override - int get hashCode => Object.hash(size, devicePixelRatio); + int get hashCode => Object.hash(logicalConstraints, physicalConstraints, devicePixelRatio); @override - String toString() => '$size at ${debugFormatDouble(devicePixelRatio)}x'; + String toString() => '$logicalConstraints at ${debugFormatDouble(devicePixelRatio)}x'; } /// The root of the render tree. @@ -76,8 +117,10 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin RenderBox? child, ViewConfiguration? configuration, required ui.FlutterView view, - }) : _configuration = configuration, - _view = view { + }) : _view = view { + if (configuration != null) { + this.configuration = configuration; + } this.child = child; } @@ -119,6 +162,14 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin /// Whether a [configuration] has been set. bool get hasConfiguration => _configuration != null; + @override + BoxConstraints get constraints { + if (!hasConfiguration) { + throw StateError('Constraints are not available because RenderView has not been given a configuration yet.'); + } + return configuration.logicalConstraints; + } + /// The [FlutterView] into which this [RenderView] will render. ui.FlutterView get flutterView => _view; final ui.FlutterView _view; @@ -188,12 +239,13 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin @override void performLayout() { assert(_rootTransform != null); - _size = configuration.size; - assert(_size.isFinite); - + final bool sizedByChild = !constraints.isTight; if (child != null) { - child!.layout(BoxConstraints.tight(_size)); + child!.layout(constraints, parentUsesSize: sizedByChild); } + _size = sizedByChild && child != null ? child!.size : constraints.smallest; + assert(size.isFinite); + assert(constraints.isSatisfiedBy(size)); } /// Determines the set of render objects located at the given position. @@ -253,7 +305,8 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin if (automaticSystemUiAdjustment) { _updateSystemChrome(); } - _view.render(scene); + assert(configuration.logicalConstraints.isSatisfiedBy(size)); + _view.render(scene, size: configuration.toPhysicalSize(size)); scene.dispose(); assert(() { if (debugRepaintRainbowEnabled || debugRepaintTextRainbowEnabled) { diff --git a/packages/flutter/test/foundation/service_extensions_test.dart b/packages/flutter/test/foundation/service_extensions_test.dart index 88056b2589..14db86f397 100644 --- a/packages/flutter/test/foundation/service_extensions_test.dart +++ b/packages/flutter/test/foundation/service_extensions_test.dart @@ -259,7 +259,8 @@ void main() { r' debug mode enabled - [a-zA-Z]+\n' r' view size: Size\(2400\.0, 1800\.0\) \(in physical pixels\)\n' r' device pixel ratio: 3\.0 \(physical pixels per logical pixel\)\n' - r' configuration: Size\(800\.0, 600\.0\) at 3\.0x \(in logical pixels\)\n' + r' configuration: BoxConstraints\(w=800\.0, h=600\.0\) at 3\.0x \(in\n' + r' logical pixels\)\n' r'$', ), }); diff --git a/packages/flutter/test/rendering/box_constraints_test.dart b/packages/flutter/test/rendering/box_constraints_test.dart index 48dadde8d5..6c6cbe1445 100644 --- a/packages/flutter/test/rendering/box_constraints_test.dart +++ b/packages/flutter/test/rendering/box_constraints_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:ui'; + import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -167,4 +169,17 @@ void main() { expect(copy.minHeight, 11.0); expect(copy.maxHeight, 18.0); }); + + test('BoxConstraints.fromViewConstraints', () { + final BoxConstraints unconstrained = BoxConstraints.fromViewConstraints( + const ViewConstraints(), + ); + expect(unconstrained, const BoxConstraints()); + + final BoxConstraints constraints = BoxConstraints.fromViewConstraints( + const ViewConstraints(minWidth: 1, maxWidth: 2, minHeight: 3, maxHeight: 4), + ); + expect(constraints, const BoxConstraints(minWidth: 1, maxWidth: 2, minHeight: 3, maxHeight: 4)); + }); + } diff --git a/packages/flutter/test/rendering/independent_layout_test.dart b/packages/flutter/test/rendering/independent_layout_test.dart index ce40300a52..ed2bbabaa9 100644 --- a/packages/flutter/test/rendering/independent_layout_test.dart +++ b/packages/flutter/test/rendering/independent_layout_test.dart @@ -32,8 +32,8 @@ class TestLayout { void main() { TestRenderingFlutterBinding.ensureInitialized(); - const ViewConfiguration testConfiguration = ViewConfiguration( - size: Size(800.0, 600.0), + final ViewConfiguration testConfiguration = ViewConfiguration( + logicalConstraints: BoxConstraints.tight(const Size(800.0, 600.0)), ); test('onscreen layout does not affect offscreen', () { diff --git a/packages/flutter/test/rendering/layers_test.dart b/packages/flutter/test/rendering/layers_test.dart index 20f446c214..48d6039095 100644 --- a/packages/flutter/test/rendering/layers_test.dart +++ b/packages/flutter/test/rendering/layers_test.dart @@ -169,7 +169,8 @@ void main() { test('switching layer link of an attached leader layer should not crash', () { final LayerLink link = LayerLink(); final LeaderLayer leaderLayer = LeaderLayer(link: link); - final RenderView view = RenderView(configuration: const ViewConfiguration(), view: RendererBinding.instance.platformDispatcher.views.single); + final FlutterView flutterView = RendererBinding.instance.platformDispatcher.views.single; + final RenderView view = RenderView(configuration: ViewConfiguration.fromView(flutterView), view: flutterView); leaderLayer.attach(view); final LayerLink link2 = LayerLink(); leaderLayer.link = link2; @@ -182,7 +183,8 @@ void main() { final LayerLink link = LayerLink(); final LeaderLayer leaderLayer1 = LeaderLayer(link: link); final LeaderLayer leaderLayer2 = LeaderLayer(link: link); - final RenderView view = RenderView(configuration: const ViewConfiguration(), view: RendererBinding.instance.platformDispatcher.views.single); + final FlutterView flutterView = RendererBinding.instance.platformDispatcher.views.single; + final RenderView view = RenderView(configuration: ViewConfiguration.fromView(flutterView), view: flutterView); leaderLayer1.attach(view); leaderLayer2.attach(view); leaderLayer2.detach(); diff --git a/packages/flutter/test/rendering/multi_view_binding_test.dart b/packages/flutter/test/rendering/multi_view_binding_test.dart index 4edfbcf312..76bd94706f 100644 --- a/packages/flutter/test/rendering/multi_view_binding_test.dart +++ b/packages/flutter/test/rendering/multi_view_binding_test.dart @@ -18,7 +18,7 @@ void main() { binding.addRenderView(view); expect(binding.renderViews, contains(view)); expect(view.configuration.devicePixelRatio, flutterView.devicePixelRatio); - expect(view.configuration.size, flutterView.physicalSize / flutterView.devicePixelRatio); + expect(view.configuration.logicalConstraints, BoxConstraints.tight(flutterView.physicalSize) / flutterView.devicePixelRatio); binding.removeRenderView(view); expect(binding.renderViews, isEmpty); @@ -51,13 +51,17 @@ void main() { final RenderView view = RenderView(view: flutterView); binding.addRenderView(view); expect(view.configuration.devicePixelRatio, 2.5); - expect(view.configuration.size, const Size(160.0, 240.0)); + expect(view.configuration.logicalConstraints.isTight, isTrue); + expect(view.configuration.logicalConstraints.minWidth, 160.0); + expect(view.configuration.logicalConstraints.minHeight, 240.0); flutterView.devicePixelRatio = 3.0; flutterView.physicalSize = const Size(300, 300); binding.handleMetricsChanged(); expect(view.configuration.devicePixelRatio, 3.0); - expect(view.configuration.size, const Size(100.0, 100.0)); + expect(view.configuration.logicalConstraints.isTight, isTrue); + expect(view.configuration.logicalConstraints.minWidth, 100.0); + expect(view.configuration.logicalConstraints.minHeight, 100.0); binding.removeRenderView(view); }); @@ -183,6 +187,8 @@ class FakeFlutterView extends Fake implements FlutterView { @override Size physicalSize; @override + ViewConstraints get physicalConstraints => ViewConstraints.tight(physicalSize); + @override ViewPadding padding; List renderedScenes = []; diff --git a/packages/flutter/test/rendering/view_constraints_test.dart b/packages/flutter/test/rendering/view_constraints_test.dart new file mode 100644 index 0000000000..b5812819a7 --- /dev/null +++ b/packages/flutter/test/rendering/view_constraints_test.dart @@ -0,0 +1,43 @@ +// 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 'dart:ui'; + +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Properly constraints the physical size', (WidgetTester tester) async { + final FlutterViewSpy view = FlutterViewSpy(view: tester.view) + ..physicalConstraints = ViewConstraints.tight(const Size(1008.0, 2198.0)) + ..devicePixelRatio = 1.912500023841858; + + await pumpWidgetWithoutViewWrapper( + tester: tester, + widget: View( + view: view, + child: const SizedBox(), + ), + ); + + expect(view.sizes.single, const Size(1008.0, 2198.0)); + }); +} + +class FlutterViewSpy extends TestFlutterView { + FlutterViewSpy({required TestFlutterView super.view}) : super(platformDispatcher: view.platformDispatcher, display: view.display); + + List sizes = []; + + @override + void render(Scene scene, {Size? size}) { + sizes.add(size); + } +} + +Future pumpWidgetWithoutViewWrapper({required WidgetTester tester, required Widget widget}) { + tester.binding.attachRootWidget(widget); + tester.binding.scheduleFrame(); + return tester.binding.pump(); +} diff --git a/packages/flutter/test/rendering/view_test.dart b/packages/flutter/test/rendering/view_test.dart index 9caed8c544..c85a38a8f2 100644 --- a/packages/flutter/test/rendering/view_test.dart +++ b/packages/flutter/test/rendering/view_test.dart @@ -16,14 +16,19 @@ void main() { Size size = const Size(20, 20), double devicePixelRatio = 2.0, }) { - return ViewConfiguration(size: size, devicePixelRatio: devicePixelRatio); + final BoxConstraints constraints = BoxConstraints.tight(size); + return ViewConfiguration( + logicalConstraints: constraints, + physicalConstraints: constraints * devicePixelRatio, + devicePixelRatio: devicePixelRatio, + ); } group('RenderView', () { test('accounts for device pixel ratio in paintBounds', () { layout(RenderAspectRatio(aspectRatio: 1.0)); pumpFrame(); - final Size logicalSize = TestRenderingFlutterBinding.instance.renderView.configuration.size; + final Size logicalSize = TestRenderingFlutterBinding.instance.renderView.size; final double devicePixelRatio = TestRenderingFlutterBinding.instance.renderView.configuration.devicePixelRatio; final Size physicalSize = logicalSize * devicePixelRatio; expect(TestRenderingFlutterBinding.instance.renderView.paintBounds, Offset.zero & physicalSize); @@ -126,11 +131,40 @@ void main() { final RenderView view = RenderView( view: RendererBinding.instance.platformDispatcher.views.single, ); - view.configuration = const ViewConfiguration(size: Size(100, 200), devicePixelRatio: 3.0); - view.configuration = const ViewConfiguration(size: Size(200, 300), devicePixelRatio: 2.0); + view.configuration = ViewConfiguration(logicalConstraints: BoxConstraints.tight(const Size(100, 200)), devicePixelRatio: 3.0); + view.configuration = ViewConfiguration(logicalConstraints: BoxConstraints.tight(const Size(200, 300)), devicePixelRatio: 2.0); PipelineOwner().rootNode = view; view.prepareInitialFrame(); }); + + test('Constraints are derived from configuration', () { + const BoxConstraints constraints = BoxConstraints(minWidth: 1, maxWidth: 2, minHeight: 3, maxHeight: 4); + const double devicePixelRatio = 3.0; + final ViewConfiguration config = ViewConfiguration( + logicalConstraints: constraints, + physicalConstraints: constraints * devicePixelRatio, + devicePixelRatio: devicePixelRatio, + ); + + // Configuration set via setter. + final RenderView view = RenderView( + view: RendererBinding.instance.platformDispatcher.views.single, + ); + expect(() => view.constraints, throwsA(isA().having( + (StateError e) => e.message, + 'message', + contains('RenderView has not been given a configuration yet'), + ))); + view.configuration = config; + expect(view.constraints, constraints); + + // Configuration set in constructor. + final RenderView view2 = RenderView( + view: RendererBinding.instance.platformDispatcher.views.single, + configuration: config, + ); + expect(view2.constraints, constraints); + }); } const Color orange = Color(0xFFFF9000); diff --git a/packages/flutter/test/widgets/independent_widget_layout_test.dart b/packages/flutter/test/widgets/independent_widget_layout_test.dart index e35c5dc768..9d91297cbf 100644 --- a/packages/flutter/test/widgets/independent_widget_layout_test.dart +++ b/packages/flutter/test/widgets/independent_widget_layout_test.dart @@ -8,8 +8,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; -const Size _kTestViewSize = Size(800.0, 600.0); - class ScheduledFrameTrackingPlatformDispatcher extends TestPlatformDispatcher { ScheduledFrameTrackingPlatformDispatcher({ required super.platformDispatcher }); @@ -36,7 +34,7 @@ class ScheduledFrameTrackingBindings extends AutomatedTestWidgetsFlutterBinding class OffscreenRenderView extends RenderView { OffscreenRenderView({required super.view}) : super( - configuration: const ViewConfiguration(size: _kTestViewSize), + configuration: TestViewConfiguration.fromView(view: view), ); @override diff --git a/packages/flutter/test/widgets/keep_alive_test.dart b/packages/flutter/test/widgets/keep_alive_test.dart index 89a44907fe..9f02368597 100644 --- a/packages/flutter/test/widgets/keep_alive_test.dart +++ b/packages/flutter/test/widgets/keep_alive_test.dart @@ -217,7 +217,8 @@ void main() { ' │ debug mode enabled - ${Platform.operatingSystem}\n' ' │ view size: Size(2400.0, 1800.0) (in physical pixels)\n' ' │ device pixel ratio: 3.0 (physical pixels per logical pixel)\n' - ' │ configuration: Size(800.0, 600.0) at 3.0x (in logical pixels)\n' + ' │ configuration: BoxConstraints(w=800.0, h=600.0) at 3.0x (in\n' + ' │ logical pixels)\n' ' │\n' ' └─child: RenderRepaintBoundary#00000\n' ' │ needs compositing\n' @@ -391,7 +392,8 @@ void main() { ' │ debug mode enabled - ${Platform.operatingSystem}\n' ' │ view size: Size(2400.0, 1800.0) (in physical pixels)\n' ' │ device pixel ratio: 3.0 (physical pixels per logical pixel)\n' - ' │ configuration: Size(800.0, 600.0) at 3.0x (in logical pixels)\n' + ' │ configuration: BoxConstraints(w=800.0, h=600.0) at 3.0x (in\n' + ' │ logical pixels)\n' ' │\n' ' └─child: RenderRepaintBoundary#00000\n' ' │ needs compositing\n' diff --git a/packages/flutter/test/widgets/view_test.dart b/packages/flutter/test/widgets/view_test.dart index 2177459d0b..7f93176504 100644 --- a/packages/flutter/test/widgets/view_test.dart +++ b/packages/flutter/test/widgets/view_test.dart @@ -449,6 +449,68 @@ void main() { }); expect(children, isNot(contains(rawViewOwner))); }); + + testWidgets('RenderView does not use size of child if constraints are tight', (WidgetTester tester) async { + const Size physicalSize = Size(300, 600); + final Size logicalSize = physicalSize / tester.view.devicePixelRatio; + tester.view.physicalConstraints = ViewConstraints.tight(physicalSize); + await tester.pumpWidget(const Placeholder()); + + final RenderView renderView = tester.renderObject(find.byType(View)); + expect(renderView.constraints, BoxConstraints.tight(logicalSize)); + expect(renderView.size, logicalSize); + + final RenderBox child = renderView.child!; + expect(child.constraints, BoxConstraints.tight(logicalSize)); + expect(child.debugCanParentUseSize, isFalse); + expect(child.size, logicalSize); + }); + + testWidgets('RenderView sizes itself to child if constraints allow it (unconstrained)', (WidgetTester tester) async { + const Size size = Size(300, 600); + tester.view.physicalConstraints = const ViewConstraints(); // unconstrained + await tester.pumpWidget(SizedBox.fromSize(size: size)); + + final RenderView renderView = tester.renderObject(find.byType(View)); + expect(renderView.constraints, const BoxConstraints()); + expect(renderView.size, size); + + final RenderBox child = renderView.child!; + expect(child.constraints, const BoxConstraints()); + expect(child.debugCanParentUseSize, isTrue); + expect(child.size, size); + }); + + testWidgets('RenderView sizes itself to child if constraints allow it (constrained)', (WidgetTester tester) async { + const Size size = Size(30, 60); + const ViewConstraints viewConstraints = ViewConstraints(maxWidth: 333, maxHeight: 666); + final BoxConstraints boxConstraints = BoxConstraints.fromViewConstraints(viewConstraints / tester.view.devicePixelRatio); + tester.view.physicalConstraints = viewConstraints; + await tester.pumpWidget(SizedBox.fromSize(size: size)); + + final RenderView renderView = tester.renderObject(find.byType(View)); + expect(renderView.constraints, boxConstraints); + expect(renderView.size, size); + + final RenderBox child = renderView.child!; + expect(child.constraints, boxConstraints); + expect(child.debugCanParentUseSize, isTrue); + expect(child.size, size); + }); + + testWidgets('RenderView respects constraints when child wants to be bigger than allowed', (WidgetTester tester) async { + const Size size = Size(3000, 6000); + const ViewConstraints viewConstraints = ViewConstraints(maxWidth: 300, maxHeight: 600); + tester.view.physicalConstraints = viewConstraints; + await tester.pumpWidget(SizedBox.fromSize(size: size)); + + final RenderView renderView = tester.renderObject(find.byType(View)); + expect(renderView.size, const Size(100, 200)); // viewConstraints.biggest / devicePixelRatio + + final RenderBox child = renderView.child!; + expect(child.debugCanParentUseSize, isTrue); + expect(child.size, const Size(100, 200)); + }); } Future pumpWidgetWithoutViewWrapper({required WidgetTester tester, required Widget widget}) { diff --git a/packages/flutter/test/widgets/widget_inspector_test.dart b/packages/flutter/test/widgets/widget_inspector_test.dart index 4902f310fe..8e5345419c 100644 --- a/packages/flutter/test/widgets/widget_inspector_test.dart +++ b/packages/flutter/test/widgets/widget_inspector_test.dart @@ -4690,7 +4690,14 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { expect(renderObject!['description'], contains('RenderView')); expect(result['parentRenderElement'], isNull); - expect(result['constraints'], isNull); + + final Map? constraints = result['constraints'] as Map?; + expect(constraints, isNotNull); + expect(constraints!['type'], equals('BoxConstraints')); + expect(constraints['minWidth'], equals('800.0')); + expect(constraints['minHeight'], equals('600.0')); + expect(constraints['maxWidth'], equals('800.0')); + expect(constraints['maxHeight'], equals('600.0')); expect(result['isBox'], isNull); final Map? size = result['size'] as Map?; diff --git a/packages/flutter_test/lib/src/binding.dart b/packages/flutter_test/lib/src/binding.dart index 93a0dd59b2..cc341e3159 100644 --- a/packages/flutter_test/lib/src/binding.dart +++ b/packages/flutter_test/lib/src/binding.dart @@ -559,8 +559,10 @@ abstract class TestWidgetsFlutterBinding extends BindingBase } final FlutterView view = renderView.flutterView; if (_surfaceSize != null && view == platformDispatcher.implicitView) { + final BoxConstraints constraints = BoxConstraints.tight(_surfaceSize!); return ViewConfiguration( - size: _surfaceSize!, + logicalConstraints: constraints, + physicalConstraints: constraints * view.devicePixelRatio, devicePixelRatio: view.devicePixelRatio, ); } @@ -1832,7 +1834,7 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { final Map? pointerIdToRecord = _renderViewToPointerIdToPointerRecord[renderView]; if (pointerIdToRecord != null && pointerIdToRecord.isNotEmpty) { - final double radius = renderView.configuration.size.shortestSide * 0.05; + final double radius = renderView.size.shortestSide * 0.05; final Path path = Path() ..addOval(Rect.fromCircle(center: Offset.zero, radius: radius)) ..moveTo(0.0, -radius * 2.0) @@ -2116,9 +2118,14 @@ class TestViewConfiguration extends ViewConfiguration { /// Creates a [TestViewConfiguration] with the given size and view. /// /// The [size] defaults to 800x600. - TestViewConfiguration.fromView({required ui.FlutterView view, super.size = _kDefaultTestViewportSize}) + TestViewConfiguration.fromView({required ui.FlutterView view, Size size = _kDefaultTestViewportSize}) : _paintMatrix = _getMatrix(size, view.devicePixelRatio, view), - super(devicePixelRatio: view.devicePixelRatio); + _physicalSize = view.physicalSize, + super( + devicePixelRatio: view.devicePixelRatio, + logicalConstraints: BoxConstraints.tight(size), + physicalConstraints: BoxConstraints.tight(size) * view.devicePixelRatio, + ); static Matrix4 _getMatrix(Size size, double devicePixelRatio, ui.FlutterView window) { final double inverseRatio = devicePixelRatio / window.devicePixelRatio; @@ -2149,6 +2156,11 @@ class TestViewConfiguration extends ViewConfiguration { @override Matrix4 toMatrix() => _paintMatrix.clone(); + final Size _physicalSize; + + @override + Size toPhysicalSize(Size logicalSize) => _physicalSize; + @override String toString() => 'TestViewConfiguration'; } diff --git a/packages/flutter_test/lib/src/window.dart b/packages/flutter_test/lib/src/window.dart index 5a38fe531a..c459657395 100644 --- a/packages/flutter_test/lib/src/window.dart +++ b/packages/flutter_test/lib/src/window.dart @@ -750,6 +750,9 @@ class TestFlutterView implements FlutterView { /// can only be set in a test environment to emulate different view /// configurations. A standard [FlutterView] is not mutable from the framework. /// + /// Setting this value also sets [physicalConstraints] to tight constraints + /// based on the given size. + /// /// See also: /// /// * [FlutterView.physicalSize] for the standard implementation @@ -760,12 +763,39 @@ class TestFlutterView implements FlutterView { Size? _physicalSize; set physicalSize(Size value) { _physicalSize = value; + // For backwards compatibility the constraints are set based on the provided size. + physicalConstraints = ViewConstraints.tight(value); + } + + /// Resets [physicalSize] (and implicitly also the [physicalConstraints]) to + /// the default value for this view. + void resetPhysicalSize() { + _physicalSize = null; + resetPhysicalConstraints(); + } + + /// The physical constraints to use for this test. + /// + /// Defaults to the value provided by [FlutterView.physicalConstraints]. This + /// can only be set in a test environment to emulate different view + /// configurations. A standard [FlutterView] is not mutable from the framework. + /// + /// See also: + /// + /// * [FlutterView.physicalConstraints] for the standard implementation + /// * [physicalConstraints] to reset this value specifically + /// * [reset] to reset all test values for this view + @override + ViewConstraints get physicalConstraints => _physicalConstraints ?? _view.physicalConstraints; + ViewConstraints? _physicalConstraints; + set physicalConstraints(ViewConstraints value) { + _physicalConstraints = value; platformDispatcher.onMetricsChanged?.call(); } - /// Resets [physicalSize] to the default value for this view. - void resetPhysicalSize() { - _physicalSize = null; + /// Resets [physicalConstraints] to the default value for this view. + void resetPhysicalConstraints() { + _physicalConstraints = null; platformDispatcher.onMetricsChanged?.call(); } @@ -874,8 +904,7 @@ class TestFlutterView implements FlutterView { @override void render(Scene scene, {Size? size}) { - // TODO(goderbauer): Wire through size after https://github.com/flutter/engine/pull/48090 rolled in. - _view.render(scene); + _view.render(scene, size: size); } @override @@ -900,6 +929,7 @@ class TestFlutterView implements FlutterView { resetDisplayFeatures(); resetPadding(); resetPhysicalSize(); + // resetPhysicalConstraints is implicitly called by resetPhysicalSize. resetSystemGestureInsets(); resetViewInsets(); resetViewPadding(); @@ -1636,8 +1666,7 @@ class TestWindow implements SingletonFlutterWindow { ) @override void render(Scene scene, {Size? size}) { - // TODO(goderbauer): Wire through size after https://github.com/flutter/engine/pull/48090 rolled in. - _view.render(scene); + _view.render(scene, size: size); } @Deprecated( diff --git a/packages/flutter_test/test/view_test.dart b/packages/flutter_test/test/view_test.dart index 5e14ced0ed..2b245a3930 100644 --- a/packages/flutter_test/test/view_test.dart +++ b/packages/flutter_test/test/view_test.dart @@ -124,6 +124,19 @@ void main() { ); }); + testWidgets('faking physicalSize fakes physicalConstraints', (WidgetTester tester) async { + const Size fakeSize = Size(50, 50); + verifyPropertyFaked( + tester: tester, + realValue: trueImplicitView().physicalConstraints, + fakeValue: ViewConstraints.tight(fakeSize), + propertyRetriever: () => boundImplicitView().physicalConstraints, + propertyFaker: (_, __) { + tester.view.physicalSize = fakeSize; + }, + ); + }); + testWidgets('can reset physicalSize', (WidgetTester tester) async { verifyPropertyReset( tester: tester, @@ -138,6 +151,47 @@ void main() { ); }); + testWidgets('resetting physicalSize resets physicalConstraints', (WidgetTester tester) async { + const Size fakeSize = Size(50, 50); + verifyPropertyReset( + tester: tester, + fakeValue: ViewConstraints.tight(fakeSize), + propertyRetriever: () => boundImplicitView().physicalConstraints, + propertyResetter: () { + tester.view.resetPhysicalSize(); + }, + propertyFaker: (_) { + tester.view.physicalSize = fakeSize; + }, + ); + }); + + testWidgets('can fake physicalConstraints', (WidgetTester tester) async { + verifyPropertyFaked( + tester: tester, + realValue: trueImplicitView().physicalConstraints, + fakeValue: const ViewConstraints(minWidth: 1, maxWidth: 2, minHeight: 3, maxHeight: 4), + propertyRetriever: () => boundImplicitView().physicalConstraints, + propertyFaker: (_, ViewConstraints fakeValue) { + tester.view.physicalConstraints = fakeValue; + }, + ); + }); + + testWidgets('can reset physicalConstraints', (WidgetTester tester) async { + verifyPropertyReset( + tester: tester, + fakeValue: const ViewConstraints(minWidth: 1, maxWidth: 2, minHeight: 3, maxHeight: 4), + propertyRetriever: () => boundImplicitView().physicalConstraints, + propertyResetter: () { + tester.view.resetPhysicalConstraints(); + }, + propertyFaker: (ViewConstraints fakeValue) { + tester.view.physicalConstraints = fakeValue; + }, + ); + }); + testWidgets('can fake systemGestureInsets', (WidgetTester tester) async { verifyPropertyFaked( tester: tester, diff --git a/packages/flutter_test/test/widget_tester_live_device_test.dart b/packages/flutter_test/test/widget_tester_live_device_test.dart index c6468a2f47..18c151e9bd 100644 --- a/packages/flutter_test/test/widget_tester_live_device_test.dart +++ b/packages/flutter_test/test/widget_tester_live_device_test.dart @@ -83,7 +83,7 @@ No widgets found at Offset(1.0, 1.0). ), ); - final Size originalSize = tester.binding.createViewConfigurationFor(tester.binding.renderView).size; // ignore: deprecated_member_use + final Size originalSize = tester.binding.renderView.size; // ignore: deprecated_member_use await tester.binding.setSurfaceSize(const Size(2000, 1800)); try { await tester.pump();