Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 2 commits into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions packages/flutter/lib/src/rendering/binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
9 changes: 8 additions & 1 deletion packages/flutter/lib/src/rendering/box.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;

Expand Down
81 changes: 67 additions & 14 deletions packages/flutter/lib/src/rendering/view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand All @@ -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;
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'$',
),
});
Expand Down
15 changes: 15 additions & 0 deletions packages/flutter/test/rendering/box_constraints_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

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

}
4 changes: 2 additions & 2 deletions packages/flutter/test/rendering/independent_layout_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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', () {
Expand Down
6 changes: 4 additions & 2 deletions packages/flutter/test/rendering/layers_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
Expand Down
12 changes: 9 additions & 3 deletions packages/flutter/test/rendering/multi_view_binding_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
});
Expand Down Expand Up @@ -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>[];
Expand Down
43 changes: 43 additions & 0 deletions packages/flutter/test/rendering/view_constraints_test.dart
Original file line number Diff line number Diff line change
@@ -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();
}
42 changes: 38 additions & 4 deletions packages/flutter/test/rendering/view_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
Loading