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 | |
5 | import 'dart:io' show Platform; |
6 | import 'dart:ui' as ui show FlutterView, Scene, SceneBuilder, SemanticsUpdate; |
7 | |
8 | import 'package:flutter/foundation.dart'; |
9 | import 'package:flutter/services.dart'; |
10 | |
11 | import 'binding.dart'; |
12 | import 'box.dart'; |
13 | import 'debug.dart'; |
14 | import 'layer.dart'; |
15 | import 'object.dart'; |
16 | |
17 | /// The layout constraints for the root render object. |
18 | @immutable |
19 | class 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. |
65 | class 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]. |
446 | typedef DebugPaintCallback = void Function(PaintingContext context, Offset offset, RenderView renderView); |
447 | |