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 | |
6 | import 'package:flutter/foundation.dart'; |
7 | import 'package:flutter/gestures.dart'; |
8 | import 'package:flutter/scheduler.dart'; |
9 | import 'package:flutter/semantics.dart'; |
10 | import 'package:flutter/services.dart'; |
11 | |
12 | import 'box.dart'; |
13 | import 'layer.dart'; |
14 | import 'object.dart'; |
15 | |
16 | /// How an embedded platform view behave during hit tests. |
17 | enum PlatformViewHitTestBehavior { |
18 | /// Opaque targets can be hit by hit tests, causing them to both receive |
19 | /// events within their bounds and prevent targets visually behind them from |
20 | /// also receiving events. |
21 | opaque, |
22 | |
23 | /// Translucent targets both receive events within their bounds and permit |
24 | /// targets visually behind them to also receive events. |
25 | translucent, |
26 | |
27 | /// Transparent targets don't receive events within their bounds and permit |
28 | /// targets visually behind them to receive events. |
29 | transparent, |
30 | } |
31 | |
32 | enum _PlatformViewState { |
33 | uninitialized, |
34 | resizing, |
35 | ready, |
36 | } |
37 | |
38 | bool _factoryTypesSetEquals<T>(Set<Factory<T>>? a, Set<Factory<T>>? b) { |
39 | if (a == b) { |
40 | return true; |
41 | } |
42 | if (a == null || b == null) { |
43 | return false; |
44 | } |
45 | return setEquals(_factoriesTypeSet(a), _factoriesTypeSet(b)); |
46 | } |
47 | |
48 | Set<Type> _factoriesTypeSet<T>(Set<Factory<T>> factories) { |
49 | return factories.map<Type>((Factory<T> factory) => factory.type).toSet(); |
50 | } |
51 | |
52 | /// A render object for an Android view. |
53 | /// |
54 | /// Requires Android API level 23 or greater. |
55 | /// |
56 | /// [RenderAndroidView] is responsible for sizing, displaying and passing touch events to an |
57 | /// Android [View](https://developer.android.com/reference/android/view/View). |
58 | /// |
59 | /// {@template flutter.rendering.RenderAndroidView.layout} |
60 | /// The render object's layout behavior is to fill all available space, the parent of this object must |
61 | /// provide bounded layout constraints. |
62 | /// {@endtemplate} |
63 | /// |
64 | /// {@template flutter.rendering.RenderAndroidView.gestures} |
65 | /// The render object participates in Flutter's gesture arenas, and dispatches touch events to the |
66 | /// platform view iff it won the arena. Specific gestures that should be dispatched to the platform |
67 | /// view can be specified with factories in the `gestureRecognizers` constructor parameter or |
68 | /// by calling `updateGestureRecognizers`. If the set of gesture recognizers is empty, the gesture |
69 | /// will be dispatched to the platform view iff it was not claimed by any other gesture recognizer. |
70 | /// {@endtemplate} |
71 | /// |
72 | /// See also: |
73 | /// |
74 | /// * [AndroidView] which is a widget that is used to show an Android view. |
75 | /// * [PlatformViewsService] which is a service for controlling platform views. |
76 | class RenderAndroidView extends PlatformViewRenderBox { |
77 | /// Creates a render object for an Android view. |
78 | RenderAndroidView({ |
79 | required AndroidViewController viewController, |
80 | required PlatformViewHitTestBehavior hitTestBehavior, |
81 | required Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers, |
82 | Clip clipBehavior = Clip.hardEdge, |
83 | }) : _viewController = viewController, |
84 | _clipBehavior = clipBehavior, |
85 | super(controller: viewController, hitTestBehavior: hitTestBehavior, gestureRecognizers: gestureRecognizers) { |
86 | _viewController.pointTransformer = (Offset offset) => globalToLocal(offset); |
87 | updateGestureRecognizers(gestureRecognizers); |
88 | _viewController.addOnPlatformViewCreatedListener(_onPlatformViewCreated); |
89 | this.hitTestBehavior = hitTestBehavior; |
90 | _setOffset(); |
91 | } |
92 | |
93 | _PlatformViewState _state = _PlatformViewState.uninitialized; |
94 | |
95 | Size? _currentTextureSize; |
96 | |
97 | bool _isDisposed = false; |
98 | |
99 | /// The Android view controller for the Android view associated with this render object. |
100 | @override |
101 | AndroidViewController get controller => _viewController; |
102 | |
103 | AndroidViewController _viewController; |
104 | |
105 | /// Sets a new Android view controller. |
106 | @override |
107 | set controller(AndroidViewController controller) { |
108 | assert(!_isDisposed); |
109 | if (_viewController == controller) { |
110 | return; |
111 | } |
112 | _viewController.removeOnPlatformViewCreatedListener(_onPlatformViewCreated); |
113 | super.controller = controller; |
114 | _viewController = controller; |
115 | _viewController.pointTransformer = (Offset offset) => globalToLocal(offset); |
116 | _sizePlatformView(); |
117 | if (_viewController.isCreated) { |
118 | markNeedsSemanticsUpdate(); |
119 | } |
120 | _viewController.addOnPlatformViewCreatedListener(_onPlatformViewCreated); |
121 | } |
122 | |
123 | /// {@macro flutter.material.Material.clipBehavior} |
124 | /// |
125 | /// Defaults to [Clip.hardEdge]. |
126 | Clip get clipBehavior => _clipBehavior; |
127 | Clip _clipBehavior = Clip.hardEdge; |
128 | set clipBehavior(Clip value) { |
129 | if (value != _clipBehavior) { |
130 | _clipBehavior = value; |
131 | markNeedsPaint(); |
132 | markNeedsSemanticsUpdate(); |
133 | } |
134 | } |
135 | |
136 | void _onPlatformViewCreated(int id) { |
137 | assert(!_isDisposed); |
138 | markNeedsSemanticsUpdate(); |
139 | } |
140 | |
141 | @override |
142 | bool get sizedByParent => true; |
143 | |
144 | @override |
145 | bool get alwaysNeedsCompositing => true; |
146 | |
147 | @override |
148 | bool get isRepaintBoundary => true; |
149 | |
150 | @override |
151 | @protected |
152 | Size computeDryLayout(covariant BoxConstraints constraints) { |
153 | return constraints.biggest; |
154 | } |
155 | |
156 | @override |
157 | void performResize() { |
158 | super.performResize(); |
159 | _sizePlatformView(); |
160 | } |
161 | |
162 | Future<void> _sizePlatformView() async { |
163 | // Android virtual displays cannot have a zero size. |
164 | // Trying to size it to 0 crashes the app, which was happening when starting the app |
165 | // with a locked screen (see: https://github.com/flutter/flutter/issues/20456). |
166 | if (_state == _PlatformViewState.resizing || size.isEmpty) { |
167 | return; |
168 | } |
169 | |
170 | _state = _PlatformViewState.resizing; |
171 | markNeedsPaint(); |
172 | |
173 | Size targetSize; |
174 | do { |
175 | targetSize = size; |
176 | _currentTextureSize = await _viewController.setSize(targetSize); |
177 | if (_isDisposed) { |
178 | return; |
179 | } |
180 | // We've resized the platform view to targetSize, but it is possible that |
181 | // while we were resizing the render object's size was changed again. |
182 | // In that case we will resize the platform view again. |
183 | } while (size != targetSize); |
184 | |
185 | _state = _PlatformViewState.ready; |
186 | markNeedsPaint(); |
187 | } |
188 | |
189 | // Sets the offset of the underlying platform view on the platform side. |
190 | // |
191 | // This allows the Android native view to draw the a11y highlights in the same |
192 | // location on the screen as the platform view widget in the Flutter framework. |
193 | // |
194 | // It also allows platform code to obtain the correct position of the Android |
195 | // native view on the screen. |
196 | void _setOffset() { |
197 | SchedulerBinding.instance.addPostFrameCallback((_) async { |
198 | if (!_isDisposed) { |
199 | if (attached) { |
200 | await _viewController.setOffset(localToGlobal(Offset.zero)); |
201 | } |
202 | // Schedule a new post frame callback. |
203 | _setOffset(); |
204 | } |
205 | }, debugLabel: 'RenderAndroidView.setOffset' ); |
206 | } |
207 | |
208 | @override |
209 | void paint(PaintingContext context, Offset offset) { |
210 | if (_viewController.textureId == null || _currentTextureSize == null) { |
211 | return; |
212 | } |
213 | |
214 | // As resizing the Android view happens asynchronously we don't know exactly when is a |
215 | // texture frame with the new size is ready for consumption. |
216 | // TextureLayer is unaware of the texture frame's size and always maps it to the |
217 | // specified rect. If the rect we provide has a different size from the current texture frame's |
218 | // size the texture frame will be scaled. |
219 | // To prevent unwanted scaling artifacts while resizing, clip the texture. |
220 | // This guarantees that the size of the texture frame we're painting is always |
221 | // _currentAndroidTextureSize. |
222 | final bool isTextureLargerThanWidget = _currentTextureSize!.width > size.width || |
223 | _currentTextureSize!.height > size.height; |
224 | if (isTextureLargerThanWidget && clipBehavior != Clip.none) { |
225 | _clipRectLayer.layer = context.pushClipRect( |
226 | true, |
227 | offset, |
228 | offset & size, |
229 | _paintTexture, |
230 | clipBehavior: clipBehavior, |
231 | oldLayer: _clipRectLayer.layer, |
232 | ); |
233 | return; |
234 | } |
235 | _clipRectLayer.layer = null; |
236 | _paintTexture(context, offset); |
237 | } |
238 | |
239 | final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>(); |
240 | |
241 | @override |
242 | void dispose() { |
243 | _isDisposed = true; |
244 | _clipRectLayer.layer = null; |
245 | _viewController.removeOnPlatformViewCreatedListener(_onPlatformViewCreated); |
246 | super.dispose(); |
247 | } |
248 | |
249 | void _paintTexture(PaintingContext context, Offset offset) { |
250 | if (_currentTextureSize == null) { |
251 | return; |
252 | } |
253 | |
254 | context.addLayer(TextureLayer( |
255 | rect: offset & _currentTextureSize!, |
256 | textureId: _viewController.textureId!, |
257 | )); |
258 | } |
259 | |
260 | @override |
261 | void describeSemanticsConfiguration(SemanticsConfiguration config) { |
262 | // Don't call the super implementation since `platformViewId` should |
263 | // be set only when the platform view is created, but the concept of |
264 | // a "created" platform view belongs to this subclass. |
265 | config.isSemanticBoundary = true; |
266 | |
267 | if (_viewController.isCreated) { |
268 | config.platformViewId = _viewController.viewId; |
269 | } |
270 | } |
271 | } |
272 | |
273 | /// Common render-layer functionality for iOS and macOS platform views. |
274 | /// |
275 | /// Provides the basic rendering logic for iOS and macOS platformviews. |
276 | /// Subclasses shall override handleEvent in order to execute custom event logic. |
277 | /// T represents the class of the view controller for the corresponding widget. |
278 | abstract class RenderDarwinPlatformView<T extends DarwinPlatformViewController> extends RenderBox { |
279 | /// Creates a render object for a platform view. |
280 | RenderDarwinPlatformView({ |
281 | required T viewController, |
282 | required this.hitTestBehavior, |
283 | required Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers, |
284 | }) : _viewController = viewController { |
285 | updateGestureRecognizers(gestureRecognizers); |
286 | } |
287 | |
288 | |
289 | /// The unique identifier of the platform view controlled by this controller. |
290 | T get viewController => _viewController; |
291 | T _viewController; |
292 | set viewController(T value) { |
293 | if (_viewController == value) { |
294 | return; |
295 | } |
296 | final bool needsSemanticsUpdate = _viewController.id != value.id; |
297 | _viewController = value; |
298 | markNeedsPaint(); |
299 | if (needsSemanticsUpdate) { |
300 | markNeedsSemanticsUpdate(); |
301 | } |
302 | } |
303 | |
304 | /// How to behave during hit testing. |
305 | // The implicit setter is enough here as changing this value will just affect |
306 | // any newly arriving events there's nothing we need to invalidate. |
307 | PlatformViewHitTestBehavior hitTestBehavior; |
308 | |
309 | @override |
310 | bool get sizedByParent => true; |
311 | |
312 | @override |
313 | bool get alwaysNeedsCompositing => true; |
314 | |
315 | @override |
316 | bool get isRepaintBoundary => true; |
317 | |
318 | PointerEvent? _lastPointerDownEvent; |
319 | |
320 | _UiKitViewGestureRecognizer? _gestureRecognizer; |
321 | |
322 | @override |
323 | @protected |
324 | Size computeDryLayout(covariant BoxConstraints constraints) { |
325 | return constraints.biggest; |
326 | } |
327 | |
328 | @override |
329 | void paint(PaintingContext context, Offset offset) { |
330 | context.addLayer(PlatformViewLayer( |
331 | rect: offset & size, |
332 | viewId: _viewController.id, |
333 | )); |
334 | } |
335 | |
336 | @override |
337 | bool hitTest(BoxHitTestResult result, { Offset? position }) { |
338 | if (hitTestBehavior == PlatformViewHitTestBehavior.transparent || !size.contains(position!)) { |
339 | return false; |
340 | } |
341 | result.add(BoxHitTestEntry(this, position)); |
342 | return hitTestBehavior == PlatformViewHitTestBehavior.opaque; |
343 | } |
344 | |
345 | @override |
346 | bool hitTestSelf(Offset position) => hitTestBehavior != PlatformViewHitTestBehavior.transparent; |
347 | |
348 | // This is registered as a global PointerRoute while the render object is attached. |
349 | void _handleGlobalPointerEvent(PointerEvent event) { |
350 | if (event is! PointerDownEvent) { |
351 | return; |
352 | } |
353 | if (!(Offset.zero & size).contains(globalToLocal(event.position))) { |
354 | return; |
355 | } |
356 | if ((event.original ?? event) != _lastPointerDownEvent) { |
357 | // The pointer event is in the bounds of this render box, but we didn't get it in handleEvent. |
358 | // This means that the pointer event was absorbed by a different render object. |
359 | // Since on the platform side the FlutterTouchIntercepting view is seeing all events that are |
360 | // within its bounds we need to tell it to reject the current touch sequence. |
361 | _viewController.rejectGesture(); |
362 | } |
363 | _lastPointerDownEvent = null; |
364 | } |
365 | |
366 | @override |
367 | void describeSemanticsConfiguration (SemanticsConfiguration config) { |
368 | super.describeSemanticsConfiguration(config); |
369 | config.isSemanticBoundary = true; |
370 | config.platformViewId = _viewController.id; |
371 | } |
372 | |
373 | @override |
374 | void attach(PipelineOwner owner) { |
375 | super.attach(owner); |
376 | GestureBinding.instance.pointerRouter.addGlobalRoute(_handleGlobalPointerEvent); |
377 | } |
378 | |
379 | @override |
380 | void detach() { |
381 | GestureBinding.instance.pointerRouter.removeGlobalRoute(_handleGlobalPointerEvent); |
382 | super.detach(); |
383 | } |
384 | |
385 | /// {@macro flutter.rendering.PlatformViewRenderBox.updateGestureRecognizers} |
386 | void updateGestureRecognizers(Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers); |
387 | } |
388 | |
389 | /// A render object for an iOS UIKit UIView. |
390 | /// |
391 | /// [RenderUiKitView] is responsible for sizing and displaying an iOS |
392 | /// [UIView](https://developer.apple.com/documentation/uikit/uiview). |
393 | /// |
394 | /// UIViews are added as subviews of the FlutterView and are composited by Quartz. |
395 | /// |
396 | /// The viewController is typically generated by [PlatformViewsRegistry.getNextPlatformViewId], the UIView |
397 | /// must have been created by calling [PlatformViewsService.initUiKitView]. |
398 | /// |
399 | /// {@macro flutter.rendering.RenderAndroidView.layout} |
400 | /// |
401 | /// {@macro flutter.rendering.RenderAndroidView.gestures} |
402 | /// |
403 | /// See also: |
404 | /// |
405 | /// * [UiKitView], which is a widget that is used to show a UIView. |
406 | /// * [PlatformViewsService], which is a service for controlling platform views. |
407 | class RenderUiKitView extends RenderDarwinPlatformView<UiKitViewController> { |
408 | /// Creates a render object for an iOS UIView. |
409 | RenderUiKitView({ |
410 | required super.viewController, |
411 | required super.hitTestBehavior, |
412 | required super.gestureRecognizers, |
413 | }); |
414 | |
415 | /// {@macro flutter.rendering.PlatformViewRenderBox.updateGestureRecognizers} |
416 | @override |
417 | void updateGestureRecognizers(Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers) { |
418 | assert( |
419 | _factoriesTypeSet(gestureRecognizers).length == gestureRecognizers.length, |
420 | 'There were multiple gesture recognizer factories for the same type, there must only be a single ' |
421 | 'gesture recognizer factory for each gesture recognizer type.' , |
422 | ); |
423 | if (_factoryTypesSetEquals(gestureRecognizers, _gestureRecognizer?.gestureRecognizerFactories)) { |
424 | return; |
425 | } |
426 | _gestureRecognizer?.dispose(); |
427 | _gestureRecognizer = _UiKitViewGestureRecognizer(viewController, gestureRecognizers); |
428 | } |
429 | |
430 | @override |
431 | void handleEvent(PointerEvent event, HitTestEntry entry) { |
432 | if (event is! PointerDownEvent) { |
433 | return; |
434 | } |
435 | _gestureRecognizer!.addPointer(event); |
436 | _lastPointerDownEvent = event.original ?? event; |
437 | } |
438 | |
439 | @override |
440 | void detach() { |
441 | _gestureRecognizer!.reset(); |
442 | super.detach(); |
443 | } |
444 | |
445 | @override |
446 | void dispose() { |
447 | _gestureRecognizer?.dispose(); |
448 | super.dispose(); |
449 | } |
450 | } |
451 | |
452 | /// A render object for a macOS platform view. |
453 | class RenderAppKitView extends RenderDarwinPlatformView<AppKitViewController> { |
454 | /// Creates a render object for a macOS AppKitView. |
455 | RenderAppKitView({ |
456 | required super.viewController, |
457 | required super.hitTestBehavior, |
458 | required super.gestureRecognizers, |
459 | }); |
460 | |
461 | // TODO(schectman): Add gesture functionality to macOS platform view when implemented. |
462 | // https://github.com/flutter/flutter/issues/128519 |
463 | // This method will need to behave the same as the same-named method for RenderUiKitView, |
464 | // but use a _AppKitViewGestureRecognizer or equivalent, whose constructor shall accept an |
465 | // AppKitViewController. |
466 | @override |
467 | void updateGestureRecognizers(Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers) {} |
468 | } |
469 | |
470 | // This recognizer constructs gesture recognizers from a set of gesture recognizer factories |
471 | // it was give, adds all of them to a gesture arena team with the _UiKitViewGestureRecognizer |
472 | // as the team captain. |
473 | // When the team wins a gesture the recognizer notifies the engine that it should release |
474 | // the touch sequence to the embedded UIView. |
475 | class _UiKitViewGestureRecognizer extends OneSequenceGestureRecognizer { |
476 | _UiKitViewGestureRecognizer( |
477 | this.controller, |
478 | this.gestureRecognizerFactories |
479 | ) { |
480 | team = GestureArenaTeam() |
481 | ..captain = this; |
482 | _gestureRecognizers = gestureRecognizerFactories.map( |
483 | (Factory<OneSequenceGestureRecognizer> recognizerFactory) { |
484 | final OneSequenceGestureRecognizer gestureRecognizer = recognizerFactory.constructor(); |
485 | gestureRecognizer.team = team; |
486 | // The below gesture recognizers requires at least one non-empty callback to |
487 | // compete in the gesture arena. |
488 | // https://github.com/flutter/flutter/issues/35394#issuecomment-562285087 |
489 | if (gestureRecognizer is LongPressGestureRecognizer) { |
490 | gestureRecognizer.onLongPress ??= (){}; |
491 | } else if (gestureRecognizer is DragGestureRecognizer) { |
492 | gestureRecognizer.onDown ??= (_){}; |
493 | } else if (gestureRecognizer is TapGestureRecognizer) { |
494 | gestureRecognizer.onTapDown ??= (_){}; |
495 | } |
496 | return gestureRecognizer; |
497 | }, |
498 | ).toSet(); |
499 | } |
500 | |
501 | // We use OneSequenceGestureRecognizers as they support gesture arena teams. |
502 | // TODO(amirh): get a list of GestureRecognizers here. |
503 | // https://github.com/flutter/flutter/issues/20953 |
504 | final Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizerFactories; |
505 | late Set<OneSequenceGestureRecognizer> _gestureRecognizers; |
506 | |
507 | final UiKitViewController controller; |
508 | |
509 | @override |
510 | void addAllowedPointer(PointerDownEvent event) { |
511 | super.addAllowedPointer(event); |
512 | for (final OneSequenceGestureRecognizer recognizer in _gestureRecognizers) { |
513 | recognizer.addPointer(event); |
514 | } |
515 | } |
516 | |
517 | @override |
518 | String get debugDescription => 'UIKit view' ; |
519 | |
520 | @override |
521 | void didStopTrackingLastPointer(int pointer) { } |
522 | |
523 | @override |
524 | void handleEvent(PointerEvent event) { |
525 | stopTrackingIfPointerNoLongerDown(event); |
526 | } |
527 | |
528 | @override |
529 | void acceptGesture(int pointer) { |
530 | controller.acceptGesture(); |
531 | } |
532 | |
533 | @override |
534 | void rejectGesture(int pointer) { |
535 | controller.rejectGesture(); |
536 | } |
537 | |
538 | void reset() { |
539 | resolve(GestureDisposition.rejected); |
540 | } |
541 | } |
542 | |
543 | typedef _HandlePointerEvent = Future<void> Function(PointerEvent event); |
544 | |
545 | // This recognizer constructs gesture recognizers from a set of gesture recognizer factories |
546 | // it was give, adds all of them to a gesture arena team with the _PlatformViewGestureRecognizer |
547 | // as the team captain. |
548 | // As long as the gesture arena is unresolved, the recognizer caches all pointer events. |
549 | // When the team wins, the recognizer sends all the cached pointer events to `_handlePointerEvent`, and |
550 | // sets itself to a "forwarding mode" where it will forward any new pointer event to `_handlePointerEvent`. |
551 | class _PlatformViewGestureRecognizer extends OneSequenceGestureRecognizer { |
552 | _PlatformViewGestureRecognizer( |
553 | _HandlePointerEvent handlePointerEvent, |
554 | this.gestureRecognizerFactories |
555 | ) { |
556 | team = GestureArenaTeam() |
557 | ..captain = this; |
558 | _gestureRecognizers = gestureRecognizerFactories.map( |
559 | (Factory<OneSequenceGestureRecognizer> recognizerFactory) { |
560 | final OneSequenceGestureRecognizer gestureRecognizer = recognizerFactory.constructor(); |
561 | gestureRecognizer.team = team; |
562 | // The below gesture recognizers requires at least one non-empty callback to |
563 | // compete in the gesture arena. |
564 | // https://github.com/flutter/flutter/issues/35394#issuecomment-562285087 |
565 | if (gestureRecognizer is LongPressGestureRecognizer) { |
566 | gestureRecognizer.onLongPress ??= (){}; |
567 | } else if (gestureRecognizer is DragGestureRecognizer) { |
568 | gestureRecognizer.onDown ??= (_){}; |
569 | } else if (gestureRecognizer is TapGestureRecognizer) { |
570 | gestureRecognizer.onTapDown ??= (_){}; |
571 | } |
572 | return gestureRecognizer; |
573 | }, |
574 | ).toSet(); |
575 | _handlePointerEvent = handlePointerEvent; |
576 | } |
577 | |
578 | late _HandlePointerEvent _handlePointerEvent; |
579 | |
580 | // Maps a pointer to a list of its cached pointer events. |
581 | // Before the arena for a pointer is resolved all events are cached here, if we win the arena |
582 | // the cached events are dispatched to `_handlePointerEvent`, if we lose the arena we clear the cache for |
583 | // the pointer. |
584 | final Map<int, List<PointerEvent>> cachedEvents = <int, List<PointerEvent>>{}; |
585 | |
586 | // Pointer for which we have already won the arena, events for pointers in this set are |
587 | // immediately dispatched to `_handlePointerEvent`. |
588 | final Set<int> forwardedPointers = <int>{}; |
589 | |
590 | // We use OneSequenceGestureRecognizers as they support gesture arena teams. |
591 | // TODO(amirh): get a list of GestureRecognizers here. |
592 | // https://github.com/flutter/flutter/issues/20953 |
593 | final Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizerFactories; |
594 | late Set<OneSequenceGestureRecognizer> _gestureRecognizers; |
595 | |
596 | @override |
597 | void addAllowedPointer(PointerDownEvent event) { |
598 | super.addAllowedPointer(event); |
599 | for (final OneSequenceGestureRecognizer recognizer in _gestureRecognizers) { |
600 | recognizer.addPointer(event); |
601 | } |
602 | } |
603 | |
604 | @override |
605 | String get debugDescription => 'Platform view' ; |
606 | |
607 | @override |
608 | void didStopTrackingLastPointer(int pointer) { } |
609 | |
610 | @override |
611 | void handleEvent(PointerEvent event) { |
612 | if (!forwardedPointers.contains(event.pointer)) { |
613 | _cacheEvent(event); |
614 | } else { |
615 | _handlePointerEvent(event); |
616 | } |
617 | stopTrackingIfPointerNoLongerDown(event); |
618 | } |
619 | |
620 | @override |
621 | void acceptGesture(int pointer) { |
622 | _flushPointerCache(pointer); |
623 | forwardedPointers.add(pointer); |
624 | } |
625 | |
626 | @override |
627 | void rejectGesture(int pointer) { |
628 | stopTrackingPointer(pointer); |
629 | cachedEvents.remove(pointer); |
630 | } |
631 | |
632 | void _cacheEvent(PointerEvent event) { |
633 | if (!cachedEvents.containsKey(event.pointer)) { |
634 | cachedEvents[event.pointer] = <PointerEvent> []; |
635 | } |
636 | cachedEvents[event.pointer]!.add(event); |
637 | } |
638 | |
639 | void _flushPointerCache(int pointer) { |
640 | cachedEvents.remove(pointer)?.forEach(_handlePointerEvent); |
641 | } |
642 | |
643 | @override |
644 | void stopTrackingPointer(int pointer) { |
645 | super.stopTrackingPointer(pointer); |
646 | forwardedPointers.remove(pointer); |
647 | } |
648 | |
649 | void reset() { |
650 | forwardedPointers.forEach(super.stopTrackingPointer); |
651 | forwardedPointers.clear(); |
652 | cachedEvents.keys.forEach(super.stopTrackingPointer); |
653 | cachedEvents.clear(); |
654 | resolve(GestureDisposition.rejected); |
655 | } |
656 | } |
657 | |
658 | /// A render object for embedding a platform view. |
659 | /// |
660 | /// [PlatformViewRenderBox] presents a platform view by adding a [PlatformViewLayer] layer, |
661 | /// integrates it with the gesture arenas system and adds relevant semantic nodes to the semantics tree. |
662 | class PlatformViewRenderBox extends RenderBox with _PlatformViewGestureMixin { |
663 | /// Creating a render object for a [PlatformViewSurface]. |
664 | PlatformViewRenderBox({ |
665 | required PlatformViewController controller, |
666 | required PlatformViewHitTestBehavior hitTestBehavior, |
667 | required Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers, |
668 | }) : assert(controller.viewId > -1), |
669 | _controller = controller { |
670 | this.hitTestBehavior = hitTestBehavior; |
671 | updateGestureRecognizers(gestureRecognizers); |
672 | } |
673 | |
674 | /// The controller for this render object. |
675 | PlatformViewController get controller => _controller; |
676 | PlatformViewController _controller; |
677 | /// Setting this value to a new value will result in a repaint. |
678 | set controller(covariant PlatformViewController controller) { |
679 | assert(controller.viewId > -1); |
680 | |
681 | if (_controller == controller) { |
682 | return; |
683 | } |
684 | final bool needsSemanticsUpdate = _controller.viewId != controller.viewId; |
685 | _controller = controller; |
686 | markNeedsPaint(); |
687 | if (needsSemanticsUpdate) { |
688 | markNeedsSemanticsUpdate(); |
689 | } |
690 | } |
691 | |
692 | /// {@template flutter.rendering.PlatformViewRenderBox.updateGestureRecognizers} |
693 | /// Updates which gestures should be forwarded to the platform view. |
694 | /// |
695 | /// Gesture recognizers created by factories in this set participate in the gesture arena for each |
696 | /// pointer that was put down on the render box. If any of the recognizers on this list wins the |
697 | /// gesture arena, the entire pointer event sequence starting from the pointer down event |
698 | /// will be dispatched to the Android view. |
699 | /// |
700 | /// The `gestureRecognizers` property must not contain more than one factory with the same [Factory.type]. |
701 | /// |
702 | /// Setting a new set of gesture recognizer factories with the same [Factory.type]s as the current |
703 | /// set has no effect, because the factories' constructors would have already been called with the previous set. |
704 | /// {@endtemplate} |
705 | /// |
706 | /// Any active gesture arena the `PlatformView` participates in is rejected when the |
707 | /// set of gesture recognizers is changed. |
708 | void updateGestureRecognizers(Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers) { |
709 | _updateGestureRecognizersWithCallBack(gestureRecognizers, _controller.dispatchPointerEvent); |
710 | } |
711 | |
712 | @override |
713 | bool get sizedByParent => true; |
714 | |
715 | @override |
716 | bool get alwaysNeedsCompositing => true; |
717 | |
718 | @override |
719 | bool get isRepaintBoundary => true; |
720 | |
721 | @override |
722 | @protected |
723 | Size computeDryLayout(covariant BoxConstraints constraints) { |
724 | return constraints.biggest; |
725 | } |
726 | |
727 | @override |
728 | void paint(PaintingContext context, Offset offset) { |
729 | context.addLayer(PlatformViewLayer( |
730 | rect: offset & size, |
731 | viewId: _controller.viewId, |
732 | )); |
733 | } |
734 | |
735 | @override |
736 | void describeSemanticsConfiguration(SemanticsConfiguration config) { |
737 | super.describeSemanticsConfiguration(config); |
738 | config.isSemanticBoundary = true; |
739 | config.platformViewId = _controller.viewId; |
740 | } |
741 | } |
742 | |
743 | /// The Mixin handling the pointer events and gestures of a platform view render box. |
744 | mixin _PlatformViewGestureMixin on RenderBox implements MouseTrackerAnnotation { |
745 | |
746 | /// How to behave during hit testing. |
747 | // Changing _hitTestBehavior might affect which objects are considered hovered over. |
748 | set hitTestBehavior(PlatformViewHitTestBehavior value) { |
749 | if (value != _hitTestBehavior) { |
750 | _hitTestBehavior = value; |
751 | if (owner != null) { |
752 | markNeedsPaint(); |
753 | } |
754 | } |
755 | } |
756 | PlatformViewHitTestBehavior? _hitTestBehavior; |
757 | |
758 | _HandlePointerEvent? _handlePointerEvent; |
759 | |
760 | /// {@macro flutter.rendering.RenderAndroidView.updateGestureRecognizers} |
761 | /// |
762 | /// Any active gesture arena the `PlatformView` participates in is rejected when the |
763 | /// set of gesture recognizers is changed. |
764 | void _updateGestureRecognizersWithCallBack(Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers, _HandlePointerEvent handlePointerEvent) { |
765 | assert( |
766 | _factoriesTypeSet(gestureRecognizers).length == gestureRecognizers.length, |
767 | 'There were multiple gesture recognizer factories for the same type, there must only be a single ' |
768 | 'gesture recognizer factory for each gesture recognizer type.' , |
769 | ); |
770 | if (_factoryTypesSetEquals(gestureRecognizers, _gestureRecognizer?.gestureRecognizerFactories)) { |
771 | return; |
772 | } |
773 | _gestureRecognizer?.dispose(); |
774 | _gestureRecognizer = _PlatformViewGestureRecognizer(handlePointerEvent, gestureRecognizers); |
775 | _handlePointerEvent = handlePointerEvent; |
776 | } |
777 | |
778 | _PlatformViewGestureRecognizer? _gestureRecognizer; |
779 | |
780 | @override |
781 | bool hitTest(BoxHitTestResult result, { required Offset position }) { |
782 | if (_hitTestBehavior == PlatformViewHitTestBehavior.transparent || !size.contains(position)) { |
783 | return false; |
784 | } |
785 | result.add(BoxHitTestEntry(this, position)); |
786 | return _hitTestBehavior == PlatformViewHitTestBehavior.opaque; |
787 | } |
788 | |
789 | @override |
790 | bool hitTestSelf(Offset position) => _hitTestBehavior != PlatformViewHitTestBehavior.transparent; |
791 | |
792 | @override |
793 | PointerEnterEventListener? get onEnter => null; |
794 | |
795 | @override |
796 | PointerExitEventListener? get onExit => null; |
797 | |
798 | @override |
799 | MouseCursor get cursor => MouseCursor.uncontrolled; |
800 | |
801 | @override |
802 | bool get validForMouseTracker => true; |
803 | |
804 | @override |
805 | void handleEvent(PointerEvent event, HitTestEntry entry) { |
806 | if (event is PointerDownEvent) { |
807 | _gestureRecognizer!.addPointer(event); |
808 | } |
809 | if (event is PointerHoverEvent) { |
810 | _handlePointerEvent?.call(event); |
811 | } |
812 | } |
813 | |
814 | @override |
815 | void detach() { |
816 | _gestureRecognizer!.reset(); |
817 | super.detach(); |
818 | } |
819 | |
820 | @override |
821 | void dispose() { |
822 | _gestureRecognizer?.dispose(); |
823 | super.dispose(); |
824 | } |
825 | } |
826 | |