Unverified Commit 4f8a9914 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Dynamic view sizing (#138648)

Towards https://github.com/flutter/flutter/issues/134501.

This change is based on https://github.com/flutter/engine/pull/48090. It changes the `RenderView` to be dynamically sized based on its content if the `FlutterView` it is configured with allows it (i.e. the `FlutterView` has loose `FlutterView.physicalConstraints`). For that, it uses those `physicalConstraints` as input to the layout algorithm by passing them on to its child (after translating them to logical constraints via the device pixel ratio). The resulting `Size` that the `RenderView` would like to be is then communicated back to the engine by passing it to the `FlutterView.render` call.

Tests will fail until https://github.com/flutter/engine/pull/48090 has rolled into the framework.
parent b4178125
...@@ -350,7 +350,7 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture ...@@ -350,7 +350,7 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
final FlutterView view = renderView.flutterView; final FlutterView view = renderView.flutterView;
final double devicePixelRatio = view.devicePixelRatio; final double devicePixelRatio = view.devicePixelRatio;
return ViewConfiguration( return ViewConfiguration(
size: view.physicalSize / devicePixelRatio, constraints: view.physicalConstraints / devicePixelRatio,
devicePixelRatio: devicePixelRatio, devicePixelRatio: devicePixelRatio,
); );
} }
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:math' as math; 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/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
...@@ -153,6 +153,13 @@ class BoxConstraints extends Constraints { ...@@ -153,6 +153,13 @@ class BoxConstraints extends Constraints {
minHeight = height ?? double.infinity, minHeight = height ?? double.infinity,
maxHeight = 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. /// The minimum width that satisfies the constraints.
final double minWidth; final double minWidth;
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:io' show Platform; import 'dart:io' show Platform;
import 'dart:ui' as ui show FlutterView, Scene, SceneBuilder, SemanticsUpdate; import 'dart:ui' as ui show FlutterView, Scene, SceneBuilder, SemanticsUpdate, ViewConstraints;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
...@@ -19,14 +19,15 @@ import 'object.dart'; ...@@ -19,14 +19,15 @@ import 'object.dart';
class ViewConfiguration { class ViewConfiguration {
/// Creates a view configuration. /// Creates a view configuration.
/// ///
/// By default, the view has zero [size] and a [devicePixelRatio] of 1.0. /// By default, the view has [ViewConstraints] with all dimensions set to zero
/// (i.e. the view is forced to [Size.zero]) and a [devicePixelRatio] of 1.0.
const ViewConfiguration({ const ViewConfiguration({
this.size = Size.zero, this.constraints = const ui.ViewConstraints(maxWidth: 0.0, maxHeight: 0.0),
this.devicePixelRatio = 1.0, this.devicePixelRatio = 1.0,
}); });
/// The size of the output surface. /// The constraints of the output surface in logical pixel.
final Size size; final ui.ViewConstraints constraints;
/// The pixel density of the output surface. /// The pixel density of the output surface.
final double devicePixelRatio; final double devicePixelRatio;
...@@ -40,21 +41,34 @@ class ViewConfiguration { ...@@ -40,21 +41,34 @@ class ViewConfiguration {
return Matrix4.diagonal3Values(devicePixelRatio, devicePixelRatio, 1.0); 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].
Size toPhysicalSize(Size logicalSize) {
return logicalSize * devicePixelRatio;
}
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
if (other.runtimeType != runtimeType) { if (other.runtimeType != runtimeType) {
return false; return false;
} }
return other is ViewConfiguration return other is ViewConfiguration
&& other.size == size && other.constraints == constraints
&& other.devicePixelRatio == devicePixelRatio; && other.devicePixelRatio == devicePixelRatio;
} }
@override @override
int get hashCode => Object.hash(size, devicePixelRatio); int get hashCode => Object.hash(constraints, devicePixelRatio);
@override @override
String toString() => '$size at ${debugFormatDouble(devicePixelRatio)}x'; String toString() => '$constraints at ${debugFormatDouble(devicePixelRatio)}x';
} }
/// The root of the render tree. /// The root of the render tree.
...@@ -76,8 +90,10 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox> ...@@ -76,8 +90,10 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
RenderBox? child, RenderBox? child,
ViewConfiguration? configuration, ViewConfiguration? configuration,
required ui.FlutterView view, required ui.FlutterView view,
}) : _configuration = configuration, }) : _view = view {
_view = view { if (configuration != null) {
this.configuration = configuration;
}
this.child = child; this.child = child;
} }
...@@ -105,6 +121,7 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox> ...@@ -105,6 +121,7 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
} }
final ViewConfiguration? oldConfiguration = _configuration; final ViewConfiguration? oldConfiguration = _configuration;
_configuration = value; _configuration = value;
_constraints = BoxConstraints.fromViewConstraints(configuration.constraints);
if (_rootTransform == null) { if (_rootTransform == null) {
// [prepareInitialFrame] has not been called yet, nothing to do for now. // [prepareInitialFrame] has not been called yet, nothing to do for now.
return; return;
...@@ -119,6 +136,15 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox> ...@@ -119,6 +136,15 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
/// Whether a [configuration] has been set. /// Whether a [configuration] has been set.
bool get hasConfiguration => _configuration != null; bool get hasConfiguration => _configuration != null;
@override
BoxConstraints get constraints {
if (_constraints == null) {
throw StateError('Constraints are not available because RenderView has not been given a configuration yet.');
}
return _constraints!;
}
BoxConstraints? _constraints;
/// The [FlutterView] into which this [RenderView] will render. /// The [FlutterView] into which this [RenderView] will render.
ui.FlutterView get flutterView => _view; ui.FlutterView get flutterView => _view;
final ui.FlutterView _view; final ui.FlutterView _view;
...@@ -188,12 +214,13 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox> ...@@ -188,12 +214,13 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
@override @override
void performLayout() { void performLayout() {
assert(_rootTransform != null); assert(_rootTransform != null);
_size = configuration.size; final bool sizedByChild = !constraints.isTight;
assert(_size.isFinite);
if (child != null) { 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. /// Determines the set of render objects located at the given position.
...@@ -253,7 +280,8 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox> ...@@ -253,7 +280,8 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
if (automaticSystemUiAdjustment) { if (automaticSystemUiAdjustment) {
_updateSystemChrome(); _updateSystemChrome();
} }
_view.render(scene); assert(configuration.constraints.isSatisfiedBy(size));
_view.render(scene, size: configuration.toPhysicalSize(size));
scene.dispose(); scene.dispose();
assert(() { assert(() {
if (debugRepaintRainbowEnabled || debugRepaintTextRainbowEnabled) { if (debugRepaintRainbowEnabled || debugRepaintTextRainbowEnabled) {
......
...@@ -259,7 +259,8 @@ void main() { ...@@ -259,7 +259,8 @@ void main() {
r' debug mode enabled - [a-zA-Z]+\n' r' debug mode enabled - [a-zA-Z]+\n'
r' view size: Size\(2400\.0, 1800\.0\) \(in physical pixels\)\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' 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: ViewConstraints\(w=800\.0, h=600\.0\) at 3\.0x \(in\n'
r' logical pixels\)\n'
r'$', r'$',
), ),
}); });
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:ui';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -167,4 +169,17 @@ void main() { ...@@ -167,4 +169,17 @@ void main() {
expect(copy.minHeight, 11.0); expect(copy.minHeight, 11.0);
expect(copy.maxHeight, 18.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));
});
} }
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:ui' show ViewConstraints;
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -32,8 +34,8 @@ class TestLayout { ...@@ -32,8 +34,8 @@ class TestLayout {
void main() { void main() {
TestRenderingFlutterBinding.ensureInitialized(); TestRenderingFlutterBinding.ensureInitialized();
const ViewConfiguration testConfiguration = ViewConfiguration( final ViewConfiguration testConfiguration = ViewConfiguration(
size: Size(800.0, 600.0), constraints: ViewConstraints.tight(const Size(800.0, 600.0)),
); );
test('onscreen layout does not affect offscreen', () { test('onscreen layout does not affect offscreen', () {
......
...@@ -18,7 +18,7 @@ void main() { ...@@ -18,7 +18,7 @@ void main() {
binding.addRenderView(view); binding.addRenderView(view);
expect(binding.renderViews, contains(view)); expect(binding.renderViews, contains(view));
expect(view.configuration.devicePixelRatio, flutterView.devicePixelRatio); expect(view.configuration.devicePixelRatio, flutterView.devicePixelRatio);
expect(view.configuration.size, flutterView.physicalSize / flutterView.devicePixelRatio); expect(view.configuration.constraints, ViewConstraints.tight(flutterView.physicalSize) / flutterView.devicePixelRatio);
binding.removeRenderView(view); binding.removeRenderView(view);
expect(binding.renderViews, isEmpty); expect(binding.renderViews, isEmpty);
...@@ -51,13 +51,17 @@ void main() { ...@@ -51,13 +51,17 @@ void main() {
final RenderView view = RenderView(view: flutterView); final RenderView view = RenderView(view: flutterView);
binding.addRenderView(view); binding.addRenderView(view);
expect(view.configuration.devicePixelRatio, 2.5); expect(view.configuration.devicePixelRatio, 2.5);
expect(view.configuration.size, const Size(160.0, 240.0)); expect(view.configuration.constraints.isTight, isTrue);
expect(view.configuration.constraints.minWidth, 160.0);
expect(view.configuration.constraints.minHeight, 240.0);
flutterView.devicePixelRatio = 3.0; flutterView.devicePixelRatio = 3.0;
flutterView.physicalSize = const Size(300, 300); flutterView.physicalSize = const Size(300, 300);
binding.handleMetricsChanged(); binding.handleMetricsChanged();
expect(view.configuration.devicePixelRatio, 3.0); expect(view.configuration.devicePixelRatio, 3.0);
expect(view.configuration.size, const Size(100.0, 100.0)); expect(view.configuration.constraints.isTight, isTrue);
expect(view.configuration.constraints.minWidth, 100.0);
expect(view.configuration.constraints.minHeight, 100.0);
binding.removeRenderView(view); binding.removeRenderView(view);
}); });
...@@ -183,6 +187,8 @@ class FakeFlutterView extends Fake implements FlutterView { ...@@ -183,6 +187,8 @@ class FakeFlutterView extends Fake implements FlutterView {
@override @override
Size physicalSize; Size physicalSize;
@override @override
ViewConstraints get physicalConstraints => ViewConstraints.tight(physicalSize);
@override
ViewPadding padding; ViewPadding padding;
List<Scene> renderedScenes = <Scene>[]; List<Scene> renderedScenes = <Scene>[];
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:ui' show ViewConstraints;
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -16,14 +18,14 @@ void main() { ...@@ -16,14 +18,14 @@ void main() {
Size size = const Size(20, 20), Size size = const Size(20, 20),
double devicePixelRatio = 2.0, double devicePixelRatio = 2.0,
}) { }) {
return ViewConfiguration(size: size, devicePixelRatio: devicePixelRatio); return ViewConfiguration(constraints: ViewConstraints.tight(size), devicePixelRatio: devicePixelRatio);
} }
group('RenderView', () { group('RenderView', () {
test('accounts for device pixel ratio in paintBounds', () { test('accounts for device pixel ratio in paintBounds', () {
layout(RenderAspectRatio(aspectRatio: 1.0)); layout(RenderAspectRatio(aspectRatio: 1.0));
pumpFrame(); 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 double devicePixelRatio = TestRenderingFlutterBinding.instance.renderView.configuration.devicePixelRatio;
final Size physicalSize = logicalSize * devicePixelRatio; final Size physicalSize = logicalSize * devicePixelRatio;
expect(TestRenderingFlutterBinding.instance.renderView.paintBounds, Offset.zero & physicalSize); expect(TestRenderingFlutterBinding.instance.renderView.paintBounds, Offset.zero & physicalSize);
...@@ -126,11 +128,38 @@ void main() { ...@@ -126,11 +128,38 @@ void main() {
final RenderView view = RenderView( final RenderView view = RenderView(
view: RendererBinding.instance.platformDispatcher.views.single, view: RendererBinding.instance.platformDispatcher.views.single,
); );
view.configuration = const ViewConfiguration(size: Size(100, 200), devicePixelRatio: 3.0); view.configuration = ViewConfiguration(constraints: ViewConstraints.tight(const Size(100, 200)), devicePixelRatio: 3.0);
view.configuration = const ViewConfiguration(size: Size(200, 300), devicePixelRatio: 2.0); view.configuration = ViewConfiguration(constraints: ViewConstraints.tight(const Size(200, 300)), devicePixelRatio: 2.0);
PipelineOwner().rootNode = view; PipelineOwner().rootNode = view;
view.prepareInitialFrame(); view.prepareInitialFrame();
}); });
test('Constraints are derived from configuration', () {
const ViewConfiguration config = ViewConfiguration(
constraints: ViewConstraints(minWidth: 1, maxWidth: 2, minHeight: 3, maxHeight: 4),
devicePixelRatio: 3.0,
);
const BoxConstraints constraints = BoxConstraints(minWidth: 1, maxWidth: 2, minHeight: 3, maxHeight: 4);
// Configuration set via setter.
final RenderView view = RenderView(
view: RendererBinding.instance.platformDispatcher.views.single,
);
expect(() => view.constraints, throwsA(isA<StateError>().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); const Color orange = Color(0xFFFF9000);
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:ui' show FlutterView; import 'dart:ui' show FlutterView, ViewConstraints;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
...@@ -36,7 +36,7 @@ class ScheduledFrameTrackingBindings extends AutomatedTestWidgetsFlutterBinding ...@@ -36,7 +36,7 @@ class ScheduledFrameTrackingBindings extends AutomatedTestWidgetsFlutterBinding
class OffscreenRenderView extends RenderView { class OffscreenRenderView extends RenderView {
OffscreenRenderView({required super.view}) : super( OffscreenRenderView({required super.view}) : super(
configuration: const ViewConfiguration(size: _kTestViewSize), configuration: ViewConfiguration(constraints: ViewConstraints.tight(_kTestViewSize)),
); );
@override @override
......
...@@ -218,7 +218,8 @@ void main() { ...@@ -218,7 +218,8 @@ void main() {
' │ debug mode enabled - ${Platform.operatingSystem}\n' ' │ debug mode enabled - ${Platform.operatingSystem}\n'
' │ view size: Size(2400.0, 1800.0) (in physical pixels)\n' ' │ view size: Size(2400.0, 1800.0) (in physical pixels)\n'
' │ device pixel ratio: 3.0 (physical pixels per logical pixel)\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: ViewConstraints(w=800.0, h=600.0) at 3.0x (in\n'
' │ logical pixels)\n'
' │\n' ' │\n'
' └─child: RenderRepaintBoundary#00000\n' ' └─child: RenderRepaintBoundary#00000\n'
' │ needs compositing\n' ' │ needs compositing\n'
...@@ -392,7 +393,8 @@ void main() { ...@@ -392,7 +393,8 @@ void main() {
' │ debug mode enabled - ${Platform.operatingSystem}\n' ' │ debug mode enabled - ${Platform.operatingSystem}\n'
' │ view size: Size(2400.0, 1800.0) (in physical pixels)\n' ' │ view size: Size(2400.0, 1800.0) (in physical pixels)\n'
' │ device pixel ratio: 3.0 (physical pixels per logical pixel)\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: ViewConstraints(w=800.0, h=600.0) at 3.0x (in\n'
' │ logical pixels)\n'
' │\n' ' │\n'
' └─child: RenderRepaintBoundary#00000\n' ' └─child: RenderRepaintBoundary#00000\n'
' │ needs compositing\n' ' │ needs compositing\n'
......
...@@ -450,6 +450,68 @@ void main() { ...@@ -450,6 +450,68 @@ void main() {
}); });
expect(children, isNot(contains(rawViewOwner))); expect(children, isNot(contains(rawViewOwner)));
}); });
testWidgetsWithLeakTracking('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<RenderView>(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);
});
testWidgetsWithLeakTracking('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<RenderView>(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);
});
testWidgetsWithLeakTracking('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<RenderView>(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);
});
testWidgetsWithLeakTracking('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<RenderView>(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<void> pumpWidgetWithoutViewWrapper({required WidgetTester tester, required Widget widget}) { Future<void> pumpWidgetWithoutViewWrapper({required WidgetTester tester, required Widget widget}) {
......
...@@ -4696,7 +4696,14 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { ...@@ -4696,7 +4696,14 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
expect(renderObject!['description'], contains('RenderView')); expect(renderObject!['description'], contains('RenderView'));
expect(result['parentRenderElement'], isNull); expect(result['parentRenderElement'], isNull);
expect(result['constraints'], isNull);
final Map<String, Object?>? constraints = result['constraints'] as Map<String, Object?>?;
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); expect(result['isBox'], isNull);
final Map<String, Object?>? size = result['size'] as Map<String, Object?>?; final Map<String, Object?>? size = result['size'] as Map<String, Object?>?;
......
...@@ -560,7 +560,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase ...@@ -560,7 +560,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
final FlutterView view = renderView.flutterView; final FlutterView view = renderView.flutterView;
if (_surfaceSize != null && view == platformDispatcher.implicitView) { if (_surfaceSize != null && view == platformDispatcher.implicitView) {
return ViewConfiguration( return ViewConfiguration(
size: _surfaceSize!, constraints: ui.ViewConstraints.tight(_surfaceSize!),
devicePixelRatio: view.devicePixelRatio, devicePixelRatio: view.devicePixelRatio,
); );
} }
...@@ -1831,7 +1831,7 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { ...@@ -1831,7 +1831,7 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
final Map<int, _LiveTestPointerRecord>? pointerIdToRecord = _renderViewToPointerIdToPointerRecord[renderView]; final Map<int, _LiveTestPointerRecord>? pointerIdToRecord = _renderViewToPointerIdToPointerRecord[renderView];
if (pointerIdToRecord != null && pointerIdToRecord.isNotEmpty) { 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() final Path path = Path()
..addOval(Rect.fromCircle(center: Offset.zero, radius: radius)) ..addOval(Rect.fromCircle(center: Offset.zero, radius: radius))
..moveTo(0.0, -radius * 2.0) ..moveTo(0.0, -radius * 2.0)
...@@ -2115,9 +2115,10 @@ class TestViewConfiguration extends ViewConfiguration { ...@@ -2115,9 +2115,10 @@ class TestViewConfiguration extends ViewConfiguration {
/// Creates a [TestViewConfiguration] with the given size and view. /// Creates a [TestViewConfiguration] with the given size and view.
/// ///
/// The [size] defaults to 800x600. /// 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), : _paintMatrix = _getMatrix(size, view.devicePixelRatio, view),
super(devicePixelRatio: view.devicePixelRatio); _physicalSize = view.physicalSize,
super(devicePixelRatio: view.devicePixelRatio, constraints: ui.ViewConstraints.tight(size));
static Matrix4 _getMatrix(Size size, double devicePixelRatio, ui.FlutterView window) { static Matrix4 _getMatrix(Size size, double devicePixelRatio, ui.FlutterView window) {
final double inverseRatio = devicePixelRatio / window.devicePixelRatio; final double inverseRatio = devicePixelRatio / window.devicePixelRatio;
...@@ -2148,6 +2149,11 @@ class TestViewConfiguration extends ViewConfiguration { ...@@ -2148,6 +2149,11 @@ class TestViewConfiguration extends ViewConfiguration {
@override @override
Matrix4 toMatrix() => _paintMatrix.clone(); Matrix4 toMatrix() => _paintMatrix.clone();
final Size _physicalSize;
@override
Size toPhysicalSize(Size logicalSize) => _physicalSize;
@override @override
String toString() => 'TestViewConfiguration'; String toString() => 'TestViewConfiguration';
} }
......
...@@ -751,6 +751,9 @@ class TestFlutterView implements FlutterView { ...@@ -751,6 +751,9 @@ class TestFlutterView implements FlutterView {
/// can only be set in a test environment to emulate different view /// can only be set in a test environment to emulate different view
/// configurations. A standard [FlutterView] is not mutable from the framework. /// 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: /// See also:
/// ///
/// * [FlutterView.physicalSize] for the standard implementation /// * [FlutterView.physicalSize] for the standard implementation
...@@ -761,12 +764,39 @@ class TestFlutterView implements FlutterView { ...@@ -761,12 +764,39 @@ class TestFlutterView implements FlutterView {
Size? _physicalSize; Size? _physicalSize;
set physicalSize(Size value) { set physicalSize(Size value) {
_physicalSize = value; _physicalSize = value;
platformDispatcher.onMetricsChanged?.call(); // For backwards compatibility the constraints are set based on the provided size.
physicalConstraints = ViewConstraints.tight(value);
} }
/// Resets [physicalSize] to the default value for this view. /// Resets [physicalSize] (and implicitly also the [physicalConstraints]) to
/// the default value for this view.
void resetPhysicalSize() { void resetPhysicalSize() {
_physicalSize = null; _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 [physicalConstraints] to the default value for this view.
void resetPhysicalConstraints() {
_physicalConstraints = null;
platformDispatcher.onMetricsChanged?.call(); platformDispatcher.onMetricsChanged?.call();
} }
...@@ -875,8 +905,7 @@ class TestFlutterView implements FlutterView { ...@@ -875,8 +905,7 @@ class TestFlutterView implements FlutterView {
@override @override
void render(Scene scene, {Size? size}) { void render(Scene scene, {Size? size}) {
// TODO(goderbauer): Wire through size after https://github.com/flutter/engine/pull/48090 rolled in. _view.render(scene, size: size);
_view.render(scene);
} }
@override @override
...@@ -901,6 +930,7 @@ class TestFlutterView implements FlutterView { ...@@ -901,6 +930,7 @@ class TestFlutterView implements FlutterView {
resetDisplayFeatures(); resetDisplayFeatures();
resetPadding(); resetPadding();
resetPhysicalSize(); resetPhysicalSize();
// resetPhysicalConstraints is implicitly called by resetPhysicalSize.
resetSystemGestureInsets(); resetSystemGestureInsets();
resetViewInsets(); resetViewInsets();
resetViewPadding(); resetViewPadding();
...@@ -1637,8 +1667,7 @@ class TestWindow implements SingletonFlutterWindow { ...@@ -1637,8 +1667,7 @@ class TestWindow implements SingletonFlutterWindow {
) )
@override @override
void render(Scene scene, {Size? size}) { void render(Scene scene, {Size? size}) {
// TODO(goderbauer): Wire through size after https://github.com/flutter/engine/pull/48090 rolled in. _view.render(scene, size: size);
_view.render(scene);
} }
@Deprecated( @Deprecated(
......
...@@ -124,6 +124,19 @@ void main() { ...@@ -124,6 +124,19 @@ void main() {
); );
}); });
testWidgets('faking physicalSize fakes physicalConstraints', (WidgetTester tester) async {
const Size fakeSize = Size(50, 50);
verifyPropertyFaked<ViewConstraints>(
tester: tester,
realValue: trueImplicitView().physicalConstraints,
fakeValue: ViewConstraints.tight(fakeSize),
propertyRetriever: () => boundImplicitView().physicalConstraints,
propertyFaker: (_, __) {
tester.view.physicalSize = fakeSize;
},
);
});
testWidgets('can reset physicalSize', (WidgetTester tester) async { testWidgets('can reset physicalSize', (WidgetTester tester) async {
verifyPropertyReset<Size>( verifyPropertyReset<Size>(
tester: tester, tester: tester,
...@@ -138,6 +151,47 @@ void main() { ...@@ -138,6 +151,47 @@ void main() {
); );
}); });
testWidgets('resetting physicalSize resets physicalConstraints', (WidgetTester tester) async {
const Size fakeSize = Size(50, 50);
verifyPropertyReset<ViewConstraints>(
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<ViewConstraints>(
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<ViewConstraints>(
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 { testWidgets('can fake systemGestureInsets', (WidgetTester tester) async {
verifyPropertyFaked<ViewPadding>( verifyPropertyFaked<ViewPadding>(
tester: tester, tester: tester,
......
...@@ -83,7 +83,7 @@ No widgets found at Offset(1.0, 1.0). ...@@ -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)); await tester.binding.setSurfaceSize(const Size(2000, 1800));
try { try {
await tester.pump(); await tester.pump();
......
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