Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit ac59aa3

Browse files
authored
[web] Handle resizes at the view level (#48892)
Currently, in multi-view mode, when the user resizes the window, Flutter views don't respond to the resize. This is because `FlutterViewEmbedder` is the one responding to resize events. But `FlutterViewEmbedder` only works with the implicit view, so multi views didn't respond to resize events. This PR moves the responsibility of responding to resize events from `FlutterViewEmbedder` to `EngineFlutterView`. Also, tests. Part of flutter/flutter#134443
1 parent 9039ac7 commit ac59aa3

File tree

3 files changed

+141
-91
lines changed

3 files changed

+141
-91
lines changed

lib/web_ui/lib/src/engine/embedder.dart

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,10 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5-
import 'package:ui/ui.dart' as ui;
6-
75
import '../engine.dart' show buildMode, renderer;
86
import 'browser_detection.dart';
97
import 'configuration.dart';
108
import 'dom.dart';
11-
import 'platform_dispatcher.dart';
12-
import 'text_editing/text_editing.dart';
13-
import 'view_embedder/style_manager.dart';
149
import 'window.dart';
1510

1611
/// Controls the placement and lifecycle of a Flutter view on the web page.
@@ -41,8 +36,6 @@ class FlutterViewEmbedder {
4136
/// global resources such svg filters and clip paths when using webkit.
4237
DomElement? _resourcesHost;
4338

44-
DomElement get _semanticsHostElement => window.dom.semanticsHost;
45-
4639
DomElement get _flutterViewElement => window.dom.rootElement;
4740
DomShadowRoot get _glassPaneShadow => window.dom.renderingHost;
4841

@@ -64,34 +57,6 @@ class FlutterViewEmbedder {
6457
);
6558

6659
renderer.reset(this);
67-
68-
window.onResize.listen(_metricsDidChange);
69-
}
70-
71-
/// Called immediately after browser window metrics change.
72-
///
73-
/// When there is a text editing going on in mobile devices, do not change
74-
/// the physicalSize, change the [window.viewInsets]. See:
75-
/// https://api.flutter.dev/flutter/dart-ui/FlutterView/viewInsets.html
76-
/// https://api.flutter.dev/flutter/dart-ui/FlutterView/physicalSize.html
77-
///
78-
/// Note: always check for rotations for a mobile device. Update the physical
79-
/// size if the change is caused by a rotation.
80-
void _metricsDidChange(ui.Size? newSize) {
81-
StyleManager.scaleSemanticsHost(
82-
_semanticsHostElement,
83-
window.devicePixelRatio,
84-
);
85-
// TODO(dit): Do not computePhysicalSize twice, https://github.com/flutter/flutter/issues/117036
86-
if (isMobile && !window.isRotation() && textEditing.isEditing) {
87-
window.computeOnScreenKeyboardInsets(true);
88-
EnginePlatformDispatcher.instance.invokeOnMetricsChanged();
89-
} else {
90-
window.computePhysicalSize();
91-
// When physical size changes this value has to be recalculated.
92-
window.computeOnScreenKeyboardInsets(false);
93-
EnginePlatformDispatcher.instance.invokeOnMetricsChanged();
94-
}
9560
}
9661

9762
/// Add an element as a global resource to be referenced by CSS.

lib/web_ui/lib/src/engine/window.dart

Lines changed: 77 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import 'package:ui/ui.dart' as ui;
1010
import 'package:ui/ui_web/src/ui_web.dart' as ui_web;
1111

1212
import '../engine.dart' show DimensionsProvider, registerHotRestartListener, renderer;
13+
import 'browser_detection.dart';
1314
import 'display.dart';
1415
import 'dom.dart';
1516
import 'mouse/context_menu.dart';
@@ -20,9 +21,11 @@ import 'platform_views/message_handler.dart';
2021
import 'pointer_binding.dart';
2122
import 'semantics.dart';
2223
import 'services.dart';
24+
import 'text_editing/text_editing.dart';
2325
import 'util.dart';
2426
import 'view_embedder/dom_manager.dart';
2527
import 'view_embedder/embedding_strategy/embedding_strategy.dart';
28+
import 'view_embedder/style_manager.dart';
2629

2730
typedef _HandleMessageCallBack = Future<bool> Function();
2831

@@ -61,6 +64,7 @@ base class EngineFlutterView implements ui.FlutterView {
6164
// hot restart.
6265
embeddingStrategy.attachViewRoot(dom.rootElement);
6366
pointerBinding = PointerBinding(this);
67+
_resizeSubscription = onResize.listen(_didResize);
6468
registerHotRestartListener(dispose);
6569
}
6670

@@ -78,6 +82,8 @@ base class EngineFlutterView implements ui.FlutterView {
7882
/// Abstracts all the DOM manipulations required to embed a Flutter view in a user-supplied `hostElement`.
7983
final EmbeddingStrategy embeddingStrategy;
8084

85+
late final StreamSubscription<ui.Size?> _resizeSubscription;
86+
8187
final ViewConfiguration _viewConfiguration = const ViewConfiguration();
8288

8389
/// Whether this [EngineFlutterView] has been disposed or not.
@@ -91,6 +97,7 @@ base class EngineFlutterView implements ui.FlutterView {
9197
return;
9298
}
9399
isDisposed = true;
100+
_resizeSubscription.cancel();
94101
dimensionsProvider.close();
95102
pointerBinding.dispose();
96103
dom.rootElement.remove();
@@ -136,41 +143,32 @@ base class EngineFlutterView implements ui.FlutterView {
136143

137144
@override
138145
ui.Size get physicalSize {
139-
if (_physicalSize == null) {
140-
computePhysicalSize();
141-
}
142-
assert(_physicalSize != null);
143-
return _physicalSize!;
146+
return _physicalSize ??= _computePhysicalSize();
144147
}
145148

146149
/// Lazily populated and cleared at the end of the frame.
147150
ui.Size? _physicalSize;
148151

149152
ui.Size? debugPhysicalSizeOverride;
150153

151-
/// Computes the physical size of the screen from [domWindow].
154+
/// Computes the physical size of the view.
152155
///
153156
/// This function is expensive. It triggers browser layout if there are
154157
/// pending DOM writes.
155-
void computePhysicalSize() {
156-
bool override = false;
158+
ui.Size _computePhysicalSize() {
159+
ui.Size? physicalSizeOverride;
157160

158161
assert(() {
159-
if (debugPhysicalSizeOverride != null) {
160-
_physicalSize = debugPhysicalSizeOverride;
161-
override = true;
162-
}
162+
physicalSizeOverride = debugPhysicalSizeOverride;
163163
return true;
164164
}());
165165

166-
if (!override) {
167-
_physicalSize = dimensionsProvider.computePhysicalSize();
168-
}
166+
return physicalSizeOverride ?? dimensionsProvider.computePhysicalSize();
169167
}
170168

171169
/// Forces the view to recompute its physical size. Useful for tests.
172170
void debugForceResize() {
173-
computePhysicalSize();
171+
_physicalSize = _computePhysicalSize();
174172
}
175173

176174
@override
@@ -202,6 +200,69 @@ base class EngineFlutterView implements ui.FlutterView {
202200
final DimensionsProvider dimensionsProvider;
203201

204202
Stream<ui.Size?> get onResize => dimensionsProvider.onResize;
203+
204+
/// Called immediately after the view has been resized.
205+
///
206+
/// When there is a text editing going on in mobile devices, do not change
207+
/// the physicalSize, change the [window.viewInsets]. See:
208+
/// https://api.flutter.dev/flutter/dart-ui/FlutterView/viewInsets.html
209+
/// https://api.flutter.dev/flutter/dart-ui/FlutterView/physicalSize.html
210+
///
211+
/// Note: always check for rotations for a mobile device. Update the physical
212+
/// size if the change is caused by a rotation.
213+
void _didResize(ui.Size? newSize) {
214+
StyleManager.scaleSemanticsHost(dom.semanticsHost, devicePixelRatio);
215+
final ui.Size newPhysicalSize = _computePhysicalSize();
216+
final bool isEditingOnMobile =
217+
isMobile && !_isRotation(newPhysicalSize) && textEditing.isEditing;
218+
if (isEditingOnMobile) {
219+
_computeOnScreenKeyboardInsets(true);
220+
} else {
221+
_physicalSize = newPhysicalSize;
222+
// When physical size changes this value has to be recalculated.
223+
_computeOnScreenKeyboardInsets(false);
224+
}
225+
platformDispatcher.invokeOnMetricsChanged();
226+
}
227+
228+
/// Uses the previous physical size and current innerHeight/innerWidth
229+
/// values to decide if a device is rotating.
230+
///
231+
/// During a rotation the height and width values will (almost) swap place.
232+
/// Values can slightly differ due to space occupied by the browser header.
233+
/// For example the following values are collected for Pixel 3 rotation:
234+
///
235+
/// height: 658 width: 393
236+
/// new height: 313 new width: 738
237+
///
238+
/// The following values are from a changed caused by virtual keyboard.
239+
///
240+
/// height: 658 width: 393
241+
/// height: 368 width: 393
242+
bool _isRotation(ui.Size newPhysicalSize) {
243+
// This method compares the new dimensions with the previous ones.
244+
// Return false if the previous dimensions are not set.
245+
if (_physicalSize != null) {
246+
// First confirm both height and width are effected.
247+
if (_physicalSize!.height != newPhysicalSize.height && _physicalSize!.width != newPhysicalSize.width) {
248+
// If prior to rotation height is bigger than width it should be the
249+
// opposite after the rotation and vice versa.
250+
if ((_physicalSize!.height > _physicalSize!.width && newPhysicalSize.height < newPhysicalSize.width) ||
251+
(_physicalSize!.width > _physicalSize!.height && newPhysicalSize.width < newPhysicalSize.height)) {
252+
// Rotation detected
253+
return true;
254+
}
255+
}
256+
}
257+
return false;
258+
}
259+
260+
void _computeOnScreenKeyboardInsets(bool isEditingOnMobile) {
261+
_viewInsets = dimensionsProvider.computeKeyboardInsets(
262+
_physicalSize!.height,
263+
isEditingOnMobile,
264+
);
265+
}
205266
}
206267

207268
final class _EngineFlutterViewImpl extends EngineFlutterView {
@@ -543,46 +604,6 @@ final class EngineFlutterWindow extends EngineFlutterView implements ui.Singleto
543604
display.debugOverrideDevicePixelRatio(value);
544605
}
545606

546-
void computeOnScreenKeyboardInsets(bool isEditingOnMobile) {
547-
_viewInsets = dimensionsProvider.computeKeyboardInsets(
548-
_physicalSize!.height,
549-
isEditingOnMobile,
550-
);
551-
}
552-
553-
/// Uses the previous physical size and current innerHeight/innerWidth
554-
/// values to decide if a device is rotating.
555-
///
556-
/// During a rotation the height and width values will (almost) swap place.
557-
/// Values can slightly differ due to space occupied by the browser header.
558-
/// For example the following values are collected for Pixel 3 rotation:
559-
///
560-
/// height: 658 width: 393
561-
/// new height: 313 new width: 738
562-
///
563-
/// The following values are from a changed caused by virtual keyboard.
564-
///
565-
/// height: 658 width: 393
566-
/// height: 368 width: 393
567-
bool isRotation() {
568-
// This method compares the new dimensions with the previous ones.
569-
// Return false if the previous dimensions are not set.
570-
if (_physicalSize != null) {
571-
final ui.Size current = dimensionsProvider.computePhysicalSize();
572-
// First confirm both height and width are effected.
573-
if (_physicalSize!.height != current.height && _physicalSize!.width != current.width) {
574-
// If prior to rotation height is bigger than width it should be the
575-
// opposite after the rotation and vice versa.
576-
if ((_physicalSize!.height > _physicalSize!.width && current.height < current.width) ||
577-
(_physicalSize!.width > _physicalSize!.height && current.width < current.height)) {
578-
// Rotation detected
579-
return true;
580-
}
581-
}
582-
}
583-
return false;
584-
}
585-
586607
// TODO(mdebbar): Deprecate this and remove it.
587608
// https://github.com/flutter/flutter/issues/127395
588609
ui.Size? get webOnlyDebugPhysicalSizeOverride {

lib/web_ui/test/engine/window_test.dart

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,4 +536,68 @@ Future<void> testMain() async {
536536
throwsAssertionError,
537537
);
538538
});
539+
540+
group('resizing', () {
541+
late DomHTMLDivElement host;
542+
late EngineFlutterView view;
543+
late int metricsChangedCount;
544+
545+
setUp(() async {
546+
EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(2.5);
547+
host = createDomHTMLDivElement();
548+
view = EngineFlutterView(EnginePlatformDispatcher.instance, host);
549+
550+
host.style
551+
..width = '10px'
552+
..height = '10px';
553+
domDocument.body!.append(host);
554+
// Let the DOM settle before starting the test, so we don't get the first
555+
// 10,10 Size in the test. Otherwise, the ResizeObserver may trigger
556+
// unexpectedly after the test has started, and break our "first" result.
557+
await Future<void>.delayed(const Duration(milliseconds: 250));
558+
559+
metricsChangedCount = 0;
560+
view.platformDispatcher.onMetricsChanged = () {
561+
metricsChangedCount++;
562+
};
563+
});
564+
565+
tearDown(() {
566+
view.dispose();
567+
host.remove();
568+
EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(null);
569+
view.platformDispatcher.onMetricsChanged = null;
570+
});
571+
572+
test('listens to resize', () async {
573+
// Initial size is 10x10, with a 2.5 dpr, is equal to 25x25 physical pixels.
574+
expect(view.physicalSize, const ui.Size(25.0, 25.0));
575+
expect(metricsChangedCount, 0);
576+
577+
// Resize the host to 20x20.
578+
host.style
579+
..width = '20px'
580+
..height = '20px';
581+
await view.onResize.first;
582+
expect(view.physicalSize, const ui.Size(50.0, 50.0));
583+
expect(metricsChangedCount, 1);
584+
});
585+
586+
test('maintains debugPhysicalSizeOverride', () async {
587+
// Initial size is 10x10, with a 2.5 dpr, is equal to 25x25 physical pixels.
588+
expect(view.physicalSize, const ui.Size(25.0, 25.0));
589+
590+
view.debugPhysicalSizeOverride = const ui.Size(100.0, 100.0);
591+
view.debugForceResize();
592+
expect(view.physicalSize, const ui.Size(100.0, 100.0));
593+
594+
// Resize the host to 20x20.
595+
host.style
596+
..width = '20px'
597+
..height = '20px';
598+
await view.onResize.first;
599+
// The view should maintain the debugPhysicalSizeOverride.
600+
expect(view.physicalSize, const ui.Size(100.0, 100.0));
601+
});
602+
});
539603
}

0 commit comments

Comments
 (0)