1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:io' show Platform;
6import 'dart:ui' as ui show FlutterView, Scene, SceneBuilder, SemanticsUpdate;
7
8import 'package:flutter/foundation.dart';
9import 'package:flutter/services.dart';
10
11import 'binding.dart';
12import 'box.dart';
13import 'debug.dart';
14import 'layer.dart';
15import 'object.dart';
16
17/// The layout constraints for the root render object.
18@immutable
19class ViewConfiguration {
20 /// Creates a view configuration.
21 ///
22 /// By default, the view has zero [size] and a [devicePixelRatio] of 1.0.
23 const ViewConfiguration({
24 this.size = Size.zero,
25 this.devicePixelRatio = 1.0,
26 });
27
28 /// The size of the output surface.
29 final Size size;
30
31 /// The pixel density of the output surface.
32 final double devicePixelRatio;
33
34 /// Creates a transformation matrix that applies the [devicePixelRatio].
35 ///
36 /// The matrix translates points from the local coordinate system of the
37 /// app (in logical pixels) to the global coordinate system of the
38 /// [FlutterView] (in physical pixels).
39 Matrix4 toMatrix() {
40 return Matrix4.diagonal3Values(devicePixelRatio, devicePixelRatio, 1.0);
41 }
42
43 @override
44 bool operator ==(Object other) {
45 if (other.runtimeType != runtimeType) {
46 return false;
47 }
48 return other is ViewConfiguration
49 && other.size == size
50 && other.devicePixelRatio == devicePixelRatio;
51 }
52
53 @override
54 int get hashCode => Object.hash(size, devicePixelRatio);
55
56 @override
57 String toString() => '$size at ${debugFormatDouble(devicePixelRatio)}x';
58}
59
60/// The root of the render tree.
61///
62/// The view represents the total output surface of the render tree and handles
63/// bootstrapping the rendering pipeline. The view has a unique child
64/// [RenderBox], which is required to fill the entire output surface.
65class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox> {
66 /// Creates the root of the render tree.
67 ///
68 /// Typically created by the binding (e.g., [RendererBinding]).
69 ///
70 /// Providing a [configuration] is optional, but a configuration must be set
71 /// before calling [prepareInitialFrame]. This decouples creating the
72 /// [RenderView] object from configuring it. Typically, the object is created
73 /// by the [View] widget and configured by the [RendererBinding] when the
74 /// [RenderView] is registered with it by the [View] widget.
75 RenderView({
76 RenderBox? child,
77 ViewConfiguration? configuration,
78 required ui.FlutterView view,
79 }) : _configuration = configuration,
80 _view = view {
81 this.child = child;
82 }
83
84 /// The current layout size of the view.
85 Size get size => _size;
86 Size _size = Size.zero;
87
88 /// The constraints used for the root layout.
89 ///
90 /// Typically, this configuration is set by the [RendererBinding], when the
91 /// [RenderView] is registered with it. It will also update the configuration
92 /// if necessary. Therefore, if used in conjunction with the [RendererBinding]
93 /// this property must not be set manually as the [RendererBinding] will just
94 /// override it.
95 ///
96 /// For tests that want to change the size of the view, set
97 /// [TestFlutterView.physicalSize] on the appropriate [TestFlutterView]
98 /// (typically [WidgetTester.view]) instead of setting a configuration
99 /// directly on the [RenderView].
100 ViewConfiguration get configuration => _configuration!;
101 ViewConfiguration? _configuration;
102 set configuration(ViewConfiguration value) {
103 if (_configuration == value) {
104 return;
105 }
106 final ViewConfiguration? oldConfiguration = _configuration;
107 _configuration = value;
108 if (_rootTransform == null) {
109 // [prepareInitialFrame] has not been called yet, nothing to do for now.
110 return;
111 }
112 if (oldConfiguration?.toMatrix() != configuration.toMatrix()) {
113 replaceRootLayer(_updateMatricesAndCreateNewRootLayer());
114 }
115 assert(_rootTransform != null);
116 markNeedsLayout();
117 }
118
119 /// Whether a [configuration] has been set.
120 bool get hasConfiguration => _configuration != null;
121
122 /// The [FlutterView] into which this [RenderView] will render.
123 ui.FlutterView get flutterView => _view;
124 final ui.FlutterView _view;
125
126 /// Whether Flutter should automatically compute the desired system UI.
127 ///
128 /// When this setting is enabled, Flutter will hit-test the layer tree at the
129 /// top and bottom of the screen on each frame looking for an
130 /// [AnnotatedRegionLayer] with an instance of a [SystemUiOverlayStyle]. The
131 /// hit-test result from the top of the screen provides the status bar settings
132 /// and the hit-test result from the bottom of the screen provides the system
133 /// nav bar settings.
134 ///
135 /// If there is no [AnnotatedRegionLayer] on the bottom, the hit-test result
136 /// from the top provides the system nav bar settings. If there is no
137 /// [AnnotatedRegionLayer] on the top, the hit-test result from the bottom
138 /// provides the system status bar settings.
139 ///
140 /// Setting this to false does not cause previous automatic adjustments to be
141 /// reset, nor does setting it to true cause the app to update immediately.
142 ///
143 /// If you want to imperatively set the system ui style instead, it is
144 /// recommended that [automaticSystemUiAdjustment] is set to false.
145 ///
146 /// See also:
147 ///
148 /// * [AnnotatedRegion], for placing [SystemUiOverlayStyle] in the layer tree.
149 /// * [SystemChrome.setSystemUIOverlayStyle], for imperatively setting the system ui style.
150 bool automaticSystemUiAdjustment = true;
151
152 /// Bootstrap the rendering pipeline by preparing the first frame.
153 ///
154 /// This should only be called once, and must be called before changing
155 /// [configuration]. It is typically called immediately after calling the
156 /// constructor.
157 ///
158 /// This does not actually schedule the first frame. Call
159 /// [PipelineOwner.requestVisualUpdate] on [owner] to do that.
160 void prepareInitialFrame() {
161 assert(owner != null);
162 assert(_rootTransform == null);
163 scheduleInitialLayout();
164 scheduleInitialPaint(_updateMatricesAndCreateNewRootLayer());
165 assert(_rootTransform != null);
166 }
167
168 Matrix4? _rootTransform;
169
170 TransformLayer _updateMatricesAndCreateNewRootLayer() {
171 _rootTransform = configuration.toMatrix();
172 final TransformLayer rootLayer = TransformLayer(transform: _rootTransform);
173 rootLayer.attach(this);
174 assert(_rootTransform != null);
175 return rootLayer;
176 }
177
178 // We never call layout() on this class, so this should never get
179 // checked. (This class is laid out using scheduleInitialLayout().)
180 @override
181 void debugAssertDoesMeetConstraints() { assert(false); }
182
183 @override
184 void performResize() {
185 assert(false);
186 }
187
188 @override
189 void performLayout() {
190 assert(_rootTransform != null);
191 _size = configuration.size;
192 assert(_size.isFinite);
193
194 if (child != null) {
195 child!.layout(BoxConstraints.tight(_size));
196 }
197 }
198
199 /// Determines the set of render objects located at the given position.
200 ///
201 /// Returns true if the given point is contained in this render object or one
202 /// of its descendants. Adds any render objects that contain the point to the
203 /// given hit test result.
204 ///
205 /// The [position] argument is in the coordinate system of the render view,
206 /// which is to say, in logical pixels. This is not necessarily the same
207 /// coordinate system as that expected by the root [Layer], which will
208 /// normally be in physical (device) pixels.
209 bool hitTest(HitTestResult result, { required Offset position }) {
210 if (child != null) {
211 child!.hitTest(BoxHitTestResult.wrap(result), position: position);
212 }
213 result.add(HitTestEntry(this));
214 return true;
215 }
216
217 @override
218 bool get isRepaintBoundary => true;
219
220 @override
221 void paint(PaintingContext context, Offset offset) {
222 if (child != null) {
223 context.paintChild(child!, offset);
224 }
225 assert(() {
226 final List<DebugPaintCallback> localCallbacks = _debugPaintCallbacks.toList();
227 for (final DebugPaintCallback paintCallback in localCallbacks) {
228 if (_debugPaintCallbacks.contains(paintCallback)) {
229 paintCallback(context, offset, this);
230 }
231 }
232 return true;
233 }());
234 }
235
236 @override
237 void applyPaintTransform(RenderBox child, Matrix4 transform) {
238 assert(_rootTransform != null);
239 transform.multiply(_rootTransform!);
240 super.applyPaintTransform(child, transform);
241 }
242
243 /// Uploads the composited layer tree to the engine.
244 ///
245 /// Actually causes the output of the rendering pipeline to appear on screen.
246 void compositeFrame() {
247 if (!kReleaseMode) {
248 FlutterTimeline.startSync('COMPOSITING');
249 }
250 try {
251 final ui.SceneBuilder builder = ui.SceneBuilder();
252 final ui.Scene scene = layer!.buildScene(builder);
253 if (automaticSystemUiAdjustment) {
254 _updateSystemChrome();
255 }
256 _view.render(scene);
257 scene.dispose();
258 assert(() {
259 if (debugRepaintRainbowEnabled || debugRepaintTextRainbowEnabled) {
260 debugCurrentRepaintColor = debugCurrentRepaintColor.withHue((debugCurrentRepaintColor.hue + 2.0) % 360.0);
261 }
262 return true;
263 }());
264 } finally {
265 if (!kReleaseMode) {
266 FlutterTimeline.finishSync();
267 }
268 }
269 }
270
271 /// Sends the provided [SemanticsUpdate] to the [FlutterView] associated with
272 /// this [RenderView].
273 ///
274 /// A [SemanticsUpdate] is produced by a [SemanticsOwner] during the
275 /// [EnginePhase.flushSemantics] phase.
276 void updateSemantics(ui.SemanticsUpdate update) {
277 _view.updateSemantics(update);
278 }
279
280 void _updateSystemChrome() {
281 // Take overlay style from the place where a system status bar and system
282 // navigation bar are placed to update system style overlay.
283 // The center of the system navigation bar and the center of the status bar
284 // are used to get SystemUiOverlayStyle's to update system overlay appearance.
285 //
286 // Horizontal center of the screen
287 // V
288 // ++++++++++++++++++++++++++
289 // | |
290 // | System status bar | <- Vertical center of the status bar
291 // | |
292 // ++++++++++++++++++++++++++
293 // | |
294 // | Content |
295 // ~ ~
296 // | |
297 // ++++++++++++++++++++++++++
298 // | |
299 // | System navigation bar | <- Vertical center of the navigation bar
300 // | |
301 // ++++++++++++++++++++++++++ <- bounds.bottom
302 final Rect bounds = paintBounds;
303 // Center of the status bar
304 final Offset top = Offset(
305 // Horizontal center of the screen
306 bounds.center.dx,
307 // The vertical center of the system status bar. The system status bar
308 // height is kept as top window padding.
309 _view.padding.top / 2.0,
310 );
311 // Center of the navigation bar
312 final Offset bottom = Offset(
313 // Horizontal center of the screen
314 bounds.center.dx,
315 // Vertical center of the system navigation bar. The system navigation bar
316 // height is kept as bottom window padding. The "1" needs to be subtracted
317 // from the bottom because available pixels are in (0..bottom) range.
318 // I.e. for a device with 1920 height, bound.bottom is 1920, but the most
319 // bottom drawn pixel is at 1919 position.
320 bounds.bottom - 1.0 - _view.padding.bottom / 2.0,
321 );
322 final SystemUiOverlayStyle? upperOverlayStyle = layer!.find<SystemUiOverlayStyle>(top);
323 // Only android has a customizable system navigation bar.
324 SystemUiOverlayStyle? lowerOverlayStyle;
325 switch (defaultTargetPlatform) {
326 case TargetPlatform.android:
327 lowerOverlayStyle = layer!.find<SystemUiOverlayStyle>(bottom);
328 case TargetPlatform.fuchsia:
329 case TargetPlatform.iOS:
330 case TargetPlatform.linux:
331 case TargetPlatform.macOS:
332 case TargetPlatform.windows:
333 break;
334 }
335 // If there are no overlay style in the UI don't bother updating.
336 if (upperOverlayStyle == null && lowerOverlayStyle == null) {
337 return;
338 }
339
340 // If both are not null, the upper provides the status bar properties and the lower provides
341 // the system navigation bar properties. This is done for advanced use cases where a widget
342 // on the top (for instance an app bar) will create an annotated region to set the status bar
343 // style and another widget on the bottom will create an annotated region to set the system
344 // navigation bar style.
345 if (upperOverlayStyle != null && lowerOverlayStyle != null) {
346 final SystemUiOverlayStyle overlayStyle = SystemUiOverlayStyle(
347 statusBarBrightness: upperOverlayStyle.statusBarBrightness,
348 statusBarIconBrightness: upperOverlayStyle.statusBarIconBrightness,
349 statusBarColor: upperOverlayStyle.statusBarColor,
350 systemStatusBarContrastEnforced: upperOverlayStyle.systemStatusBarContrastEnforced,
351 systemNavigationBarColor: lowerOverlayStyle.systemNavigationBarColor,
352 systemNavigationBarDividerColor: lowerOverlayStyle.systemNavigationBarDividerColor,
353 systemNavigationBarIconBrightness: lowerOverlayStyle.systemNavigationBarIconBrightness,
354 systemNavigationBarContrastEnforced: lowerOverlayStyle.systemNavigationBarContrastEnforced,
355 );
356 SystemChrome.setSystemUIOverlayStyle(overlayStyle);
357 return;
358 }
359 // If only one of the upper or the lower overlay style is not null, it provides all properties.
360 // This is done for developer convenience as it allows setting both status bar style and
361 // navigation bar style using only one annotated region layer (for instance the one
362 // automatically created by an [AppBar]).
363 final bool isAndroid = defaultTargetPlatform == TargetPlatform.android;
364 final SystemUiOverlayStyle definedOverlayStyle = (upperOverlayStyle ?? lowerOverlayStyle)!;
365 final SystemUiOverlayStyle overlayStyle = SystemUiOverlayStyle(
366 statusBarBrightness: definedOverlayStyle.statusBarBrightness,
367 statusBarIconBrightness: definedOverlayStyle.statusBarIconBrightness,
368 statusBarColor: definedOverlayStyle.statusBarColor,
369 systemStatusBarContrastEnforced: definedOverlayStyle.systemStatusBarContrastEnforced,
370 systemNavigationBarColor: isAndroid ? definedOverlayStyle.systemNavigationBarColor : null,
371 systemNavigationBarDividerColor: isAndroid ? definedOverlayStyle.systemNavigationBarDividerColor : null,
372 systemNavigationBarIconBrightness: isAndroid ? definedOverlayStyle.systemNavigationBarIconBrightness : null,
373 systemNavigationBarContrastEnforced: isAndroid ? definedOverlayStyle.systemNavigationBarContrastEnforced : null,
374 );
375 SystemChrome.setSystemUIOverlayStyle(overlayStyle);
376 }
377
378 @override
379 Rect get paintBounds => Offset.zero & (size * configuration.devicePixelRatio);
380
381 @override
382 Rect get semanticBounds {
383 assert(_rootTransform != null);
384 return MatrixUtils.transformRect(_rootTransform!, Offset.zero & size);
385 }
386
387 @override
388 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
389 // call to ${super.debugFillProperties(description)} is omitted because the
390 // root superclasses don't include any interesting information for this
391 // class
392 assert(() {
393 properties.add(DiagnosticsNode.message('debug mode enabled - ${kIsWeb ? 'Web' : Platform.operatingSystem}'));
394 return true;
395 }());
396 properties.add(DiagnosticsProperty<Size>('view size', _view.physicalSize, tooltip: 'in physical pixels'));
397 properties.add(DoubleProperty('device pixel ratio', _view.devicePixelRatio, tooltip: 'physical pixels per logical pixel'));
398 properties.add(DiagnosticsProperty<ViewConfiguration>('configuration', configuration, tooltip: 'in logical pixels'));
399 if (_view.platformDispatcher.semanticsEnabled) {
400 properties.add(DiagnosticsNode.message('semantics enabled'));
401 }
402 }
403
404 static final List<DebugPaintCallback> _debugPaintCallbacks = <DebugPaintCallback>[];
405
406 /// Registers a [DebugPaintCallback] that is called every time a [RenderView]
407 /// repaints in debug mode.
408 ///
409 /// The callback may paint a debug overlay on top of the content of the
410 /// [RenderView] provided to the callback. Callbacks are invoked in the
411 /// order they were registered in.
412 ///
413 /// Neither registering a callback nor the continued presence of a callback
414 /// changes how often [RenderView]s are repainted. It is up to the owner of
415 /// the callback to call [markNeedsPaint] on any [RenderView] for which it
416 /// wants to update the painted overlay.
417 ///
418 /// Does nothing in release mode.
419 static void debugAddPaintCallback(DebugPaintCallback callback) {
420 assert(() {
421 _debugPaintCallbacks.add(callback);
422 return true;
423 }());
424 }
425
426 /// Removes a callback registered with [debugAddPaintCallback].
427 ///
428 /// It does not schedule a frame to repaint the [RenderView]s without the
429 /// overlay painted by the removed callback. It is up to the owner of the
430 /// callback to call [markNeedsPaint] on the relevant [RenderView]s to
431 /// repaint them without the overlay.
432 ///
433 /// Does nothing in release mode.
434 static void debugRemovePaintCallback(DebugPaintCallback callback) {
435 assert(() {
436 _debugPaintCallbacks.remove(callback);
437 return true;
438 }());
439 }
440}
441
442/// A callback for painting a debug overlay on top of the provided [RenderView].
443///
444/// Used by [RenderView.debugAddPaintCallback] and
445/// [RenderView.debugRemovePaintCallback].
446typedef DebugPaintCallback = void Function(PaintingContext context, Offset offset, RenderView renderView);
447