| Flutter iOS Embedder |

FlutterPlatformViews.mm
Go to the documentation of this file.
1 // Copyright 2013 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 <UIKit/UIGestureRecognizerSubclass.h>
6
7 #include <list>
8 #include <map>
9 #include <memory>
10 #include <string>
11
12 #include "flutter/common/graphics/persistent_cache.h"
13 #include "flutter/fml/platform/darwin/scoped_nsobject.h"
14 #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterChannels.h"
15 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterOverlayView.h"
16 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h"
17 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h"
18 #import "flutter/shell/platform/darwin/ios/ios_surface.h"
19
20 @implementation UIView (FirstResponder)
21 - (BOOL)flt_hasFirstResponderInViewHierarchySubtree {
22 if (self.isFirstResponder) {
23 return YES;
24 }
25 for (UIView* subview in self.subviews) {
26 if (subview.flt_hasFirstResponderInViewHierarchySubtree) {
27 return YES;
28 }
29 }
30 return NO;
31 }
32 @end
33
34 // Determines if the `clip_rect` from a clipRect mutator contains the
35 // `platformview_boundingrect`.
36 //
37 // `clip_rect` is in its own coordinate space. The rect needs to be transformed by
38 // `transform_matrix` to be in the coordinate space where the PlatformView is displayed.
39 //
40 // `platformview_boundingrect` is the final bounding rect of the PlatformView in the coordinate
41 // space where the PlatformView is displayed.
42 static bool ClipRectContainsPlatformViewBoundingRect(const SkRect& clip_rect,
43 const SkRect& platformview_boundingrect,
44 const SkMatrix& transform_matrix) {
45 SkRect transformed_rect = transform_matrix.mapRect(clip_rect);
46 return transformed_rect.contains(platformview_boundingrect);
47 }
48
49 // Determines if the `clipRRect` from a clipRRect mutator contains the
50 // `platformview_boundingrect`.
51 //
52 // `clip_rrect` is in its own coordinate space. The rrect needs to be transformed by
53 // `transform_matrix` to be in the coordinate space where the PlatformView is displayed.
54 //
55 // `platformview_boundingrect` is the final bounding rect of the PlatformView in the coordinate
56 // space where the PlatformView is displayed.
57 static bool ClipRRectContainsPlatformViewBoundingRect(const SkRRect& clip_rrect,
58 const SkRect& platformview_boundingrect,
59 const SkMatrix& transform_matrix) {
60 SkVector upper_left = clip_rrect.radii(SkRRect::Corner::kUpperLeft_Corner);
61 SkVector upper_right = clip_rrect.radii(SkRRect::Corner::kUpperRight_Corner);
62 SkVector lower_right = clip_rrect.radii(SkRRect::Corner::kLowerRight_Corner);
63 SkVector lower_left = clip_rrect.radii(SkRRect::Corner::kLowerLeft_Corner);
64 SkScalar transformed_upper_left_x = transform_matrix.mapRadius(upper_left.x());
65 SkScalar transformed_upper_left_y = transform_matrix.mapRadius(upper_left.y());
66 SkScalar transformed_upper_right_x = transform_matrix.mapRadius(upper_right.x());
67 SkScalar transformed_upper_right_y = transform_matrix.mapRadius(upper_right.y());
68 SkScalar transformed_lower_right_x = transform_matrix.mapRadius(lower_right.x());
69 SkScalar transformed_lower_right_y = transform_matrix.mapRadius(lower_right.y());
70 SkScalar transformed_lower_left_x = transform_matrix.mapRadius(lower_left.x());
71 SkScalar transformed_lower_left_y = transform_matrix.mapRadius(lower_left.y());
72 SkRect transformed_clip_rect = transform_matrix.mapRect(clip_rrect.rect());
73 SkRRect transformed_rrect;
74 SkVector corners[] = {{transformed_upper_left_x, transformed_upper_left_y},
75 {transformed_upper_right_x, transformed_upper_right_y},
76 {transformed_lower_right_x, transformed_lower_right_y},
77 {transformed_lower_left_x, transformed_lower_left_y}};
78 transformed_rrect.setRectRadii(transformed_clip_rect, corners);
79 return transformed_rrect.contains(platformview_boundingrect);
80 }
81
82 namespace flutter {
83 // Becomes NO if Apple's API changes and blurred backdrop filters cannot be applied.
84 BOOL canApplyBlurBackdrop = YES;
85
86 std::shared_ptr<FlutterPlatformViewLayer> FlutterPlatformViewLayerPool::GetLayer(
87 GrDirectContext* gr_context,
88 const std::shared_ptr<IOSContext>& ios_context) {
89 if (available_layer_index_ >= layers_.size()) {
90 std::shared_ptr<FlutterPlatformViewLayer> layer;
91 fml::scoped_nsobject<FlutterOverlayView> overlay_view;
92 fml::scoped_nsobject<FlutterOverlayView> overlay_view_wrapper;
93
94 if (!gr_context) {
95 overlay_view.reset([[FlutterOverlayView alloc] init]);
96 overlay_view_wrapper.reset([[FlutterOverlayView alloc] init]);
97
98 auto ca_layer = fml::scoped_nsobject<CALayer>{[[overlay_view.get() layer] retain]};
99 std::unique_ptr<IOSSurface> ios_surface = IOSSurface::Create(ios_context, ca_layer);
100 std::unique_ptr<Surface> surface = ios_surface->CreateGPUSurface();
101
102 layer = std::make_shared<FlutterPlatformViewLayer>(
103 std::move(overlay_view), std::move(overlay_view_wrapper), std::move(ios_surface),
104 std::move(surface));
105 } else {
106 CGFloat screenScale = [UIScreen mainScreen].scale;
107 overlay_view.reset([[FlutterOverlayView alloc] initWithContentsScale:screenScale]);
108 overlay_view_wrapper.reset([[FlutterOverlayView alloc] initWithContentsScale:screenScale]);
109
110 auto ca_layer = fml::scoped_nsobject<CALayer>{[[overlay_view.get() layer] retain]};
111 std::unique_ptr<IOSSurface> ios_surface = IOSSurface::Create(ios_context, ca_layer);
112 std::unique_ptr<Surface> surface = ios_surface->CreateGPUSurface(gr_context);
113
114 layer = std::make_shared<FlutterPlatformViewLayer>(
115 std::move(overlay_view), std::move(overlay_view_wrapper), std::move(ios_surface),
116 std::move(surface));
117 layer->gr_context = gr_context;
118 }
119 // The overlay view wrapper masks the overlay view.
120 // This is required to keep the backing surface size unchanged between frames.
121 //
122 // Otherwise, changing the size of the overlay would require a new surface,
123 // which can be very expensive.
124 //
125 // This is the case of an animation in which the overlay size is changing in every frame.
126 //
127 // +------------------------+
128 // | overlay_view |
129 // | +--------------+ | +--------------+
130 // | | wrapper | | == mask => | overlay_view |
131 // | +--------------+ | +--------------+
132 // +------------------------+
133 layer->overlay_view_wrapper.get().clipsToBounds = YES;
134 [layer->overlay_view_wrapper.get() addSubview:layer->overlay_view];
135 layers_.push_back(layer);
136 }
137 std::shared_ptr<FlutterPlatformViewLayer> layer = layers_[available_layer_index_];
138 if (gr_context != layer->gr_context) {
139 layer->gr_context = gr_context;
140 // The overlay already exists, but the GrContext was changed so we need to recreate
141 // the rendering surface with the new GrContext.
142 IOSSurface* ios_surface = layer->ios_surface.get();
143 std::unique_ptr<Surface> surface = ios_surface->CreateGPUSurface(gr_context);
144 layer->surface = std::move(surface);
145 }
146 available_layer_index_++;
147 return layer;
148 }
149
150 void FlutterPlatformViewLayerPool::RecycleLayers() {
151 available_layer_index_ = 0;
152 }
153
154 std::vector<std::shared_ptr<FlutterPlatformViewLayer>>
155 FlutterPlatformViewLayerPool::GetUnusedLayers() {
156 std::vector<std::shared_ptr<FlutterPlatformViewLayer>> results;
157 for (size_t i = available_layer_index_; i < layers_.size(); i++) {
158 results.push_back(layers_[i]);
159 }
160 return results;
161 }
162
163 void FlutterPlatformViewsController::SetFlutterView(UIView* flutter_view) {
164 flutter_view_.reset([flutter_view retain]);
165 }
166
167 void FlutterPlatformViewsController::SetFlutterViewController(
168 UIViewController* flutter_view_controller) {
169 flutter_view_controller_.reset([flutter_view_controller retain]);
170 }
171
172 UIViewController* FlutterPlatformViewsController::getFlutterViewController() {
173 return flutter_view_controller_.get();
174 }
175
176 void FlutterPlatformViewsController::OnMethodCall(FlutterMethodCall* call, FlutterResult& result) {
177 if ([[call method] isEqualToString:@"create"]) {
178 OnCreate(call, result);
179 } else if ([[call method] isEqualToString:@"dispose"]) {
180 OnDispose(call, result);
181 } else if ([[call method] isEqualToString:@"acceptGesture"]) {
182 OnAcceptGesture(call, result);
183 } else if ([[call method] isEqualToString:@"rejectGesture"]) {
184 OnRejectGesture(call, result);
185 } else {
186 result(FlutterMethodNotImplemented);
187 }
188 }
189
190 void FlutterPlatformViewsController::OnCreate(FlutterMethodCall* call, FlutterResult& result) {
191 NSDictionary<NSString*, id>* args = [call arguments];
192
193 int64_t viewId = [args[@"id"] longLongValue];
194 NSString* viewTypeString = args[@"viewType"];
195 std::string viewType(viewTypeString.UTF8String);
196
197 if (views_.count(viewId) != 0) {
198 result([FlutterError errorWithCode:@"recreating_view"
199 message:@"trying to create an already created view"
200 details:[NSString stringWithFormat:@"view id: '%lld'", viewId]]);
201 }
202
203 NSObject<FlutterPlatformViewFactory>* factory = factories_[viewType].get();
204 if (factory == nil) {
205 result([FlutterError
206 errorWithCode:@"unregistered_view_type"
207 message:[NSString stringWithFormat:@"A UIKitView widget is trying to create a "
208 @"PlatformView with an unregistered type: < %@ >",
209 viewTypeString]
210 details:@"If you are the author of the PlatformView, make sure `registerViewFactory` "
211 @"is invoked.\n"
212 @"See: "
213 @"https://docs.flutter.dev/development/platform-integration/"
214 @"platform-views#on-the-platform-side-1 for more details.\n"
215 @"If you are not the author of the PlatformView, make sure to call "
216 @"`GeneratedPluginRegistrant.register`."]);
217 return;
218 }
219
220 id params = nil;
221 if ([factory respondsToSelector:@selector(createArgsCodec)]) {
222 NSObject<FlutterMessageCodec>* codec = [factory createArgsCodec];
223 if (codec != nil && args[@"params"] != nil) {
224 FlutterStandardTypedData* paramsData = args[@"params"];
225 params = [codec decode:paramsData.data];
226 }
227 }
228
229 NSObject<FlutterPlatformView>* embedded_view = [factory createWithFrame:CGRectZero
230 viewIdentifier:viewId
231 arguments:params];
232 UIView* platform_view = [embedded_view view];
233 // Set a unique view identifier, so the platform view can be identified in unit tests.
234 platform_view.accessibilityIdentifier =
235 [NSString stringWithFormat:@"platform_view[%lld]", viewId];
236 views_[viewId] = fml::scoped_nsobject<NSObject<FlutterPlatformView>>([embedded_view retain]);
237
238 FlutterTouchInterceptingView* touch_interceptor = [[[FlutterTouchInterceptingView alloc]
239 initWithEmbeddedView:platform_view
240 platformViewsController:GetWeakPtr()
241 gestureRecognizersBlockingPolicy:gesture_recognizers_blocking_policies[viewType]]
242 autorelease];
243
244 touch_interceptors_[viewId] =
245 fml::scoped_nsobject<FlutterTouchInterceptingView>([touch_interceptor retain]);
246
247 ChildClippingView* clipping_view =
248 [[[ChildClippingView alloc] initWithFrame:CGRectZero] autorelease];
249 [clipping_view addSubview:touch_interceptor];
250 root_views_[viewId] = fml::scoped_nsobject<UIView>([clipping_view retain]);
251
252 result(nil);
253 }
254
255 void FlutterPlatformViewsController::OnDispose(FlutterMethodCall* call, FlutterResult& result) {
256 NSNumber* arg = [call arguments];
257 int64_t viewId = [arg longLongValue];
258
259 if (views_.count(viewId) == 0) {
260 result([FlutterError errorWithCode:@"unknown_view"
261 message:@"trying to dispose an unknown"
262 details:[NSString stringWithFormat:@"view id: '%lld'", viewId]]);
263 return;
264 }
265 // We wait for next submitFrame to dispose views.
266 views_to_dispose_.insert(viewId);
267 result(nil);
268 }
269
270 void FlutterPlatformViewsController::OnAcceptGesture(FlutterMethodCall* call,
271 FlutterResult& result) {
272 NSDictionary<NSString*, id>* args = [call arguments];
273 int64_t viewId = [args[@"id"] longLongValue];
274
275 if (views_.count(viewId) == 0) {
276 result([FlutterError errorWithCode:@"unknown_view"
277 message:@"trying to set gesture state for an unknown view"
278 details:[NSString stringWithFormat:@"view id: '%lld'", viewId]]);
279 return;
280 }
281
282 FlutterTouchInterceptingView* view = touch_interceptors_[viewId].get();
283 [view releaseGesture];
284
285 result(nil);
286 }
287
288 void FlutterPlatformViewsController::OnRejectGesture(FlutterMethodCall* call,
289 FlutterResult& result) {
290 NSDictionary<NSString*, id>* args = [call arguments];
291 int64_t viewId = [args[@"id"] longLongValue];
292
293 if (views_.count(viewId) == 0) {
294 result([FlutterError errorWithCode:@"unknown_view"
295 message:@"trying to set gesture state for an unknown view"
296 details:[NSString stringWithFormat:@"view id: '%lld'", viewId]]);
297 return;
298 }
299
300 FlutterTouchInterceptingView* view = touch_interceptors_[viewId].get();
301 [view blockGesture];
302
303 result(nil);
304 }
305
306 void FlutterPlatformViewsController::RegisterViewFactory(
307 NSObject<FlutterPlatformViewFactory>* factory,
308 NSString* factoryId,
309 FlutterPlatformViewGestureRecognizersBlockingPolicy gestureRecognizerBlockingPolicy) {
310 std::string idString([factoryId UTF8String]);
311 FML_CHECK(factories_.count(idString) == 0);
312 factories_[idString] =
313 fml::scoped_nsobject<NSObject<FlutterPlatformViewFactory>>([factory retain]);
314 gesture_recognizers_blocking_policies[idString] = gestureRecognizerBlockingPolicy;
315 }
316
317 void FlutterPlatformViewsController::BeginFrame(SkISize frame_size) {
318 ResetFrameState();
319 frame_size_ = frame_size;
320 }
321
322 void FlutterPlatformViewsController::CancelFrame() {
323 ResetFrameState();
324 }
325
326 // TODO(cyanglaz): https://github.com/flutter/flutter/issues/56474
327 // Make this method check if there are pending view operations instead.
328 // Also rename it to `HasPendingViewOperations`.
329 bool FlutterPlatformViewsController::HasPlatformViewThisOrNextFrame() {
330 return !composition_order_.empty() || !active_composition_order_.empty();
331 }
332
333 const int FlutterPlatformViewsController::kDefaultMergedLeaseDuration;
334
335 PostPrerollResult FlutterPlatformViewsController::PostPrerollAction(
336 const fml::RefPtr<fml::RasterThreadMerger>& raster_thread_merger) {
337 // TODO(cyanglaz): https://github.com/flutter/flutter/issues/56474
338 // Rename `has_platform_view` to `view_mutated` when the above issue is resolved.
339 if (!HasPlatformViewThisOrNextFrame()) {
340 return PostPrerollResult::kSuccess;
341 }
342 if (!raster_thread_merger->IsMerged()) {
343 // The raster thread merger may be disabled if the rasterizer is being
344 // created or teared down.
345 //
346 // In such cases, the current frame is dropped, and a new frame is attempted
347 // with the same layer tree.
348 //
349 // Eventually, the frame is submitted once this method returns `kSuccess`.
350 // At that point, the raster tasks are handled on the platform thread.
351 CancelFrame();
352 return PostPrerollResult::kSkipAndRetryFrame;
353 }
354 // If the post preroll action is successful, we will display platform views in the current frame.
355 // In order to sync the rendering of the platform views (quartz) with skia's rendering,
356 // We need to begin an explicit CATransaction. This transaction needs to be submitted
357 // after the current frame is submitted.
358 BeginCATransaction();
359 raster_thread_merger->ExtendLeaseTo(kDefaultMergedLeaseDuration);
360 return PostPrerollResult::kSuccess;
361 }
362
363 void FlutterPlatformViewsController::EndFrame(
364 bool should_resubmit_frame,
365 const fml::RefPtr<fml::RasterThreadMerger>& raster_thread_merger) {
366 if (should_resubmit_frame) {
367 raster_thread_merger->MergeWithLease(kDefaultMergedLeaseDuration);
368 }
369 }
370
371 void FlutterPlatformViewsController::PushFilterToVisitedPlatformViews(
372 const std::shared_ptr<const DlImageFilter>& filter,
373 const SkRect& filter_rect) {
374 for (int64_t id : visited_platform_views_) {
375 EmbeddedViewParams params = current_composition_params_[id];
376 params.PushImageFilter(filter, filter_rect);
377 current_composition_params_[id] = params;
378 }
379 }
380
381 void FlutterPlatformViewsController::PrerollCompositeEmbeddedView(
382 int64_t view_id,
383 std::unique_ptr<EmbeddedViewParams> params) {
384 // All the CATransactions should be committed by the end of the last frame,
385 // so catransaction_added_ must be false.
386 FML_DCHECK(!catransaction_added_);
387
388 SkRect view_bounds = SkRect::Make(frame_size_);
389 std::unique_ptr<EmbedderViewSlice> view;
390 view = std::make_unique<DisplayListEmbedderViewSlice>(view_bounds);
391 slices_.insert_or_assign(view_id, std::move(view));
392
393 composition_order_.push_back(view_id);
394
395 if (current_composition_params_.count(view_id) == 1 &&
396 current_composition_params_[view_id] == *params.get()) {
397 // Do nothing if the params didn't change.
398 return;
399 }
400 current_composition_params_[view_id] = EmbeddedViewParams(*params.get());
401 views_to_recomposite_.insert(view_id);
402 }
403
404 size_t FlutterPlatformViewsController::EmbeddedViewCount() {
405 return composition_order_.size();
406 }
407
408 UIView* FlutterPlatformViewsController::GetPlatformViewByID(int64_t view_id) {
409 return [GetFlutterTouchInterceptingViewByID(view_id) embeddedView];
410 }
411
412 FlutterTouchInterceptingView* FlutterPlatformViewsController::GetFlutterTouchInterceptingViewByID(
413 int64_t view_id) {
414 if (views_.empty()) {
415 return nil;
416 }
417 return touch_interceptors_[view_id].get();
418 }
419
420 long FlutterPlatformViewsController::FindFirstResponderPlatformViewId() {
421 for (auto const& [id, root_view] : root_views_) {
422 if ((UIView*)(root_view.get()).flt_hasFirstResponderInViewHierarchySubtree) {
423 return id;
424 }
425 }
426 return -1;
427 }
428
429 int FlutterPlatformViewsController::CountClips(const MutatorsStack& mutators_stack) {
430 std::vector<std::shared_ptr<Mutator>>::const_reverse_iterator iter = mutators_stack.Bottom();
431 int clipCount = 0;
432 while (iter != mutators_stack.Top()) {
433 if ((*iter)->IsClipType()) {
434 clipCount++;
435 }
436 ++iter;
437 }
438 return clipCount;
439 }
440
441 void FlutterPlatformViewsController::ClipViewSetMaskView(UIView* clipView) {
442 if (clipView.maskView) {
443 return;
444 }
445 UIView* flutterView = flutter_view_.get();
446 CGRect frame =
447 CGRectMake(-clipView.frame.origin.x, -clipView.frame.origin.y,
448 CGRectGetWidth(flutterView.bounds), CGRectGetHeight(flutterView.bounds));
449 clipView.maskView = [mask_view_pool_.get() getMaskViewWithFrame:frame];
450 }
451
452 // This method is only called when the `embedded_view` needs to be re-composited at the current
453 // frame. See: `CompositeWithParams` for details.
454 void FlutterPlatformViewsController::ApplyMutators(const MutatorsStack& mutators_stack,
455 UIView* embedded_view,
456 const SkRect& bounding_rect) {
457 if (flutter_view_ == nullptr) {
458 return;
459 }
460 FML_DCHECK(CATransform3DEqualToTransform(embedded_view.layer.transform, CATransform3DIdentity));
461 ResetAnchor(embedded_view.layer);
462 ChildClippingView* clipView = (ChildClippingView*)embedded_view.superview;
463
464 SkMatrix transformMatrix;
465 NSMutableArray* blurFilters = [[[NSMutableArray alloc] init] autorelease];
466 FML_DCHECK(!clipView.maskView ||
467 [clipView.maskView isKindOfClass:[FlutterClippingMaskView class]]);
468 if (clipView.maskView) {
469 [mask_view_pool_.get() insertViewToPoolIfNeeded:(FlutterClippingMaskView*)(clipView.maskView)];
470 clipView.maskView = nil;
471 }
472 CGFloat screenScale = [UIScreen mainScreen].scale;
473 auto iter = mutators_stack.Begin();
474 while (iter != mutators_stack.End()) {
475 switch ((*iter)->GetType()) {
476 case kTransform: {
477 transformMatrix.preConcat((*iter)->GetMatrix());
478 break;
479 }
480 case kClipRect: {
481 if (ClipRectContainsPlatformViewBoundingRect((*iter)->GetRect(), bounding_rect,
482 transformMatrix)) {
483 break;
484 }
485 ClipViewSetMaskView(clipView);
486 [(FlutterClippingMaskView*)clipView.maskView clipRect:(*iter)->GetRect()
487 matrix:transformMatrix];
488 break;
489 }
490 case kClipRRect: {
491 if (ClipRRectContainsPlatformViewBoundingRect((*iter)->GetRRect(), bounding_rect,
492 transformMatrix)) {
493 break;
494 }
495 ClipViewSetMaskView(clipView);
496 [(FlutterClippingMaskView*)clipView.maskView clipRRect:(*iter)->GetRRect()
497 matrix:transformMatrix];
498 break;
499 }
500 case kClipPath: {
501 // TODO(cyanglaz): Find a way to pre-determine if path contains the PlatformView boudning
502 // rect. See `ClipRRectContainsPlatformViewBoundingRect`.
503 // https://github.com/flutter/flutter/issues/118650
504 ClipViewSetMaskView(clipView);
505 [(FlutterClippingMaskView*)clipView.maskView clipPath:(*iter)->GetPath()
506 matrix:transformMatrix];
507 break;
508 }
509 case kOpacity:
510 embedded_view.alpha = (*iter)->GetAlphaFloat() * embedded_view.alpha;
511 break;
512 case kBackdropFilter: {
513 // Only support DlBlurImageFilter for BackdropFilter.
514 if (!canApplyBlurBackdrop || !(*iter)->GetFilterMutation().GetFilter().asBlur()) {
515 break;
516 }
517 CGRect filterRect =
518 flutter::GetCGRectFromSkRect((*iter)->GetFilterMutation().GetFilterRect());
519 // `filterRect` is in global coordinates. We need to convert to local space.
520 filterRect = CGRectApplyAffineTransform(
521 filterRect, CGAffineTransformMakeScale(1 / screenScale, 1 / screenScale));
522 // `filterRect` reprents the rect that should be filtered inside the `flutter_view_`.
523 // The `PlatformViewFilter` needs the frame inside the `clipView` that needs to be
524 // filtered.
525 if (CGRectIsNull(CGRectIntersection(filterRect, clipView.frame))) {
526 break;
527 }
528 CGRect intersection = CGRectIntersection(filterRect, clipView.frame);
529 CGRect frameInClipView = [flutter_view_.get() convertRect:intersection toView:clipView];
530 // sigma_x is arbitrarily chosen as the radius value because Quartz sets
531 // sigma_x and sigma_y equal to each other. DlBlurImageFilter's Tile Mode
532 // is not supported in Quartz's gaussianBlur CAFilter, so it is not used
533 // to blur the PlatformView.
534 CGFloat blurRadius = (*iter)->GetFilterMutation().GetFilter().asBlur()->sigma_x();
535 UIVisualEffectView* visualEffectView = [[[UIVisualEffectView alloc]
536 initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]] autorelease];
537 PlatformViewFilter* filter =
538 [[[PlatformViewFilter alloc] initWithFrame:frameInClipView
539 blurRadius:blurRadius
540 visualEffectView:visualEffectView] autorelease];
541 if (!filter) {
542 canApplyBlurBackdrop = NO;
543 } else {
544 [blurFilters addObject:filter];
545 }
546 break;
547 }
548 }
549 ++iter;
550 }
551
552 if (canApplyBlurBackdrop) {
553 [clipView applyBlurBackdropFilters:blurFilters];
554 }
555
556 // The UIKit frame is set based on the logical resolution (points) instead of physical.
557 // (https://developer.apple.com/library/archive/documentation/DeviceInformation/Reference/iOSDeviceCompatibility/Displays/Displays.html).
558 // However, flow is based on the physical resolution. For example, 1000 pixels in flow equals
559 // 500 points in UIKit for devices that has screenScale of 2. We need to scale the transformMatrix
560 // down to the logical resoltion before applying it to the layer of PlatformView.
561 transformMatrix.postScale(1 / screenScale, 1 / screenScale);
562
563 // Reverse the offset of the clipView.
564 // The clipView's frame includes the final translate of the final transform matrix.
565 // Thus, this translate needs to be reversed so the platform view can layout at the correct
566 // offset.
567 //
568 // Note that the transforms are not applied to the clipping paths because clipping paths happen on
569 // the mask view, whose origin is always (0,0) to the flutter_view.
570 transformMatrix.postTranslate(-clipView.frame.origin.x, -clipView.frame.origin.y);
571
572 embedded_view.layer.transform = flutter::GetCATransform3DFromSkMatrix(transformMatrix);
573 }
574
575 // Composite the PlatformView with `view_id`.
576 //
577 // Every frame, during the paint traversal of the layer tree, this method is called for all
578 // the PlatformViews in `views_to_recomposite_`.
579 //
580 // Note that `views_to_recomposite_` does not represent all the views in the view hierarchy,
581 // if a PlatformView does not change its composition parameter from last frame, it is not
582 // included in the `views_to_recomposite_`.
583 void FlutterPlatformViewsController::CompositeWithParams(int64_t view_id,
584 const EmbeddedViewParams& params) {
585 CGRect frame = CGRectMake(0, 0, params.sizePoints().width(), params.sizePoints().height());
586 FlutterTouchInterceptingView* touchInterceptor = touch_interceptors_[view_id].get();
587 #if FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG
588 FML_DCHECK(CGPointEqualToPoint([touchInterceptor embeddedView].frame.origin, CGPointZero));
589 if (non_zero_origin_views_.find(view_id) == non_zero_origin_views_.end() &&
590 !CGPointEqualToPoint([touchInterceptor embeddedView].frame.origin, CGPointZero)) {
591 non_zero_origin_views_.insert(view_id);
592 NSLog(
593 @"A Embedded PlatformView's origin is not CGPointZero.\n"
594 " View id: %@\n"
595 " View info: \n %@ \n"
596 "A non-zero origin might cause undefined behavior.\n"
597 "See https://github.com/flutter/flutter/issues/109700 for more details.\n"
598 "If you are the author of the PlatformView, please update the implementation of the "
599 "PlatformView to have a (0, 0) origin.\n"
600 "If you have a valid case of using a non-zero origin, "
601 "please leave a comment at https://github.com/flutter/flutter/issues/109700 with details.",
602 @(view_id), [touchInterceptor embeddedView]);
603 }
604 #endif
605 touchInterceptor.layer.transform = CATransform3DIdentity;
606 touchInterceptor.frame = frame;
607 touchInterceptor.alpha = 1;
608
609 const MutatorsStack& mutatorStack = params.mutatorsStack();
610 UIView* clippingView = root_views_[view_id].get();
611 // The frame of the clipping view should be the final bounding rect.
612 // Because the translate matrix in the Mutator Stack also includes the offset,
613 // when we apply the transforms matrix in |ApplyMutators|, we need
614 // to remember to do a reverse translate.
615 const SkRect& rect = params.finalBoundingRect();
616 CGFloat screenScale = [UIScreen mainScreen].scale;
617 clippingView.frame = CGRectMake(rect.x() / screenScale, rect.y() / screenScale,
618 rect.width() / screenScale, rect.height() / screenScale);
619 ApplyMutators(mutatorStack, touchInterceptor, rect);
620 }
621
622 DlCanvas* FlutterPlatformViewsController::CompositeEmbeddedView(int64_t view_id) {
623 // Any UIKit related code has to run on main thread.
624 FML_DCHECK([[NSThread currentThread] isMainThread]);
625 // Do nothing if the view doesn't need to be composited.
626 if (views_to_recomposite_.count(view_id) == 0) {
627 return slices_[view_id]->canvas();
628 }
629 CompositeWithParams(view_id, current_composition_params_[view_id]);
630 views_to_recomposite_.erase(view_id);
631 return slices_[view_id]->canvas();
632 }
633
634 void FlutterPlatformViewsController::Reset() {
635 for (int64_t view_id : active_composition_order_) {
636 UIView* sub_view = root_views_[view_id].get();
637 [sub_view removeFromSuperview];
638 }
639 root_views_.clear();
640 touch_interceptors_.clear();
641 views_.clear();
642 composition_order_.clear();
643 active_composition_order_.clear();
644 slices_.clear();
645 current_composition_params_.clear();
646 clip_count_.clear();
647 views_to_recomposite_.clear();
648 layer_pool_->RecycleLayers();
649 visited_platform_views_.clear();
650 }
651
652 SkRect FlutterPlatformViewsController::GetPlatformViewRect(int64_t view_id) {
653 UIView* platform_view = GetPlatformViewByID(view_id);
654 UIScreen* screen = [UIScreen mainScreen];
655 CGRect platform_view_cgrect = [platform_view convertRect:platform_view.bounds
656 toView:flutter_view_];
657 return SkRect::MakeXYWH(platform_view_cgrect.origin.x * screen.scale, //
658 platform_view_cgrect.origin.y * screen.scale, //
659 platform_view_cgrect.size.width * screen.scale, //
660 platform_view_cgrect.size.height * screen.scale //
661 );
662 }
663
664 bool FlutterPlatformViewsController::SubmitFrame(GrDirectContext* gr_context,
665 const std::shared_ptr<IOSContext>& ios_context,
666 std::unique_ptr<SurfaceFrame> frame) {
667 TRACE_EVENT0("flutter", "FlutterPlatformViewsController::SubmitFrame");
668
669 // Any UIKit related code has to run on main thread.
670 FML_DCHECK([[NSThread currentThread] isMainThread]);
671 if (flutter_view_ == nullptr) {
672 return frame->Submit();
673 }
674
675 DisposeViews();
676
677 DlCanvas* background_canvas = frame->Canvas();
678
679 // Resolve all pending GPU operations before allocating a new surface.
680 background_canvas->Flush();
681
682 // Clipping the background canvas before drawing the picture recorders requires
683 // saving and restoring the clip context.
684 DlAutoCanvasRestore save(background_canvas, /*doSave=*/true);
685
686 // Maps a platform view id to a vector of `FlutterPlatformViewLayer`.
687 LayersMap platform_view_layers;
688
689 auto did_submit = true;
690 auto num_platform_views = composition_order_.size();
691
692 for (size_t i = 0; i < num_platform_views; i++) {
693 int64_t platform_view_id = composition_order_[i];
694 EmbedderViewSlice* slice = slices_[platform_view_id].get();
695 slice->end_recording();
696
697 // Check if the current picture contains overlays that intersect with the
698 // current platform view or any of the previous platform views.
699 for (size_t j = i + 1; j > 0; j--) {
700 int64_t current_platform_view_id = composition_order_[j - 1];
701 SkRect platform_view_rect = GetPlatformViewRect(current_platform_view_id);
702 std::list<SkRect> intersection_rects =
703 slice->searchNonOverlappingDrawnRects(platform_view_rect);
704 auto allocation_size = intersection_rects.size();
705
706 // For testing purposes, the overlay id is used to find the overlay view.
707 // This is the index of the layer for the current platform view.
708 auto overlay_id = platform_view_layers[current_platform_view_id].size();
709
710 // If the max number of allocations per platform view is exceeded,
711 // then join all the rects into a single one.
712 //
713 // TODO(egarciad): Consider making this configurable.
714 // https://github.com/flutter/flutter/issues/52510
715 if (allocation_size > kMaxLayerAllocations) {
716 SkRect joined_rect = SkRect::MakeEmpty();
717 for (const SkRect& rect : intersection_rects) {
718 joined_rect.join(rect);
719 }
720 // Replace the rects in the intersection rects list for a single rect that is
721 // the union of all the rects in the list.
722 intersection_rects.clear();
723 intersection_rects.push_back(joined_rect);
724 }
725 for (SkRect& joined_rect : intersection_rects) {
726 // Get the intersection rect between the current rect
727 // and the platform view rect.
728 joined_rect.intersect(platform_view_rect);
729 // Subpixels in the platform may not align with the canvas subpixels.
730 // To workaround it, round the floating point bounds and make the rect slightly larger.
731 // For example, {0.3, 0.5, 3.1, 4.7} becomes {0, 0, 4, 5}.
732 joined_rect.setLTRB(std::floor(joined_rect.left()), std::floor(joined_rect.top()),
733 std::ceil(joined_rect.right()), std::ceil(joined_rect.bottom()));
734 // Clip the background canvas, so it doesn't contain any of the pixels drawn
735 // on the overlay layer.
736 background_canvas->ClipRect(joined_rect, DlCanvas::ClipOp::kDifference);
737 // Get a new host layer.
738 std::shared_ptr<FlutterPlatformViewLayer> layer = GetLayer(gr_context, //
739 ios_context, //
740 slice, //
741 joined_rect, //
742 current_platform_view_id, //
743 overlay_id //
744 );
745 did_submit &= layer->did_submit_last_frame;
746 platform_view_layers[current_platform_view_id].push_back(layer);
747 overlay_id++;
748 }
749 }
750 slice->render_into(background_canvas);
751 }
752
753 // Manually trigger the SkAutoCanvasRestore before we submit the frame
754 save.Restore();
755
756 // If a layer was allocated in the previous frame, but it's not used in the current frame,
757 // then it can be removed from the scene.
758 RemoveUnusedLayers();
759 // Organize the layers by their z indexes.
760 BringLayersIntoView(platform_view_layers);
761 // Mark all layers as available, so they can be used in the next frame.
762 layer_pool_->RecycleLayers();
763
764 did_submit &= frame->Submit();
765
766 // If the frame is submitted with embedded platform views,
767 // there should be a |[CATransaction begin]| call in this frame prior to all the drawing.
768 // If that case, we need to commit the transaction.
769 CommitCATransactionIfNeeded();
770 return did_submit;
771 }
772
773 void FlutterPlatformViewsController::BringLayersIntoView(LayersMap layer_map) {
774 FML_DCHECK(flutter_view_);
775 UIView* flutter_view = flutter_view_.get();
776 // Clear the `active_composition_order_`, which will be populated down below.
777 active_composition_order_.clear();
778 NSMutableArray* desired_platform_subviews = [NSMutableArray array];
779 for (size_t i = 0; i < composition_order_.size(); i++) {
780 int64_t platform_view_id = composition_order_[i];
781 std::vector<std::shared_ptr<FlutterPlatformViewLayer>> layers = layer_map[platform_view_id];
782 UIView* platform_view_root = root_views_[platform_view_id].get();
783 [desired_platform_subviews addObject:platform_view_root];
784 for (const std::shared_ptr<FlutterPlatformViewLayer>& layer : layers) {
785 [desired_platform_subviews addObject:layer->overlay_view_wrapper];
786 }
787 active_composition_order_.push_back(platform_view_id);
788 }
789
790 NSSet* desired_platform_subviews_set = [NSSet setWithArray:desired_platform_subviews];
791 NSArray* existing_platform_subviews = [flutter_view.subviews
792 filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id object,
793 NSDictionary* bindings) {
794 return [desired_platform_subviews_set containsObject:object];
795 }]];
796 // Manipulate view hierarchy only if needed, to address a performance issue where
797 // `BringLayersIntoView` is called even when view hierarchy stays the same.
798 // See: https://github.com/flutter/flutter/issues/121833
799 // TODO(hellohuanlin): investigate if it is possible to skip unnecessary BringLayersIntoView.
800 if (![desired_platform_subviews isEqualToArray:existing_platform_subviews]) {
801 for (UIView* subview in desired_platform_subviews) {
802 // `addSubview` will automatically reorder subview if it is already added.
803 [flutter_view addSubview:subview];
804 }
805 }
806 }
807
808 std::shared_ptr<FlutterPlatformViewLayer> FlutterPlatformViewsController::GetLayer(
809 GrDirectContext* gr_context,
810 const std::shared_ptr<IOSContext>& ios_context,
811 EmbedderViewSlice* slice,
812 SkRect rect,
813 int64_t view_id,
814 int64_t overlay_id) {
815 FML_DCHECK(flutter_view_);
816 std::shared_ptr<FlutterPlatformViewLayer> layer = layer_pool_->GetLayer(gr_context, ios_context);
817
818 UIView* overlay_view_wrapper = layer->overlay_view_wrapper.get();
819 auto screenScale = [UIScreen mainScreen].scale;
820 // Set the size of the overlay view wrapper.
821 // This wrapper view masks the overlay view.
822 overlay_view_wrapper.frame = CGRectMake(rect.x() / screenScale, rect.y() / screenScale,
823 rect.width() / screenScale, rect.height() / screenScale);
824 // Set a unique view identifier, so the overlay_view_wrapper can be identified in XCUITests.
825 overlay_view_wrapper.accessibilityIdentifier =
826 [NSString stringWithFormat:@"platform_view[%lld].overlay[%lld]", view_id, overlay_id];
827
828 UIView* overlay_view = layer->overlay_view.get();
829 // Set the size of the overlay view.
830 // This size is equal to the device screen size.
831 overlay_view.frame = [flutter_view_.get() convertRect:flutter_view_.get().bounds
832 toView:overlay_view_wrapper];
833 // Set a unique view identifier, so the overlay_view can be identified in XCUITests.
834 overlay_view.accessibilityIdentifier =
835 [NSString stringWithFormat:@"platform_view[%lld].overlay_view[%lld]", view_id, overlay_id];
836
837 std::unique_ptr<SurfaceFrame> frame = layer->surface->AcquireFrame(frame_size_);
838 // If frame is null, AcquireFrame already printed out an error message.
839 if (!frame) {
840 return layer;
841 }
842 DlCanvas* overlay_canvas = frame->Canvas();
843 int restore_count = overlay_canvas->GetSaveCount();
844 overlay_canvas->Save();
845 overlay_canvas->ClipRect(rect);
846 overlay_canvas->Clear(DlColor::kTransparent());
847 slice->render_into(overlay_canvas);
848 overlay_canvas->RestoreToCount(restore_count);
849
850 layer->did_submit_last_frame = frame->Submit();
851 return layer;
852 }
853
854 void FlutterPlatformViewsController::RemoveUnusedLayers() {
855 std::vector<std::shared_ptr<FlutterPlatformViewLayer>> layers = layer_pool_->GetUnusedLayers();
856 for (const std::shared_ptr<FlutterPlatformViewLayer>& layer : layers) {
857 [layer->overlay_view_wrapper removeFromSuperview];
858 }
859
860 std::unordered_set<int64_t> composition_order_set;
861 for (int64_t view_id : composition_order_) {
862 composition_order_set.insert(view_id);
863 }
864 // Remove unused platform views.
865 for (int64_t view_id : active_composition_order_) {
866 if (composition_order_set.find(view_id) == composition_order_set.end()) {
867 UIView* platform_view_root = root_views_[view_id].get();
868 [platform_view_root removeFromSuperview];
869 }
870 }
871 }
872
873 void FlutterPlatformViewsController::DisposeViews() {
874 if (views_to_dispose_.empty()) {
875 return;
876 }
877
878 FML_DCHECK([[NSThread currentThread] isMainThread]);
879
880 std::unordered_set<int64_t> views_to_composite(composition_order_.begin(),
881 composition_order_.end());
882 std::unordered_set<int64_t> views_to_delay_dispose;
883 for (int64_t viewId : views_to_dispose_) {
884 if (views_to_composite.count(viewId)) {
885 views_to_delay_dispose.insert(viewId);
886 continue;
887 }
888 UIView* root_view = root_views_[viewId].get();
889 [root_view removeFromSuperview];
890 views_.erase(viewId);
891 touch_interceptors_.erase(viewId);
892 root_views_.erase(viewId);
893 current_composition_params_.erase(viewId);
894 clip_count_.erase(viewId);
895 views_to_recomposite_.erase(viewId);
896 }
897
898 views_to_dispose_ = std::move(views_to_delay_dispose);
899 }
900
901 void FlutterPlatformViewsController::BeginCATransaction() {
902 FML_DCHECK([[NSThread currentThread] isMainThread]);
903 FML_DCHECK(!catransaction_added_);
904 [CATransaction begin];
905 catransaction_added_ = true;
906 }
907
908 void FlutterPlatformViewsController::CommitCATransactionIfNeeded() {
909 if (catransaction_added_) {
910 FML_DCHECK([[NSThread currentThread] isMainThread]);
911 [CATransaction commit];
912 catransaction_added_ = false;
913 }
914 }
915
916 void FlutterPlatformViewsController::ResetFrameState() {
917 slices_.clear();
918 composition_order_.clear();
919 visited_platform_views_.clear();
920 }
921
922 } // namespace flutter
923
924 // This recognizers delays touch events from being dispatched to the responder chain until it failed
925 // recognizing a gesture.
926 //
927 // We only fail this recognizer when asked to do so by the Flutter framework (which does so by
928 // invoking an acceptGesture method on the platform_views channel). And this is how we allow the
929 // Flutter framework to delay or prevent the embedded view from getting a touch sequence.
930 @interface DelayingGestureRecognizer : UIGestureRecognizer <UIGestureRecognizerDelegate>
931
932 // Indicates that if the `DelayingGestureRecognizer`'s state should be set to
933 // `UIGestureRecognizerStateEnded` during next `touchesEnded` call.
934 @property(nonatomic) bool shouldEndInNextTouchesEnded;
935
936 // Indicates that the `DelayingGestureRecognizer`'s `touchesEnded` has been invoked without
937 // setting the state to `UIGestureRecognizerStateEnded`.
938 @property(nonatomic) bool touchedEndedWithoutBlocking;
939
940 - (instancetype)initWithTarget:(id)target
941 action:(SEL)action
942 forwardingRecognizer:(UIGestureRecognizer*)forwardingRecognizer;
943 @end
944
945 // While the DelayingGestureRecognizer is preventing touches from hitting the responder chain
946 // the touch events are not arriving to the FlutterView (and thus not arriving to the Flutter
947 // framework). We use this gesture recognizer to dispatch the events directly to the FlutterView
948 // while during this phase.
949 //
950 // If the Flutter framework decides to dispatch events to the embedded view, we fail the
951 // DelayingGestureRecognizer which sends the events up the responder chain. But since the events
952 // are handled by the embedded view they are not delivered to the Flutter framework in this phase
953 // as well. So during this phase as well the ForwardingGestureRecognizer dispatched the events
954 // directly to the FlutterView.
955 @interface ForwardingGestureRecognizer : UIGestureRecognizer <UIGestureRecognizerDelegate>
956 - (instancetype)initWithTarget:(id)target
957 platformViewsController:
958 (fml::WeakPtr<flutter::FlutterPlatformViewsController>)platformViewsController;
959 @end
960
961 @implementation FlutterTouchInterceptingView {
962 fml::scoped_nsobject<DelayingGestureRecognizer> _delayingRecognizer;
963 FlutterPlatformViewGestureRecognizersBlockingPolicy _blockingPolicy;
964 UIView* _embeddedView;
965 // The used as the accessiblityContainer.
966 // The `accessiblityContainer` is used in UIKit to determine the parent of this accessibility
967 // node.
968 NSObject* _flutterAccessibilityContainer;
969 }
970 - (instancetype)initWithEmbeddedView:(UIView*)embeddedView
971 platformViewsController:
972 (fml::WeakPtr<flutter::FlutterPlatformViewsController>)platformViewsController
973 gestureRecognizersBlockingPolicy:
974 (FlutterPlatformViewGestureRecognizersBlockingPolicy)blockingPolicy {
975 self = [super initWithFrame:embeddedView.frame];
976 if (self) {
977 self.multipleTouchEnabled = YES;
978 _embeddedView = embeddedView;
979 embeddedView.autoresizingMask =
980 (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight);
981
982 [self addSubview:embeddedView];
983
984 ForwardingGestureRecognizer* forwardingRecognizer = [[[ForwardingGestureRecognizer alloc]
985 initWithTarget:self
986 platformViewsController:std::move(platformViewsController)] autorelease];
987
988 _delayingRecognizer.reset([[DelayingGestureRecognizer alloc]
989 initWithTarget:self
990 action:nil
991 forwardingRecognizer:forwardingRecognizer]);
992 _blockingPolicy = blockingPolicy;
993
994 [self addGestureRecognizer:_delayingRecognizer.get()];
995 [self addGestureRecognizer:forwardingRecognizer];
996 }
997 return self;
998 }
999
1000 - (UIView*)embeddedView {
1001 return [[_embeddedView retain] autorelease];
1002 }
1003
1004 - (void)releaseGesture {
1005 _delayingRecognizer.get().state = UIGestureRecognizerStateFailed;
1006 }
1007
1008 - (void)blockGesture {
1009 switch (_blockingPolicy) {
1010 case FlutterPlatformViewGestureRecognizersBlockingPolicyEager:
1011 // We block all other gesture recognizers immediately in this policy.
1012 _delayingRecognizer.get().state = UIGestureRecognizerStateEnded;
1013 break;
1014 case FlutterPlatformViewGestureRecognizersBlockingPolicyWaitUntilTouchesEnded:
1015 if (_delayingRecognizer.get().touchedEndedWithoutBlocking) {
1016 // If touchesEnded of the `DelayingGesureRecognizer` has been already invoked,
1017 // we want to set the state of the `DelayingGesureRecognizer` to
1018 // `UIGestureRecognizerStateEnded` as soon as possible.
1019 _delayingRecognizer.get().state = UIGestureRecognizerStateEnded;
1020 } else {
1021 // If touchesEnded of the `DelayingGesureRecognizer` has not been invoked,
1022 // We will set a flag to notify the `DelayingGesureRecognizer` to set the state to
1023 // `UIGestureRecognizerStateEnded` when touchesEnded is called.
1024 _delayingRecognizer.get().shouldEndInNextTouchesEnded = YES;
1025 }
1026 break;
1027 default:
1028 break;
1029 }
1030 }
1031
1032 // We want the intercepting view to consume the touches and not pass the touches up to the parent
1033 // view. Make the touch event method not call super will not pass the touches up to the parent view.
1034 // Hence we overide the touch event methods and do nothing.
1035 - (void)touchesBegan:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
1036 }
1037
1038 - (void)touchesMoved:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
1039 }
1040
1041 - (void)touchesCancelled:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
1042 }
1043
1044 - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
1045 }
1046
1047 - (void)setFlutterAccessibilityContainer:(NSObject*)flutterAccessibilityContainer {
1048 _flutterAccessibilityContainer = flutterAccessibilityContainer;
1049 }
1050
1051 - (id)accessibilityContainer {
1052 return _flutterAccessibilityContainer;
1053 }
1054
1055 @end
1056
1057 @implementation DelayingGestureRecognizer {
1058 fml::scoped_nsobject<UIGestureRecognizer> _forwardingRecognizer;
1059 }
1060
1061 - (instancetype)initWithTarget:(id)target
1062 action:(SEL)action
1063 forwardingRecognizer:(UIGestureRecognizer*)forwardingRecognizer {
1064 self = [super initWithTarget:target action:action];
1065 if (self) {
1066 self.delaysTouchesBegan = YES;
1067 self.delaysTouchesEnded = YES;
1068 self.delegate = self;
1069 self.shouldEndInNextTouchesEnded = NO;
1070 self.touchedEndedWithoutBlocking = NO;
1071 _forwardingRecognizer.reset([forwardingRecognizer retain]);
1072 }
1073 return self;
1074 }
1075
1076 - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
1077 shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer*)otherGestureRecognizer {
1078 // The forwarding gesture recognizer should always get all touch events, so it should not be
1079 // required to fail by any other gesture recognizer.
1080 return otherGestureRecognizer != _forwardingRecognizer.get() && otherGestureRecognizer != self;
1081 }
1082
1083 - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
1084 shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer*)otherGestureRecognizer {
1085 return otherGestureRecognizer == self;
1086 }
1087
1088 - (void)touchesBegan:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
1089 self.touchedEndedWithoutBlocking = NO;
1090 [super touchesBegan:touches withEvent:event];
1091 }
1092
1093 - (void)touchesEnded:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
1094 if (self.shouldEndInNextTouchesEnded) {
1095 self.state = UIGestureRecognizerStateEnded;
1096 self.shouldEndInNextTouchesEnded = NO;
1097 } else {
1098 self.touchedEndedWithoutBlocking = YES;
1099 }
1100 [super touchesEnded:touches withEvent:event];
1101 }
1102
1103 - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
1104 self.state = UIGestureRecognizerStateFailed;
1105 }
1106 @end
1107
1108 @implementation ForwardingGestureRecognizer {
1109 // Weak reference to FlutterPlatformViewsController. The FlutterPlatformViewsController has
1110 // a reference to the FlutterViewController, where we can dispatch pointer events to.
1111 //
1112 // The lifecycle of FlutterPlatformViewsController is bind to FlutterEngine, which should always
1113 // outlives the FlutterViewController. And ForwardingGestureRecognizer is owned by a subview of
1114 // FlutterView, so the ForwardingGestureRecognizer never out lives FlutterViewController.
1115 // Therefore, `_platformViewsController` should never be nullptr.
1116 fml::WeakPtr<flutter::FlutterPlatformViewsController> _platformViewsController;
1117 // Counting the pointers that has started in one touch sequence.
1118 NSInteger _currentTouchPointersCount;
1119 // We can't dispatch events to the framework without this back pointer.
1120 // This gesture recognizer retains the `FlutterViewController` until the
1121 // end of a gesture sequence, that is all the touches in touchesBegan are concluded
1122 // with |touchesCancelled| or |touchesEnded|.
1123 fml::scoped_nsobject<UIViewController> _flutterViewController;
1124 }
1125
1126 - (instancetype)initWithTarget:(id)target
1127 platformViewsController:
1128 (fml::WeakPtr<flutter::FlutterPlatformViewsController>)platformViewsController {
1129 self = [super initWithTarget:target action:nil];
1130 if (self) {
1131 self.delegate = self;
1132 FML_DCHECK(platformViewsController.get() != nullptr);
1133 _platformViewsController = std::move(platformViewsController);
1134 _currentTouchPointersCount = 0;
1135 }
1136 return self;
1137 }
1138
1139 - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
1140 FML_DCHECK(_currentTouchPointersCount >= 0);
1141 if (_currentTouchPointersCount == 0) {
1142 // At the start of each gesture sequence, we reset the `_flutterViewController`,
1143 // so that all the touch events in the same sequence are forwarded to the same
1144 // `_flutterViewController`.
1145 _flutterViewController.reset([_platformViewsController->getFlutterViewController() retain]);
1146 }
1147 [_flutterViewController.get() touchesBegan:touches withEvent:event];
1148 _currentTouchPointersCount += touches.count;
1149 }
1150
1151 - (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {
1152 [_flutterViewController.get() touchesMoved:touches withEvent:event];
1153 }
1154
1155 - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
1156 [_flutterViewController.get() touchesEnded:touches withEvent:event];
1157 _currentTouchPointersCount -= touches.count;
1158 // Touches in one touch sequence are sent to the touchesEnded method separately if different
1159 // fingers stop touching the screen at different time. So one touchesEnded method triggering does
1160 // not necessarially mean the touch sequence has ended. We Only set the state to
1161 // UIGestureRecognizerStateFailed when all the touches in the current touch sequence is ended.
1162 if (_currentTouchPointersCount == 0) {
1163 self.state = UIGestureRecognizerStateFailed;
1164 _flutterViewController.reset(nil);
1165 }
1166 }
1167
1168 - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
1169 // In the event of platform view is removed, iOS generates a "stationary" change type instead of
1170 // "cancelled" change type.
1171 // Flutter needs all the cancelled touches to be "cancelled" change types in order to correctly
1172 // handle gesture sequence.
1173 // We always override the change type to "cancelled".
1174 [((FlutterViewController*)_flutterViewController.get()) forceTouchesCancelled:touches];
1175 _currentTouchPointersCount -= touches.count;
1176 if (_currentTouchPointersCount == 0) {
1177 self.state = UIGestureRecognizerStateFailed;
1178 _flutterViewController.reset(nil);
1179 }
1180 }
1181
1182 - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
1183 shouldRecognizeSimultaneouslyWithGestureRecognizer:
1184 (UIGestureRecognizer*)otherGestureRecognizer {
1185 return YES;
1186 }
1187 @end
DelayingGestureRecognizer::touchedEndedWithoutBlocking
bool touchedEndedWithoutBlocking
Definition: FlutterPlatformViews.mm:938
UIView(FirstResponder)::flt_hasFirstResponderInViewHierarchySubtree
BOOL flt_hasFirstResponderInViewHierarchySubtree
Definition: FlutterPlatformViews_Internal.h:442
FlutterPlatformViewGestureRecognizersBlockingPolicyWaitUntilTouchesEnded
@ FlutterPlatformViewGestureRecognizersBlockingPolicyWaitUntilTouchesEnded
Definition: FlutterPlugin.h:268
FlutterViewController
Definition: FlutterViewController.h:55
-[FlutterTouchInterceptingView blockGesture]
void blockGesture()
Definition: FlutterPlatformViews.mm:1008
FlutterMethodNotImplemented
FLUTTER_DARWIN_EXPORT NSObject const * FlutterMethodNotImplemented
_flutterViewController
fml::scoped_nsobject< UIViewController > _flutterViewController
Definition: FlutterPlatformViews.mm:1123
-[FlutterTouchInterceptingView releaseGesture]
void releaseGesture()
Definition: FlutterPlatformViews.mm:1004
flutter::IOSSurface::CreateGPUSurface
virtual std::unique_ptr< Surface > CreateGPUSurface(GrDirectContext *gr_context=nullptr)=0
_currentTouchPointersCount
NSInteger _currentTouchPointersCount
Definition: FlutterPlatformViews.mm:1108
FlutterError
Definition: FlutterCodecs.h:246
ForwardingGestureRecognizer
Definition: FlutterPlatformViews.mm:955
FlutterChannels.h
-[FlutterTouchInterceptingView embeddedView]
UIView * embeddedView()
Definition: FlutterPlatformViews.mm:1000
-[ChildClippingView applyBlurBackdropFilters:]
void applyBlurBackdropFilters:(NSArray< PlatformViewFilter * > *filters)
Definition: FlutterPlatformViews_Internal.mm:203
flutter::canApplyBlurBackdrop
BOOL canApplyBlurBackdrop
Definition: FlutterPlatformViews.mm:84
flutter::ResetAnchor
void ResetAnchor(CALayer *layer)
Definition: FlutterPlatformViews_Internal.mm:56
platform_view
std::unique_ptr< flutter::PlatformViewIOS > platform_view
Definition: FlutterEnginePlatformViewTest.mm:61
_blockingPolicy
FlutterPlatformViewGestureRecognizersBlockingPolicy _blockingPolicy
Definition: FlutterPlatformViews.mm:961
_flutterAccessibilityContainer
NSObject * _flutterAccessibilityContainer
Definition: FlutterPlatformViews.mm:968
flutter::IOSSurface
Definition: ios_surface.h:25
action
SemanticsAction action
Definition: SemanticsObjectTestMocks.h:21
_platformViewsController
std::shared_ptr< flutter::FlutterPlatformViewsController > _platformViewsController
Definition: FlutterEngine.mm:124
ios_surface.h
FlutterMethodCall
Definition: FlutterCodecs.h:220
FlutterPlatformViewGestureRecognizersBlockingPolicyEager
@ FlutterPlatformViewGestureRecognizersBlockingPolicyEager
Definition: FlutterPlugin.h:260
flutter
Definition: accessibility_bridge.h:28
-[flutter::FlutterPlatformViewsController GetFlutterTouchInterceptingViewByID]
FlutterTouchInterceptingView * GetFlutterTouchInterceptingViewByID(int64_t view_id)
Definition: FlutterPlatformViews.mm:412
FlutterOverlayView.h
FlutterPlatformViews_Internal.h
FlutterResult
void(^ FlutterResult)(id _Nullable result)
Definition: FlutterChannels.h:196
FlutterPlatformViewGestureRecognizersBlockingPolicy
FlutterPlatformViewGestureRecognizersBlockingPolicy
Definition: FlutterPlugin.h:252
DelayingGestureRecognizer
Definition: FlutterPlatformViews.mm:930
UIView(FirstResponder)
Definition: FlutterPlatformViews.mm:20
ChildClippingView
Definition: FlutterPlatformViews_Internal.h:116
FlutterOverlayView
Definition: FlutterOverlayView.h:29
FlutterViewController_Internal.h
FlutterStandardTypedData
Definition: FlutterCodecs.h:298
ClipRRectContainsPlatformViewBoundingRect
static bool ClipRRectContainsPlatformViewBoundingRect(const SkRRect &clip_rrect, const SkRect &platformview_boundingrect, const SkMatrix &transform_matrix)
Definition: FlutterPlatformViews.mm:57
FlutterTouchInterceptingView
Definition: FlutterPlatformViews.mm:961
flutter::GetCATransform3DFromSkMatrix
CATransform3D GetCATransform3DFromSkMatrix(const SkMatrix &matrix)
Definition: FlutterPlatformViews_Internal.mm:41
flutter::GetCGRectFromSkRect
CGRect GetCGRectFromSkRect(const SkRect &clipSkRect)
Definition: FlutterPlatformViews_Internal.mm:62
PlatformViewFilter
Definition: FlutterPlatformViews_Internal.h:77
id
int32_t id
Definition: SemanticsObjectTestMocks.h:20
DelayingGestureRecognizer::shouldEndInNextTouchesEnded
bool shouldEndInNextTouchesEnded
Definition: FlutterPlatformViews.mm:934
ClipRectContainsPlatformViewBoundingRect
static bool ClipRectContainsPlatformViewBoundingRect(const SkRect &clip_rect, const SkRect &platformview_boundingrect, const SkMatrix &transform_matrix)
Definition: FlutterPlatformViews.mm:42
FlutterMethodCall::arguments
id arguments
Definition: FlutterCodecs.h:238
FlutterClippingMaskView
Definition: FlutterPlatformViews_Internal.h:28
_embeddedView
UIView * _embeddedView
Definition: FlutterPlatformViews.mm:964