flutter_flutter/packages/flutter/test/rendering/multi_view_binding_test.dart
Michael Goderbauer 4534a24c09
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.
2024-01-09 14:10:43 -08:00

215 lines
7.9 KiB
Dart

// 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/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
final RendererBinding binding = RenderingFlutterBinding.ensureInitialized();
test('Adding/removing renderviews updates renderViews getter', () {
final FlutterView flutterView = FakeFlutterView();
final RenderView view = RenderView(view: flutterView);
expect(binding.renderViews, isEmpty);
binding.addRenderView(view);
expect(binding.renderViews, contains(view));
expect(view.configuration.devicePixelRatio, flutterView.devicePixelRatio);
expect(view.configuration.logicalConstraints, BoxConstraints.tight(flutterView.physicalSize) / flutterView.devicePixelRatio);
binding.removeRenderView(view);
expect(binding.renderViews, isEmpty);
});
test('illegal add/remove renderviews', () {
final FlutterView flutterView = FakeFlutterView();
final RenderView view1 = RenderView(view: flutterView);
final RenderView view2 = RenderView(view: flutterView);
final RenderView view3 = RenderView(view: FakeFlutterView(viewId: 200));
expect(binding.renderViews, isEmpty);
binding.addRenderView(view1);
expect(binding.renderViews, contains(view1));
expect(() => binding.addRenderView(view1), throwsAssertionError);
expect(() => binding.addRenderView(view2), throwsAssertionError);
expect(() => binding.removeRenderView(view2), throwsAssertionError);
expect(() => binding.removeRenderView(view3), throwsAssertionError);
expect(binding.renderViews, contains(view1));
binding.removeRenderView(view1);
expect(binding.renderViews, isEmpty);
expect(() => binding.removeRenderView(view1), throwsAssertionError);
expect(() => binding.removeRenderView(view2), throwsAssertionError);
});
test('changing metrics updates configuration', () {
final FakeFlutterView flutterView = FakeFlutterView();
final RenderView view = RenderView(view: flutterView);
binding.addRenderView(view);
expect(view.configuration.devicePixelRatio, 2.5);
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.logicalConstraints.isTight, isTrue);
expect(view.configuration.logicalConstraints.minWidth, 100.0);
expect(view.configuration.logicalConstraints.minHeight, 100.0);
binding.removeRenderView(view);
});
test('semantics actions are performed on the right view', () {
final FakeFlutterView flutterView1 = FakeFlutterView(viewId: 1);
final FakeFlutterView flutterView2 = FakeFlutterView(viewId: 2);
final RenderView renderView1 = RenderView(view: flutterView1);
final RenderView renderView2 = RenderView(view: flutterView2);
final PipelineOwnerSpy owner1 = PipelineOwnerSpy()
..rootNode = renderView1;
final PipelineOwnerSpy owner2 = PipelineOwnerSpy()
..rootNode = renderView2;
binding.addRenderView(renderView1);
binding.addRenderView(renderView2);
binding.performSemanticsAction(
const SemanticsActionEvent(type: SemanticsAction.copy, viewId: 1, nodeId: 11),
);
expect(owner1.semanticsOwner.performedActions.single, (11, SemanticsAction.copy, null));
expect(owner2.semanticsOwner.performedActions, isEmpty);
owner1.semanticsOwner.performedActions.clear();
binding.performSemanticsAction(
const SemanticsActionEvent(type: SemanticsAction.tap, viewId: 2, nodeId: 22),
);
expect(owner1.semanticsOwner.performedActions, isEmpty);
expect(owner2.semanticsOwner.performedActions.single, (22, SemanticsAction.tap, null));
owner2.semanticsOwner.performedActions.clear();
binding.performSemanticsAction(
const SemanticsActionEvent(type: SemanticsAction.tap, viewId: 3, nodeId: 22),
);
expect(owner1.semanticsOwner.performedActions, isEmpty);
expect(owner2.semanticsOwner.performedActions, isEmpty);
binding.removeRenderView(renderView1);
binding.removeRenderView(renderView2);
});
test('all registered renderviews are asked to composite frame', () {
final FakeFlutterView flutterView1 = FakeFlutterView(viewId: 1);
final FakeFlutterView flutterView2 = FakeFlutterView(viewId: 2);
final RenderView renderView1 = RenderView(view: flutterView1);
final RenderView renderView2 = RenderView(view: flutterView2);
final PipelineOwner owner1 = PipelineOwner()..rootNode = renderView1;
final PipelineOwner owner2 = PipelineOwner()..rootNode = renderView2;
binding.rootPipelineOwner.adoptChild(owner1);
binding.rootPipelineOwner.adoptChild(owner2);
binding.addRenderView(renderView1);
binding.addRenderView(renderView2);
renderView1.prepareInitialFrame();
renderView2.prepareInitialFrame();
expect(flutterView1.renderedScenes, isEmpty);
expect(flutterView2.renderedScenes, isEmpty);
binding.handleBeginFrame(Duration.zero);
binding.handleDrawFrame();
expect(flutterView1.renderedScenes, hasLength(1));
expect(flutterView2.renderedScenes, hasLength(1));
binding.removeRenderView(renderView1);
binding.handleBeginFrame(Duration.zero);
binding.handleDrawFrame();
expect(flutterView1.renderedScenes, hasLength(1));
expect(flutterView2.renderedScenes, hasLength(2));
binding.removeRenderView(renderView2);
binding.handleBeginFrame(Duration.zero);
binding.handleDrawFrame();
expect(flutterView1.renderedScenes, hasLength(1));
expect(flutterView2.renderedScenes, hasLength(2));
});
test('hit-testing reaches the right view', () {
final FakeFlutterView flutterView1 = FakeFlutterView(viewId: 1);
final FakeFlutterView flutterView2 = FakeFlutterView(viewId: 2);
final RenderView renderView1 = RenderView(view: flutterView1);
final RenderView renderView2 = RenderView(view: flutterView2);
binding.addRenderView(renderView1);
binding.addRenderView(renderView2);
HitTestResult result = HitTestResult();
binding.hitTestInView(result, Offset.zero, 1);
expect(result.path, hasLength(2));
expect(result.path.first.target, renderView1);
expect(result.path.last.target, binding);
result = HitTestResult();
binding.hitTestInView(result, Offset.zero, 2);
expect(result.path, hasLength(2));
expect(result.path.first.target, renderView2);
expect(result.path.last.target, binding);
result = HitTestResult();
binding.hitTestInView(result, Offset.zero, 3);
expect(result.path.single.target, binding);
binding.removeRenderView(renderView1);
binding.removeRenderView(renderView2);
});
}
class FakeFlutterView extends Fake implements FlutterView {
FakeFlutterView({
this.viewId = 100,
this.devicePixelRatio = 2.5,
this.physicalSize = const Size(400,600),
this.padding = FakeViewPadding.zero,
});
@override
final int viewId;
@override
double devicePixelRatio;
@override
Size physicalSize;
@override
ViewConstraints get physicalConstraints => ViewConstraints.tight(physicalSize);
@override
ViewPadding padding;
List<Scene> renderedScenes = <Scene>[];
@override
void render(Scene scene, {Size? size}) {
renderedScenes.add(scene);
}
}
class PipelineOwnerSpy extends PipelineOwner {
@override
final SemanticsOwnerSpy semanticsOwner = SemanticsOwnerSpy();
}
class SemanticsOwnerSpy extends Fake implements SemanticsOwner {
final List<(int, SemanticsAction, Object?)> performedActions = <(int, SemanticsAction, Object?)>[];
@override
void performAction(int id, SemanticsAction action, [ Object? args ]) {
performedActions.add((id, action, args));
}
}