You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[iPhone Duo] Make sure PlatformDispatcher.instance.displays is correct when open/close iPhone Duo #195177
When we open/close iPhone Duo, the PlatformDispatcher.instance.displays always shows the outer display's dimension.
Refer to this dummy project to print out the display size: #195177 (comment)
Proposal
This is because in the engine, we always pass in UIScreen.main, which refers to the outer display. When running on inner display, PlatformDispatcher.instance.displays should give the correct display dimension.
here's a dummy project used to get the display dimension (with help from gemini)
import 'dart:async';
import 'dart:ui' as ui;
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
// Goal: prove that app code can read display information (`PlatformDispatcher.displays`,
// `FlutterView.display`) at a point where the engine cannot yet know which screen the Flutter view
// will be on. So the engine must send some non-zero value before the view is in a window, i.e. the
// `UIScreen.mainScreen` fallback in `-[FlutterEngine updateDisplays]` is still needed (sending
// `.zero` would give these reads a wrong value).
//
// How: Dart reads the display synchronously, then asks native (the "display_probe" channel) whether
// the engine's view controller is in a window. The native answer is computed strictly *after* the
// Dart read, and a view only goes from "not in a window" to "in a window", so if native says there
// is no window, there was no window (and so no screen to ask) at the time of the read either.
//
// Two engines are exercised in one run:
// - "implicit": the storyboard FlutterViewController's engine (`main`).
// - "headless": an engine run in `application:didFinishLaunchingWithOptions:` before any scene
// connects, then attached to a FlutterViewController ~3s after the scene becomes active
// (`headlessMain`).
const MethodChannel _probe = MethodChannel('display_probe');
void main() => _run('implicit');
@pragma('vm:entry-point')
void headlessMain() => _run('headless');
void _log(String tag, String message) {
// ignore: avoid_print
print(
'[DISPLAY-DART $tag @${DateTime.now().millisecondsSinceEpoch}] $message',
);
}
String _describeDisplay() {
final ui.PlatformDispatcher dispatcher = ui.PlatformDispatcher.instance;
final String displays = dispatcher.displays
.map(
(ui.Display d) => 'id=${d.id} size=${d.size} dpr=${d.devicePixelRatio}',
)
.join(', ');
final ui.FlutterView? view = dispatcher.implicitView;
final String viewInfo = view == null
? 'implicitView=null'
: 'implicitView.physicalSize=${view.physicalSize}';
return 'displays=[$displays] $viewInfo';
}
// Whether this engine's view has been seen in a window. The "no window at read time" argument only
// holds until then: afterwards the view can leave its window (e.g. covered by a full-screen modal),
// and the engine keeps the last known display information.
bool _sawWindow = false;
Future<void> _probeAndReport(String tag, String when, String readValue) async {
Map<Object?, Object?>? state;
try {
state = await _probe.invokeMapMethod<Object?, Object?>('probe');
} on MissingPluginException {
_log(tag, '$when: display_probe channel not registered yet');
return;
}
final bool hasWindow = state?['hasWindow'] == true;
_log(tag, '$when read: $readValue');
_log(tag, '$when native state (computed after the read): $state');
final String verdict;
if (hasWindow) {
_sawWindow = true;
verdict =
'INCONCLUSIVE: the view was in a window by the time native answered.';
} else if (_sawWindow) {
verdict =
'N/A: the view left its window; the value is the last known display.';
} else {
verdict =
'PROOF: app code read display info before the engine\'s view was ever in a '
'window, so the value can only have come from the fallback.';
}
_log(tag, '$when -> $verdict');
}
void _run(String tag) {
// Read before anything else, as early as app code possibly can.
final String earliestRead = _describeDisplay();
WidgetsFlutterBinding.ensureInitialized();
unawaited(_probeAndReport(tag, 'main() first line', earliestRead));
// Log whenever the display information changes (e.g. the engine re-sends it once the view is on
// a screen that differs from the fallback).
String lastDisplay = earliestRead;
final ui.VoidCallback? bindingHandler =
ui.PlatformDispatcher.instance.onMetricsChanged;
ui.PlatformDispatcher.instance.onMetricsChanged = () {
bindingHandler?.call();
final String display = _describeDisplay();
if (display != lastDisplay) {
lastDisplay = display;
unawaited(_probeAndReport(tag, 'onMetricsChanged', display));
// Give the framework time to relayout and send the new semantics tree.
_scheduleA11yProbe(tag, 'after metrics change');
}
};
// Keep sampling for a while so the headless engine's pre-attach and post-attach phases both show
// up in the logs.
int tick = 0;
Timer.periodic(const Duration(seconds: 1), (Timer timer) {
tick++;
unawaited(_probeAndReport(tag, 't+${tick}s', _describeDisplay()));
if (tick >= 6) {
timer.cancel();
}
});
_scheduleA11yProbe(tag, 'startup');
runApp(_LogApp(tag));
}
// Accessibility check for the `SemanticsObjectContainer.accessibilityFrame` change. Only the
// implicit engine registers the native side (see `A11yProbe` in SceneDelegate.swift).
const MethodChannel _a11yProbe = MethodChannel('a11y_probe');
Timer? _a11yProbeTimer;
void _scheduleA11yProbe(String tag, String when) {
if (tag != 'implicit') {
return;
}
_a11yProbeTimer?.cancel();
_a11yProbeTimer = Timer(
const Duration(seconds: 2),
() => unawaited(_probeA11y(tag, when)),
);
}
Future<void> _probeA11y(String tag, String when) async {
Map<Object?, Object?>? state;
try {
state = await _a11yProbe.invokeMapMethod<Object?, Object?>('a11y');
} on MissingPluginException {
_log(tag, 'a11y $when: a11y_probe channel not registered');
return;
}
if (state == null || state['error'] != null) {
_log(tag, 'a11y $when: ${state?['error']}');
return;
}
final List<Object?> outside =
(state['leavesOutsideContainer'] as List<Object?>?) ?? <Object?>[];
_log(
tag,
'a11y $when: windowScreenBounds=${state['windowScreenBounds']} '
'mainScreenBounds=${state['mainScreenBounds']}',
);
_log(tag, 'a11y $when: containerFrames=${state['containerFrames']}');
_log(tag, 'a11y $when: leaves=${state['leaves']}');
_log(
tag,
outside.isEmpty
? 'a11y $when -> PASS: every labeled element is inside its container frame.'
: 'a11y $when -> FAIL: elements outside their container frame (Voice Control may not '
'label them): $outside',
);
}
class _LogApp extends StatefulWidget {
const _LogApp(this.tag);
final String tag;
@override
State<_LogApp> createState() => _LogAppState();
}
class _LogAppState extends State<_LogApp> {
// Always shown. The overlay child is placed far from its anchor (like the OverlayPortal case that
// `SemanticsObjectContainer.accessibilityFrame` was written for), in the bottom-right corner so
// that it is outside the outer display's bounds when the view is on the larger inner display.
final OverlayPortalController _overlayController = OverlayPortalController()
..show();
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: ColoredBox(
color: const Color(0xFFFFFFFF),
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) => Stack(
children: <Widget>[
Center(
child: Text(
'Engine: ${widget.tag}\nDisplay verification: see console logs',
textAlign: TextAlign.center,
style: const TextStyle(
color: Color(0xFF000000),
fontSize: 18,
),
),
),
Positioned(
left: 16,
top: 60,
child: OverlayPortal(
controller: _overlayController,
overlayChildBuilder: (BuildContext context) =>
const Positioned(
right: 16,
bottom: 40,
child: _Button(
label: 'Overlay button bottom-right',
),
),
child: const _Button(label: 'Anchor button top-left'),
),
),
],
),
),
],
),
),
);
}
}
class _Button extends StatelessWidget {
const _Button({required this.label});
final String label;
@override
Widget build(BuildContext context) {
return Semantics(
button: true,
label: label,
onTap: () {},
excludeSemantics: true,
child: ColoredBox(
color: const Color(0xFF2196F3),
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(
label,
style: const TextStyle(color: Color(0xFFFFFFFF), fontSize: 16),
),
),
),
);
}
}
Activity
hellohuanlin commentedon Oct 23, 2026
here's a dummy project used to get the display dimension (with help from gemini)
[UIScreen mainScreen]in iOS 16 #125496[iPhone Duo] Use the correct UIScreen to report the display size and …
[iPhone Duo] Use the correct UIScreen to report the display size and …
[CP-beta][iPhone Duo] Use the correct UIScreen to report the display …
[ios] Update the screen in didUpdateEffectiveGeometry (flutter#195508)