Reapply "Dynamic view sizing" (#140165) (#140918)

This reverts commit
d24c01bd0c.

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.
This commit is contained in:
Michael Goderbauer 2024-01-09 14:10:43 -08:00 committed by GitHub
parent 0c40f21fc5
commit 4534a24c09
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 371 additions and 51 deletions

View file

@ -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.

View file

@ -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;

View file

@ -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>
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<RenderBox>
/// 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<RenderBox>
@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<RenderBox>
if (automaticSystemUiAdjustment) {
_updateSystemChrome();
}
_view.render(scene);
assert(configuration.logicalConstraints.isSatisfiedBy(size));
_view.render(scene, size: configuration.toPhysicalSize(size));
scene.dispose();
assert(() {
if (debugRepaintRainbowEnabled || debugRepaintTextRainbowEnabled) {

View file

@ -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'$',
),
});

View file

@ -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));
});
}

View file

@ -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', () {

View file

@ -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();

View file

@ -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<Scene> renderedScenes = <Scene>[];

View file

@ -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<Size?> sizes = <Size?>[];
@override
void render(Scene scene, {Size? size}) {
sizes.add(size);
}
}
Future<void> pumpWidgetWithoutViewWrapper({required WidgetTester tester, required Widget widget}) {
tester.binding.attachRootWidget(widget);
tester.binding.scheduleFrame();
return tester.binding.pump();
}

View file

@ -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<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);

View file

@ -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

View file

@ -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'

View file

@ -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<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);
});
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<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);
});
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<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);
});
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<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}) {

View file

@ -4690,7 +4690,14 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
expect(renderObject!['description'], contains('RenderView'));
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);
final Map<String, Object?>? size = result['size'] as Map<String, Object?>?;

View file

@ -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<int, _LiveTestPointerRecord>? 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';
}

View file

@ -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(

View file

@ -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 {
verifyPropertyReset<Size>(
tester: tester,
@ -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 {
verifyPropertyFaked<ViewPadding>(
tester: tester,

View file

@ -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();