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

500 lines
20 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:io' show Platform;
import 'dart:ui' as ui show FlutterView, Scene, SceneBuilder, SemanticsUpdate;
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'binding.dart';
import 'box.dart';
import 'debug.dart';
import 'layer.dart';
import 'object.dart';
/// The layout constraints for the root render object.
@immutable
class ViewConfiguration {
/// Creates a view configuration.
///
/// 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.physicalConstraints = const BoxConstraints(maxWidth: 0, maxHeight: 0),
this.logicalConstraints = const BoxConstraints(maxWidth: 0, maxHeight: 0),
this.devicePixelRatio = 1.0,
});
/// 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;
/// Creates a transformation matrix that applies the [devicePixelRatio].
///
/// The matrix translates points from the local coordinate system of the
/// app (in logical pixels) to the global coordinate system of the
/// [FlutterView] (in physical pixels).
Matrix4 toMatrix() {
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.logicalConstraints == logicalConstraints
&& other.physicalConstraints == physicalConstraints
&& other.devicePixelRatio == devicePixelRatio;
}
@override
int get hashCode => Object.hash(logicalConstraints, physicalConstraints, devicePixelRatio);
@override
String toString() => '$logicalConstraints at ${debugFormatDouble(devicePixelRatio)}x';
}
/// The root of the render tree.
///
/// The view represents the total output surface of the render tree and handles
/// bootstrapping the rendering pipeline. The view has a unique child
/// [RenderBox], which is required to fill the entire output surface.
class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox> {
/// Creates the root of the render tree.
///
/// Typically created by the binding (e.g., [RendererBinding]).
///
/// Providing a [configuration] is optional, but a configuration must be set
/// before calling [prepareInitialFrame]. This decouples creating the
/// [RenderView] object from configuring it. Typically, the object is created
/// by the [View] widget and configured by the [RendererBinding] when the
/// [RenderView] is registered with it by the [View] widget.
RenderView({
RenderBox? child,
ViewConfiguration? configuration,
required ui.FlutterView view,
}) : _view = view {
if (configuration != null) {
this.configuration = configuration;
}
this.child = child;
}
/// The current layout size of the view.
Size get size => _size;
Size _size = Size.zero;
/// The constraints used for the root layout.
///
/// Typically, this configuration is set by the [RendererBinding], when the
/// [RenderView] is registered with it. It will also update the configuration
/// if necessary. Therefore, if used in conjunction with the [RendererBinding]
/// this property must not be set manually as the [RendererBinding] will just
/// override it.
///
/// For tests that want to change the size of the view, set
/// [TestFlutterView.physicalSize] on the appropriate [TestFlutterView]
/// (typically [WidgetTester.view]) instead of setting a configuration
/// directly on the [RenderView].
ViewConfiguration get configuration => _configuration!;
ViewConfiguration? _configuration;
set configuration(ViewConfiguration value) {
if (_configuration == value) {
return;
}
final ViewConfiguration? oldConfiguration = _configuration;
_configuration = value;
if (_rootTransform == null) {
// [prepareInitialFrame] has not been called yet, nothing to do for now.
return;
}
if (oldConfiguration?.toMatrix() != configuration.toMatrix()) {
replaceRootLayer(_updateMatricesAndCreateNewRootLayer());
}
assert(_rootTransform != null);
markNeedsLayout();
}
/// 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;
/// Whether Flutter should automatically compute the desired system UI.
///
/// When this setting is enabled, Flutter will hit-test the layer tree at the
/// top and bottom of the screen on each frame looking for an
/// [AnnotatedRegionLayer] with an instance of a [SystemUiOverlayStyle]. The
/// hit-test result from the top of the screen provides the status bar settings
/// and the hit-test result from the bottom of the screen provides the system
/// nav bar settings.
///
/// If there is no [AnnotatedRegionLayer] on the bottom, the hit-test result
/// from the top provides the system nav bar settings. If there is no
/// [AnnotatedRegionLayer] on the top, the hit-test result from the bottom
/// provides the system status bar settings.
///
/// Setting this to false does not cause previous automatic adjustments to be
/// reset, nor does setting it to true cause the app to update immediately.
///
/// If you want to imperatively set the system ui style instead, it is
/// recommended that [automaticSystemUiAdjustment] is set to false.
///
/// See also:
///
/// * [AnnotatedRegion], for placing [SystemUiOverlayStyle] in the layer tree.
/// * [SystemChrome.setSystemUIOverlayStyle], for imperatively setting the system ui style.
bool automaticSystemUiAdjustment = true;
/// Bootstrap the rendering pipeline by preparing the first frame.
///
/// This should only be called once, and must be called before changing
/// [configuration]. It is typically called immediately after calling the
/// constructor.
///
/// This does not actually schedule the first frame. Call
/// [PipelineOwner.requestVisualUpdate] on [owner] to do that.
void prepareInitialFrame() {
assert(owner != null);
assert(_rootTransform == null);
scheduleInitialLayout();
scheduleInitialPaint(_updateMatricesAndCreateNewRootLayer());
assert(_rootTransform != null);
}
Matrix4? _rootTransform;
TransformLayer _updateMatricesAndCreateNewRootLayer() {
_rootTransform = configuration.toMatrix();
final TransformLayer rootLayer = TransformLayer(transform: _rootTransform);
rootLayer.attach(this);
assert(_rootTransform != null);
return rootLayer;
}
// We never call layout() on this class, so this should never get
// checked. (This class is laid out using scheduleInitialLayout().)
@override
void debugAssertDoesMeetConstraints() { assert(false); }
@override
void performResize() {
assert(false);
}
@override
void performLayout() {
assert(_rootTransform != null);
final bool sizedByChild = !constraints.isTight;
if (child != null) {
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.
///
/// Returns true if the given point is contained in this render object or one
/// of its descendants. Adds any render objects that contain the point to the
/// given hit test result.
///
/// The [position] argument is in the coordinate system of the render view,
/// which is to say, in logical pixels. This is not necessarily the same
/// coordinate system as that expected by the root [Layer], which will
/// normally be in physical (device) pixels.
bool hitTest(HitTestResult result, { required Offset position }) {
if (child != null) {
child!.hitTest(BoxHitTestResult.wrap(result), position: position);
}
result.add(HitTestEntry(this));
return true;
}
@override
bool get isRepaintBoundary => true;
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
context.paintChild(child!, offset);
}
assert(() {
final List<DebugPaintCallback> localCallbacks = _debugPaintCallbacks.toList();
for (final DebugPaintCallback paintCallback in localCallbacks) {
if (_debugPaintCallbacks.contains(paintCallback)) {
paintCallback(context, offset, this);
}
}
return true;
}());
}
@override
void applyPaintTransform(RenderBox child, Matrix4 transform) {
assert(_rootTransform != null);
transform.multiply(_rootTransform!);
super.applyPaintTransform(child, transform);
}
/// Uploads the composited layer tree to the engine.
///
/// Actually causes the output of the rendering pipeline to appear on screen.
void compositeFrame() {
if (!kReleaseMode) {
FlutterTimeline.startSync('COMPOSITING');
}
try {
final ui.SceneBuilder builder = ui.SceneBuilder();
final ui.Scene scene = layer!.buildScene(builder);
if (automaticSystemUiAdjustment) {
_updateSystemChrome();
}
assert(configuration.logicalConstraints.isSatisfiedBy(size));
_view.render(scene, size: configuration.toPhysicalSize(size));
scene.dispose();
assert(() {
if (debugRepaintRainbowEnabled || debugRepaintTextRainbowEnabled) {
debugCurrentRepaintColor = debugCurrentRepaintColor.withHue((debugCurrentRepaintColor.hue + 2.0) % 360.0);
}
return true;
}());
} finally {
if (!kReleaseMode) {
FlutterTimeline.finishSync();
}
}
}
/// Sends the provided [SemanticsUpdate] to the [FlutterView] associated with
/// this [RenderView].
///
/// A [SemanticsUpdate] is produced by a [SemanticsOwner] during the
/// [EnginePhase.flushSemantics] phase.
void updateSemantics(ui.SemanticsUpdate update) {
_view.updateSemantics(update);
}
void _updateSystemChrome() {
// Take overlay style from the place where a system status bar and system
// navigation bar are placed to update system style overlay.
// The center of the system navigation bar and the center of the status bar
// are used to get SystemUiOverlayStyle's to update system overlay appearance.
//
// Horizontal center of the screen
// V
// ++++++++++++++++++++++++++
// | |
// | System status bar | <- Vertical center of the status bar
// | |
// ++++++++++++++++++++++++++
// | |
// | Content |
// ~ ~
// | |
// ++++++++++++++++++++++++++
// | |
// | System navigation bar | <- Vertical center of the navigation bar
// | |
// ++++++++++++++++++++++++++ <- bounds.bottom
final Rect bounds = paintBounds;
// Center of the status bar
final Offset top = Offset(
// Horizontal center of the screen
bounds.center.dx,
// The vertical center of the system status bar. The system status bar
// height is kept as top window padding.
_view.padding.top / 2.0,
);
// Center of the navigation bar
final Offset bottom = Offset(
// Horizontal center of the screen
bounds.center.dx,
// Vertical center of the system navigation bar. The system navigation bar
// height is kept as bottom window padding. The "1" needs to be subtracted
// from the bottom because available pixels are in (0..bottom) range.
// I.e. for a device with 1920 height, bound.bottom is 1920, but the most
// bottom drawn pixel is at 1919 position.
bounds.bottom - 1.0 - _view.padding.bottom / 2.0,
);
final SystemUiOverlayStyle? upperOverlayStyle = layer!.find<SystemUiOverlayStyle>(top);
// Only android has a customizable system navigation bar.
SystemUiOverlayStyle? lowerOverlayStyle;
switch (defaultTargetPlatform) {
case TargetPlatform.android:
lowerOverlayStyle = layer!.find<SystemUiOverlayStyle>(bottom);
case TargetPlatform.fuchsia:
case TargetPlatform.iOS:
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
break;
}
// If there are no overlay style in the UI don't bother updating.
if (upperOverlayStyle == null && lowerOverlayStyle == null) {
return;
}
// If both are not null, the upper provides the status bar properties and the lower provides
// the system navigation bar properties. This is done for advanced use cases where a widget
// on the top (for instance an app bar) will create an annotated region to set the status bar
// style and another widget on the bottom will create an annotated region to set the system
// navigation bar style.
if (upperOverlayStyle != null && lowerOverlayStyle != null) {
final SystemUiOverlayStyle overlayStyle = SystemUiOverlayStyle(
statusBarBrightness: upperOverlayStyle.statusBarBrightness,
statusBarIconBrightness: upperOverlayStyle.statusBarIconBrightness,
statusBarColor: upperOverlayStyle.statusBarColor,
systemStatusBarContrastEnforced: upperOverlayStyle.systemStatusBarContrastEnforced,
systemNavigationBarColor: lowerOverlayStyle.systemNavigationBarColor,
systemNavigationBarDividerColor: lowerOverlayStyle.systemNavigationBarDividerColor,
systemNavigationBarIconBrightness: lowerOverlayStyle.systemNavigationBarIconBrightness,
systemNavigationBarContrastEnforced: lowerOverlayStyle.systemNavigationBarContrastEnforced,
);
SystemChrome.setSystemUIOverlayStyle(overlayStyle);
return;
}
// If only one of the upper or the lower overlay style is not null, it provides all properties.
// This is done for developer convenience as it allows setting both status bar style and
// navigation bar style using only one annotated region layer (for instance the one
// automatically created by an [AppBar]).
final bool isAndroid = defaultTargetPlatform == TargetPlatform.android;
final SystemUiOverlayStyle definedOverlayStyle = (upperOverlayStyle ?? lowerOverlayStyle)!;
final SystemUiOverlayStyle overlayStyle = SystemUiOverlayStyle(
statusBarBrightness: definedOverlayStyle.statusBarBrightness,
statusBarIconBrightness: definedOverlayStyle.statusBarIconBrightness,
statusBarColor: definedOverlayStyle.statusBarColor,
systemStatusBarContrastEnforced: definedOverlayStyle.systemStatusBarContrastEnforced,
systemNavigationBarColor: isAndroid ? definedOverlayStyle.systemNavigationBarColor : null,
systemNavigationBarDividerColor: isAndroid ? definedOverlayStyle.systemNavigationBarDividerColor : null,
systemNavigationBarIconBrightness: isAndroid ? definedOverlayStyle.systemNavigationBarIconBrightness : null,
systemNavigationBarContrastEnforced: isAndroid ? definedOverlayStyle.systemNavigationBarContrastEnforced : null,
);
SystemChrome.setSystemUIOverlayStyle(overlayStyle);
}
@override
Rect get paintBounds => Offset.zero & (size * configuration.devicePixelRatio);
@override
Rect get semanticBounds {
assert(_rootTransform != null);
return MatrixUtils.transformRect(_rootTransform!, Offset.zero & size);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
// call to ${super.debugFillProperties(description)} is omitted because the
// root superclasses don't include any interesting information for this
// class
assert(() {
properties.add(DiagnosticsNode.message('debug mode enabled - ${kIsWeb ? 'Web' : Platform.operatingSystem}'));
return true;
}());
properties.add(DiagnosticsProperty<Size>('view size', _view.physicalSize, tooltip: 'in physical pixels'));
properties.add(DoubleProperty('device pixel ratio', _view.devicePixelRatio, tooltip: 'physical pixels per logical pixel'));
properties.add(DiagnosticsProperty<ViewConfiguration>('configuration', configuration, tooltip: 'in logical pixels'));
if (_view.platformDispatcher.semanticsEnabled) {
properties.add(DiagnosticsNode.message('semantics enabled'));
}
}
static final List<DebugPaintCallback> _debugPaintCallbacks = <DebugPaintCallback>[];
/// Registers a [DebugPaintCallback] that is called every time a [RenderView]
/// repaints in debug mode.
///
/// The callback may paint a debug overlay on top of the content of the
/// [RenderView] provided to the callback. Callbacks are invoked in the
/// order they were registered in.
///
/// Neither registering a callback nor the continued presence of a callback
/// changes how often [RenderView]s are repainted. It is up to the owner of
/// the callback to call [markNeedsPaint] on any [RenderView] for which it
/// wants to update the painted overlay.
///
/// Does nothing in release mode.
static void debugAddPaintCallback(DebugPaintCallback callback) {
assert(() {
_debugPaintCallbacks.add(callback);
return true;
}());
}
/// Removes a callback registered with [debugAddPaintCallback].
///
/// It does not schedule a frame to repaint the [RenderView]s without the
/// overlay painted by the removed callback. It is up to the owner of the
/// callback to call [markNeedsPaint] on the relevant [RenderView]s to
/// repaint them without the overlay.
///
/// Does nothing in release mode.
static void debugRemovePaintCallback(DebugPaintCallback callback) {
assert(() {
_debugPaintCallbacks.remove(callback);
return true;
}());
}
}
/// A callback for painting a debug overlay on top of the provided [RenderView].
///
/// Used by [RenderView.debugAddPaintCallback] and
/// [RenderView.debugRemovePaintCallback].
typedef DebugPaintCallback = void Function(PaintingContext context, Offset offset, RenderView renderView);