blob: fdd540c2aa658e83e1923268cd2b3bffc1520fe6 [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import <UIKit/UIGestureRecognizerSubclass.h>
#include <list>
#include <map>
#include <memory>
#include <string>
#include "flutter/common/graphics/persistent_cache.h"
#include "flutter/flow/rtree.h"
#include "flutter/fml/platform/darwin/scoped_nsobject.h"
#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterChannels.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterOverlayView.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h"
#import "flutter/shell/platform/darwin/ios/ios_surface.h"
#import "flutter/shell/platform/darwin/ios/ios_surface_gl.h"
namespace flutter {
std::shared_ptr<FlutterPlatformViewLayer> FlutterPlatformViewLayerPool::GetLayer(
GrDirectContext* gr_context,
std::shared_ptr<IOSContext> ios_context) {
if (available_layer_index_ >= layers_.size()) {
std::shared_ptr<FlutterPlatformViewLayer> layer;
fml::scoped_nsobject<FlutterOverlayView> overlay_view;
fml::scoped_nsobject<FlutterOverlayView> overlay_view_wrapper;
if (!gr_context) {
overlay_view.reset([[FlutterOverlayView alloc] init]);
overlay_view_wrapper.reset([[FlutterOverlayView alloc] init]);
auto ca_layer = fml::scoped_nsobject<CALayer>{[[overlay_view.get() layer] retain]};
std::unique_ptr<IOSSurface> ios_surface = IOSSurface::Create(ios_context, ca_layer);
std::unique_ptr<Surface> surface = ios_surface->CreateGPUSurface();
layer = std::make_shared<FlutterPlatformViewLayer>(
std::move(overlay_view), std::move(overlay_view_wrapper), std::move(ios_surface),
std::move(surface));
} else {
CGFloat screenScale = [UIScreen mainScreen].scale;
overlay_view.reset([[FlutterOverlayView alloc] initWithContentsScale:screenScale]);
overlay_view_wrapper.reset([[FlutterOverlayView alloc] initWithContentsScale:screenScale]);
auto ca_layer = fml::scoped_nsobject<CALayer>{[[overlay_view.get() layer] retain]};
std::unique_ptr<IOSSurface> ios_surface = IOSSurface::Create(ios_context, ca_layer);
std::unique_ptr<Surface> surface = ios_surface->CreateGPUSurface(gr_context);
layer = std::make_shared<FlutterPlatformViewLayer>(
std::move(overlay_view), std::move(overlay_view_wrapper), std::move(ios_surface),
std::move(surface));
layer->gr_context = gr_context;
}
// The overlay view wrapper masks the overlay view.
// This is required to keep the backing surface size unchanged between frames.
//
// Otherwise, changing the size of the overlay would require a new surface,
// which can be very expensive.
//
// This is the case of an animation in which the overlay size is changing in every frame.
//
// +------------------------+
// | overlay_view |
// | +--------------+ | +--------------+
// | | wrapper | | == mask => | overlay_view |
// | +--------------+ | +--------------+
// +------------------------+
overlay_view_wrapper.get().clipsToBounds = YES;
[overlay_view_wrapper.get() addSubview:overlay_view];
layers_.push_back(layer);
}
std::shared_ptr<FlutterPlatformViewLayer> layer = layers_[available_layer_index_];
if (gr_context != layer->gr_context) {
layer->gr_context = gr_context;
// The overlay already exists, but the GrContext was changed so we need to recreate
// the rendering surface with the new GrContext.
IOSSurface* ios_surface = layer->ios_surface.get();
std::unique_ptr<Surface> surface = ios_surface->CreateGPUSurface(gr_context);
layer->surface = std::move(surface);
}
available_layer_index_++;
return layer;
}
void FlutterPlatformViewLayerPool::RecycleLayers() {
available_layer_index_ = 0;
}
std::vector<std::shared_ptr<FlutterPlatformViewLayer>>
FlutterPlatformViewLayerPool::GetUnusedLayers() {
std::vector<std::shared_ptr<FlutterPlatformViewLayer>> results;
for (size_t i = available_layer_index_; i < layers_.size(); i++) {
results.push_back(layers_[i]);
}
return results;
}
void FlutterPlatformViewsController::SetFlutterView(UIView* flutter_view) {
flutter_view_.reset([flutter_view retain]);
}
void FlutterPlatformViewsController::SetFlutterViewController(
UIViewController* flutter_view_controller) {
flutter_view_controller_.reset([flutter_view_controller retain]);
}
UIViewController* FlutterPlatformViewsController::getFlutterViewController() {
return flutter_view_controller_.get();
}
void FlutterPlatformViewsController::OnMethodCall(FlutterMethodCall* call, FlutterResult& result) {
if ([[call method] isEqualToString:@"create"]) {
OnCreate(call, result);
} else if ([[call method] isEqualToString:@"dispose"]) {
OnDispose(call, result);
} else if ([[call method] isEqualToString:@"acceptGesture"]) {
OnAcceptGesture(call, result);
} else if ([[call method] isEqualToString:@"rejectGesture"]) {
OnRejectGesture(call, result);
} else {
result(FlutterMethodNotImplemented);
}
}
void FlutterPlatformViewsController::OnCreate(FlutterMethodCall* call, FlutterResult& result) {
NSDictionary<NSString*, id>* args = [call arguments];
long viewId = [args[@"id"] longValue];
std::string viewType([args[@"viewType"] UTF8String]);
if (views_.count(viewId) != 0) {
result([FlutterError errorWithCode:@"recreating_view"
message:@"trying to create an already created view"
details:[NSString stringWithFormat:@"view id: '%ld'", viewId]]);
}
NSObject<FlutterPlatformViewFactory>* factory = factories_[viewType].get();
if (factory == nil) {
result([FlutterError errorWithCode:@"unregistered_view_type"
message:@"trying to create a view with an unregistered type"
details:[NSString stringWithFormat:@"unregistered view type: '%@'",
args[@"viewType"]]]);
return;
}
id params = nil;
if ([factory respondsToSelector:@selector(createArgsCodec)]) {
NSObject<FlutterMessageCodec>* codec = [factory createArgsCodec];
if (codec != nil && args[@"params"] != nil) {
FlutterStandardTypedData* paramsData = args[@"params"];
params = [codec decode:paramsData.data];
}
}
NSObject<FlutterPlatformView>* embedded_view = [factory createWithFrame:CGRectZero
viewIdentifier:viewId
arguments:params];
UIView* platform_view = [embedded_view view];
// Set a unique view identifier, so the platform view can be identified in unit tests.
platform_view.accessibilityIdentifier = [NSString stringWithFormat:@"platform_view[%ld]", viewId];
views_[viewId] = fml::scoped_nsobject<NSObject<FlutterPlatformView>>([embedded_view retain]);
FlutterTouchInterceptingView* touch_interceptor = [[[FlutterTouchInterceptingView alloc]
initWithEmbeddedView:platform_view
platformViewsController:GetWeakPtr()
gestureRecognizersBlockingPolicy:gesture_recognizers_blocking_policies[viewType]]
autorelease];
touch_interceptors_[viewId] =
fml::scoped_nsobject<FlutterTouchInterceptingView>([touch_interceptor retain]);
ChildClippingView* clipping_view =
[[[ChildClippingView alloc] initWithFrame:CGRectZero] autorelease];
[clipping_view addSubview:touch_interceptor];
root_views_[viewId] = fml::scoped_nsobject<UIView>([clipping_view retain]);
result(nil);
}
void FlutterPlatformViewsController::OnDispose(FlutterMethodCall* call, FlutterResult& result) {
NSNumber* arg = [call arguments];
int64_t viewId = [arg longLongValue];
if (views_.count(viewId) == 0) {
result([FlutterError errorWithCode:@"unknown_view"
message:@"trying to dispose an unknown"
details:[NSString stringWithFormat:@"view id: '%lld'", viewId]]);
return;
}
// We wait for next submitFrame to dispose views.
views_to_dispose_.insert(viewId);
result(nil);
}
void FlutterPlatformViewsController::OnAcceptGesture(FlutterMethodCall* call,
FlutterResult& result) {
NSDictionary<NSString*, id>* args = [call arguments];
int64_t viewId = [args[@"id"] longLongValue];
if (views_.count(viewId) == 0) {
result([FlutterError errorWithCode:@"unknown_view"
message:@"trying to set gesture state for an unknown view"
details:[NSString stringWithFormat:@"view id: '%lld'", viewId]]);
return;
}
FlutterTouchInterceptingView* view = touch_interceptors_[viewId].get();
[view releaseGesture];
result(nil);
}
void FlutterPlatformViewsController::OnRejectGesture(FlutterMethodCall* call,
FlutterResult& result) {
NSDictionary<NSString*, id>* args = [call arguments];
int64_t viewId = [args[@"id"] longLongValue];
if (views_.count(viewId) == 0) {
result([FlutterError errorWithCode:@"unknown_view"
message:@"trying to set gesture state for an unknown view"
details:[NSString stringWithFormat:@"view id: '%lld'", viewId]]);
return;
}
FlutterTouchInterceptingView* view = touch_interceptors_[viewId].get();
[view blockGesture];
result(nil);
}
void FlutterPlatformViewsController::RegisterViewFactory(
NSObject<FlutterPlatformViewFactory>* factory,
NSString* factoryId,
FlutterPlatformViewGestureRecognizersBlockingPolicy gestureRecognizerBlockingPolicy) {
std::string idString([factoryId UTF8String]);
FML_CHECK(factories_.count(idString) == 0);
factories_[idString] =
fml::scoped_nsobject<NSObject<FlutterPlatformViewFactory>>([factory retain]);
gesture_recognizers_blocking_policies[idString] = gestureRecognizerBlockingPolicy;
}
void FlutterPlatformViewsController::BeginFrame(SkISize frame_size) {
ResetFrameState();
frame_size_ = frame_size;
}
void FlutterPlatformViewsController::CancelFrame() {
ResetFrameState();
}
// TODO(cyanglaz): https://github.com/flutter/flutter/issues/56474
// Make this method check if there are pending view operations instead.
// Also rename it to `HasPendingViewOperations`.
bool FlutterPlatformViewsController::HasPlatformViewThisOrNextFrame() {
return composition_order_.size() > 0 || active_composition_order_.size() > 0;
}
const int FlutterPlatformViewsController::kDefaultMergedLeaseDuration;
PostPrerollResult FlutterPlatformViewsController::PostPrerollAction(
fml::RefPtr<fml::RasterThreadMerger> raster_thread_merger) {
// TODO(cyanglaz): https://github.com/flutter/flutter/issues/56474
// Rename `has_platform_view` to `view_mutated` when the above issue is resolved.
if (!HasPlatformViewThisOrNextFrame()) {
return PostPrerollResult::kSuccess;
}
if (!raster_thread_merger->IsMerged()) {
// The raster thread merger may be disabled if the rasterizer is being
// created or teared down.
//
// In such cases, the current frame is dropped, and a new frame is attempted
// with the same layer tree.
//
// Eventually, the frame is submitted once this method returns `kSuccess`.
// At that point, the raster tasks are handled on the platform thread.
CancelFrame();
raster_thread_merger->MergeWithLease(kDefaultMergedLeaseDuration);
return PostPrerollResult::kSkipAndRetryFrame;
}
// If the post preroll action is successful, we will display platform views in the current frame.
// In order to sync the rendering of the platform views (quartz) with skia's rendering,
// We need to begin an explicit CATransaction. This transaction needs to be submitted
// after the current frame is submitted.
BeginCATransaction();
raster_thread_merger->ExtendLeaseTo(kDefaultMergedLeaseDuration);
return PostPrerollResult::kSuccess;
}
void FlutterPlatformViewsController::PrerollCompositeEmbeddedView(
int view_id,
std::unique_ptr<EmbeddedViewParams> params) {
// All the CATransactions should be committed by the end of the last frame,
// so catransaction_added_ must be false.
FML_DCHECK(!catransaction_added_);
picture_recorders_[view_id] = std::make_unique<SkPictureRecorder>();
auto rtree_factory = RTreeFactory();
platform_view_rtrees_[view_id] = rtree_factory.getInstance();
picture_recorders_[view_id]->beginRecording(SkRect::Make(frame_size_), &rtree_factory);
composition_order_.push_back(view_id);
if (current_composition_params_.count(view_id) == 1 &&
current_composition_params_[view_id] == *params.get()) {
// Do nothing if the params didn't change.
return;
}
current_composition_params_[view_id] = EmbeddedViewParams(*params.get());
views_to_recomposite_.insert(view_id);
}
UIView* FlutterPlatformViewsController::GetPlatformViewByID(int view_id) {
if (views_.empty()) {
return nil;
}
return [touch_interceptors_[view_id].get() embeddedView];
}
std::vector<SkCanvas*> FlutterPlatformViewsController::GetCurrentCanvases() {
std::vector<SkCanvas*> canvases;
for (size_t i = 0; i < composition_order_.size(); i++) {
int64_t view_id = composition_order_[i];
canvases.push_back(picture_recorders_[view_id]->getRecordingCanvas());
}
return canvases;
}
int FlutterPlatformViewsController::CountClips(const MutatorsStack& mutators_stack) {
std::vector<std::shared_ptr<Mutator>>::const_reverse_iterator iter = mutators_stack.Bottom();
int clipCount = 0;
while (iter != mutators_stack.Top()) {
if ((*iter)->IsClipType()) {
clipCount++;
}
++iter;
}
return clipCount;
}
void FlutterPlatformViewsController::ApplyMutators(const MutatorsStack& mutators_stack,
UIView* embedded_view) {
if (flutter_view_ == nullptr) {
return;
}
FML_DCHECK(CATransform3DEqualToTransform(embedded_view.layer.transform, CATransform3DIdentity));
ResetAnchor(embedded_view.layer);
ChildClippingView* clipView = (ChildClippingView*)embedded_view.superview;
// The UIKit frame is set based on the logical resolution instead of physical.
// (https://developer.apple.com/library/archive/documentation/DeviceInformation/Reference/iOSDeviceCompatibility/Displays/Displays.html).
// However, flow is based on the physical resolution. For example, 1000 pixels in flow equals
// 500 points in UIKit. And until this point, we did all the calculation based on the flow
// resolution. So we need to scale down to match UIKit's logical resolution.
CGFloat screenScale = [UIScreen mainScreen].scale;
CATransform3D finalTransform = CATransform3DMakeScale(1 / screenScale, 1 / screenScale, 1);
UIView* flutter_view = flutter_view_.get();
FlutterClippingMaskView* maskView = [[[FlutterClippingMaskView alloc]
initWithFrame:CGRectMake(-clipView.frame.origin.x, -clipView.frame.origin.y,
CGRectGetWidth(flutter_view.bounds),
CGRectGetHeight(flutter_view.bounds))] autorelease];
auto iter = mutators_stack.Begin();
while (iter != mutators_stack.End()) {
switch ((*iter)->GetType()) {
case transform: {
CATransform3D transform = GetCATransform3DFromSkMatrix((*iter)->GetMatrix());
finalTransform = CATransform3DConcat(transform, finalTransform);
break;
}
case clip_rect:
[maskView clipRect:(*iter)->GetRect() matrix:finalTransform];
break;
case clip_rrect:
[maskView clipRRect:(*iter)->GetRRect() matrix:finalTransform];
break;
case clip_path:
[maskView clipPath:(*iter)->GetPath() matrix:finalTransform];
break;
case opacity:
embedded_view.alpha = (*iter)->GetAlphaFloat() * embedded_view.alpha;
break;
}
++iter;
}
// Reverse the offset of the clipView.
// The clipView's frame includes the final translate of the final transform matrix.
// So we need to revese this translate so the platform view can layout at the correct offset.
//
// Note that we don't apply this transform matrix the clippings because clippings happen on the
// mask view, whose origin is always (0,0) to the flutter_view.
CATransform3D reverseTranslate =
CATransform3DMakeTranslation(-clipView.frame.origin.x, -clipView.frame.origin.y, 0);
embedded_view.layer.transform = CATransform3DConcat(finalTransform, reverseTranslate);
clipView.maskView = maskView;
}
void FlutterPlatformViewsController::CompositeWithParams(int view_id,
const EmbeddedViewParams& params) {
CGRect frame = CGRectMake(0, 0, params.sizePoints().width(), params.sizePoints().height());
UIView* touchInterceptor = touch_interceptors_[view_id].get();
touchInterceptor.layer.transform = CATransform3DIdentity;
touchInterceptor.frame = frame;
touchInterceptor.alpha = 1;
const MutatorsStack& mutatorStack = params.mutatorsStack();
UIView* clippingView = root_views_[view_id].get();
// The frame of the clipping view should be the final bounding rect.
// Because the translate matrix in the Mutator Stack also includes the offset,
// when we apply the transforms matrix in |ApplyMutators|, we need
// to remember to do a reverse translate.
const SkRect& rect = params.finalBoundingRect();
CGFloat screenScale = [UIScreen mainScreen].scale;
clippingView.frame = CGRectMake(rect.x() / screenScale, rect.y() / screenScale,
rect.width() / screenScale, rect.height() / screenScale);
ApplyMutators(mutatorStack, touchInterceptor);
}
SkCanvas* FlutterPlatformViewsController::CompositeEmbeddedView(int view_id) {
// Any UIKit related code has to run on main thread.
FML_DCHECK([[NSThread currentThread] isMainThread]);
// Do nothing if the view doesn't need to be composited.
if (views_to_recomposite_.count(view_id) == 0) {
return picture_recorders_[view_id]->getRecordingCanvas();
}
CompositeWithParams(view_id, current_composition_params_[view_id]);
views_to_recomposite_.erase(view_id);
return picture_recorders_[view_id]->getRecordingCanvas();
}
void FlutterPlatformViewsController::Reset() {
UIView* flutter_view = flutter_view_.get();
for (UIView* sub_view in [flutter_view subviews]) {
[sub_view removeFromSuperview];
}
root_views_.clear();
touch_interceptors_.clear();
views_.clear();
composition_order_.clear();
active_composition_order_.clear();
picture_recorders_.clear();
platform_view_rtrees_.clear();
current_composition_params_.clear();
clip_count_.clear();
views_to_recomposite_.clear();
layer_pool_->RecycleLayers();
}
SkRect FlutterPlatformViewsController::GetPlatformViewRect(int view_id) {
UIView* platform_view = GetPlatformViewByID(view_id);
UIScreen* screen = [UIScreen mainScreen];
CGRect platform_view_cgrect = [platform_view convertRect:platform_view.bounds
toView:flutter_view_];
return SkRect::MakeXYWH(platform_view_cgrect.origin.x * screen.scale, //
platform_view_cgrect.origin.y * screen.scale, //
platform_view_cgrect.size.width * screen.scale, //
platform_view_cgrect.size.height * screen.scale //
);
}
bool FlutterPlatformViewsController::SubmitFrame(GrDirectContext* gr_context,
std::shared_ptr<IOSContext> ios_context,
std::unique_ptr<SurfaceFrame> frame) {
// Any UIKit related code has to run on main thread.
FML_DCHECK([[NSThread currentThread] isMainThread]);
if (flutter_view_ == nullptr) {
return frame->Submit();
}
DisposeViews();
SkCanvas* background_canvas = frame->SkiaCanvas();
// Resolve all pending GPU operations before allocating a new surface.
background_canvas->flush();
// Clipping the background canvas before drawing the picture recorders requires to
// save and restore the clip context.
SkAutoCanvasRestore save(background_canvas, /*doSave=*/true);
// Maps a platform view id to a vector of `FlutterPlatformViewLayer`.
LayersMap platform_view_layers;
auto did_submit = true;
auto num_platform_views = composition_order_.size();
for (size_t i = 0; i < num_platform_views; i++) {
int64_t platform_view_id = composition_order_[i];
sk_sp<RTree> rtree = platform_view_rtrees_[platform_view_id];
sk_sp<SkPicture> picture = picture_recorders_[platform_view_id]->finishRecordingAsPicture();
// Check if the current picture contains overlays that intersect with the
// current platform view or any of the previous platform views.
for (size_t j = i + 1; j > 0; j--) {
int64_t current_platform_view_id = composition_order_[j - 1];
SkRect platform_view_rect = GetPlatformViewRect(current_platform_view_id);
std::list<SkRect> intersection_rects =
rtree->searchNonOverlappingDrawnRects(platform_view_rect);
auto allocation_size = intersection_rects.size();
// For testing purposes, the overlay id is used to find the overlay view.
// This is the index of the layer for the current platform view.
auto overlay_id = platform_view_layers[current_platform_view_id].size();
// If the max number of allocations per platform view is exceeded,
// then join all the rects into a single one.
//
// TODO(egarciad): Consider making this configurable.
// https://github.com/flutter/flutter/issues/52510
if (allocation_size > kMaxLayerAllocations) {
SkRect joined_rect;
for (const SkRect& rect : intersection_rects) {
joined_rect.join(rect);
}
// Replace the rects in the intersection rects list for a single rect that is
// the union of all the rects in the list.
intersection_rects.clear();
intersection_rects.push_back(joined_rect);
}
for (SkRect& joined_rect : intersection_rects) {
// Get the intersection rect between the current rect
// and the platform view rect.
joined_rect.intersect(platform_view_rect);
// Subpixels in the platform may not align with the canvas subpixels.
// To workaround it, round the floating point bounds and make the rect slightly larger.
// For example, {0.3, 0.5, 3.1, 4.7} becomes {0, 0, 4, 5}.
joined_rect.setLTRB(std::floor(joined_rect.left()), std::floor(joined_rect.top()),
std::ceil(joined_rect.right()), std::ceil(joined_rect.bottom()));
// Clip the background canvas, so it doesn't contain any of the pixels drawn
// on the overlay layer.
background_canvas->clipRect(joined_rect, SkClipOp::kDifference);
// Get a new host layer.
std::shared_ptr<FlutterPlatformViewLayer> layer = GetLayer(gr_context, //
ios_context, //
picture, //
joined_rect, //
current_platform_view_id, //
overlay_id //
);
did_submit &= layer->did_submit_last_frame;
platform_view_layers[current_platform_view_id].push_back(layer);
overlay_id++;
}
}
background_canvas->drawPicture(picture);
}
// If a layer was allocated in the previous frame, but it's not used in the current frame,
// then it can be removed from the scene.
RemoveUnusedLayers();
// Organize the layers by their z indexes.
BringLayersIntoView(platform_view_layers);
// Mark all layers as available, so they can be used in the next frame.
layer_pool_->RecycleLayers();
did_submit &= frame->Submit();
// If the frame is submitted with embedded platform views,
// there should be a |[CATransaction begin]| call in this frame prior to all the drawing.
// If that case, we need to commit the transaction.
CommitCATransactionIfNeeded();
return did_submit;
}
void FlutterPlatformViewsController::BringLayersIntoView(LayersMap layer_map) {
FML_DCHECK(flutter_view_);
UIView* flutter_view = flutter_view_.get();
auto zIndex = 0;
// Clear the `active_composition_order_`, which will be populated down below.
active_composition_order_.clear();
for (size_t i = 0; i < composition_order_.size(); i++) {
int64_t platform_view_id = composition_order_[i];
std::vector<std::shared_ptr<FlutterPlatformViewLayer>> layers = layer_map[platform_view_id];
UIView* platform_view_root = root_views_[platform_view_id].get();
if (platform_view_root.superview != flutter_view) {
[flutter_view addSubview:platform_view_root];
} else {
platform_view_root.layer.zPosition = zIndex++;
}
for (const std::shared_ptr<FlutterPlatformViewLayer>& layer : layers) {
if ([layer->overlay_view_wrapper superview] != flutter_view) {
[flutter_view addSubview:layer->overlay_view_wrapper];
} else {
layer->overlay_view_wrapper.get().layer.zPosition = zIndex++;
}
}
active_composition_order_.push_back(platform_view_id);
}
}
std::shared_ptr<FlutterPlatformViewLayer> FlutterPlatformViewsController::GetLayer(
GrDirectContext* gr_context,
std::shared_ptr<IOSContext> ios_context,
sk_sp<SkPicture> picture,
SkRect rect,
int64_t view_id,
int64_t overlay_id) {
FML_DCHECK(flutter_view_);
std::shared_ptr<FlutterPlatformViewLayer> layer = layer_pool_->GetLayer(gr_context, ios_context);
UIView* overlay_view_wrapper = layer->overlay_view_wrapper.get();
auto screenScale = [UIScreen mainScreen].scale;
// Set the size of the overlay view wrapper.
// This wrapper view masks the overlay view.
overlay_view_wrapper.frame = CGRectMake(rect.x() / screenScale, rect.y() / screenScale,
rect.width() / screenScale, rect.height() / screenScale);
// Set a unique view identifier, so the overlay wrapper can be identified in unit tests.
overlay_view_wrapper.accessibilityIdentifier =
[NSString stringWithFormat:@"platform_view[%lld].overlay[%lld]", view_id, overlay_id];
UIView* overlay_view = layer->overlay_view.get();
// Set the size of the overlay view.
// This size is equal to the the device screen size.
overlay_view.frame = flutter_view_.get().bounds;
std::unique_ptr<SurfaceFrame> frame = layer->surface->AcquireFrame(frame_size_);
// If frame is null, AcquireFrame already printed out an error message.
if (!frame) {
return layer;
}
SkCanvas* overlay_canvas = frame->SkiaCanvas();
overlay_canvas->clear(SK_ColorTRANSPARENT);
// Offset the picture since its absolute position on the scene is determined
// by the position of the overlay view.
overlay_canvas->translate(-rect.x(), -rect.y());
overlay_canvas->drawPicture(picture);
layer->did_submit_last_frame = frame->Submit();
return layer;
}
void FlutterPlatformViewsController::RemoveUnusedLayers() {
std::vector<std::shared_ptr<FlutterPlatformViewLayer>> layers = layer_pool_->GetUnusedLayers();
for (const std::shared_ptr<FlutterPlatformViewLayer>& layer : layers) {
[layer->overlay_view_wrapper removeFromSuperview];
}
std::unordered_set<int64_t> composition_order_set;
for (int64_t view_id : composition_order_) {
composition_order_set.insert(view_id);
}
// Remove unused platform views.
for (int64_t view_id : active_composition_order_) {
if (composition_order_set.find(view_id) == composition_order_set.end()) {
UIView* platform_view_root = root_views_[view_id].get();
[platform_view_root removeFromSuperview];
}
}
}
void FlutterPlatformViewsController::DisposeViews() {
if (views_to_dispose_.empty()) {
return;
}
FML_DCHECK([[NSThread currentThread] isMainThread]);
for (int64_t viewId : views_to_dispose_) {
UIView* root_view = root_views_[viewId].get();
[root_view removeFromSuperview];
views_.erase(viewId);
touch_interceptors_.erase(viewId);
root_views_.erase(viewId);
current_composition_params_.erase(viewId);
clip_count_.erase(viewId);
views_to_recomposite_.erase(viewId);
}
views_to_dispose_.clear();
}
void FlutterPlatformViewsController::BeginCATransaction() {
FML_DCHECK([[NSThread currentThread] isMainThread]);
FML_DCHECK(!catransaction_added_);
[CATransaction begin];
catransaction_added_ = true;
}
void FlutterPlatformViewsController::CommitCATransactionIfNeeded() {
if (catransaction_added_) {
FML_DCHECK([[NSThread currentThread] isMainThread]);
[CATransaction commit];
catransaction_added_ = false;
}
}
void FlutterPlatformViewsController::ResetFrameState() {
picture_recorders_.clear();
composition_order_.clear();
}
} // namespace flutter
// This recognizers delays touch events from being dispatched to the responder chain until it failed
// recognizing a gesture.
//
// We only fail this recognizer when asked to do so by the Flutter framework (which does so by
// invoking an acceptGesture method on the platform_views channel). And this is how we allow the
// Flutter framework to delay or prevent the embedded view from getting a touch sequence.
@interface DelayingGestureRecognizer : UIGestureRecognizer <UIGestureRecognizerDelegate>
// Indicates that if the `DelayingGestureRecognizer`'s state should be set to
// `UIGestureRecognizerStateEnded` during next `touchesEnded` call.
@property(nonatomic) bool shouldEndInNextTouchesEnded;
// Indicates that the `DelayingGestureRecognizer`'s `touchesEnded` has been invoked without
// setting the state to `UIGestureRecognizerStateEnded`.
@property(nonatomic) bool touchedEndedWithoutBlocking;
- (instancetype)initWithTarget:(id)target
action:(SEL)action
forwardingRecognizer:(UIGestureRecognizer*)forwardingRecognizer;
@end
// While the DelayingGestureRecognizer is preventing touches from hitting the responder chain
// the touch events are not arriving to the FlutterView (and thus not arriving to the Flutter
// framework). We use this gesture recognizer to dispatch the events directly to the FlutterView
// while during this phase.
//
// If the Flutter framework decides to dispatch events to the embedded view, we fail the
// DelayingGestureRecognizer which sends the events up the responder chain. But since the events
// are handled by the embedded view they are not delivered to the Flutter framework in this phase
// as well. So during this phase as well the ForwardingGestureRecognizer dispatched the events
// directly to the FlutterView.
@interface ForwardingGestureRecognizer : UIGestureRecognizer <UIGestureRecognizerDelegate>
- (instancetype)initWithTarget:(id)target
platformViewsController:
(fml::WeakPtr<flutter::FlutterPlatformViewsController>)platformViewsController;
@end
@implementation FlutterTouchInterceptingView {
fml::scoped_nsobject<DelayingGestureRecognizer> _delayingRecognizer;
FlutterPlatformViewGestureRecognizersBlockingPolicy _blockingPolicy;
UIView* _embeddedView;
}
- (instancetype)initWithEmbeddedView:(UIView*)embeddedView
platformViewsController:
(fml::WeakPtr<flutter::FlutterPlatformViewsController>)platformViewsController
gestureRecognizersBlockingPolicy:
(FlutterPlatformViewGestureRecognizersBlockingPolicy)blockingPolicy {
self = [super initWithFrame:embeddedView.frame];
if (self) {
self.multipleTouchEnabled = YES;
_embeddedView = embeddedView;
embeddedView.autoresizingMask =
(UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight);
[self addSubview:embeddedView];
ForwardingGestureRecognizer* forwardingRecognizer = [[[ForwardingGestureRecognizer alloc]
initWithTarget:self
platformViewsController:std::move(platformViewsController)] autorelease];
_delayingRecognizer.reset([[DelayingGestureRecognizer alloc]
initWithTarget:self
action:nil
forwardingRecognizer:forwardingRecognizer]);
_blockingPolicy = blockingPolicy;
[self addGestureRecognizer:_delayingRecognizer.get()];
[self addGestureRecognizer:forwardingRecognizer];
}
return self;
}
- (UIView*)embeddedView {
return [[_embeddedView retain] autorelease];
}
- (void)releaseGesture {
_delayingRecognizer.get().state = UIGestureRecognizerStateFailed;
}
- (void)blockGesture {
switch (_blockingPolicy) {
case FlutterPlatformViewGestureRecognizersBlockingPolicyEager:
// We block all other gesture recognizers immediately in this policy.
_delayingRecognizer.get().state = UIGestureRecognizerStateEnded;
break;
case FlutterPlatformViewGestureRecognizersBlockingPolicyWaitUntilTouchesEnded:
if (_delayingRecognizer.get().touchedEndedWithoutBlocking) {
// If touchesEnded of the `DelayingGesureRecognizer` has been already invoked,
// we want to set the state of the `DelayingGesureRecognizer` to
// `UIGestureRecognizerStateEnded` as soon as possible.
_delayingRecognizer.get().state = UIGestureRecognizerStateEnded;
} else {
// If touchesEnded of the `DelayingGesureRecognizer` has not been invoked,
// We will set a flag to notify the `DelayingGesureRecognizer` to set the state to
// `UIGestureRecognizerStateEnded` when touchesEnded is called.
_delayingRecognizer.get().shouldEndInNextTouchesEnded = YES;
}
break;
default:
break;
}
}
// We want the intercepting view to consume the touches and not pass the touches up to the parent
// view. Make the touch event method not call super will not pass the touches up to the parent view.
// Hence we overide the touch event methods and do nothing.
- (void)touchesBegan:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
}
- (void)touchesMoved:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
}
- (void)touchesCancelled:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
}
- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
}
@end
@implementation DelayingGestureRecognizer {
fml::scoped_nsobject<UIGestureRecognizer> _forwardingRecognizer;
}
- (instancetype)initWithTarget:(id)target
action:(SEL)action
forwardingRecognizer:(UIGestureRecognizer*)forwardingRecognizer {
self = [super initWithTarget:target action:action];
if (self) {
self.delaysTouchesBegan = YES;
self.delaysTouchesEnded = YES;
self.delegate = self;
self.shouldEndInNextTouchesEnded = NO;
self.touchedEndedWithoutBlocking = NO;
_forwardingRecognizer.reset([forwardingRecognizer retain]);
}
return self;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer*)otherGestureRecognizer {
// The forwarding gesture recognizer should always get all touch events, so it should not be
// required to fail by any other gesture recognizer.
return otherGestureRecognizer != _forwardingRecognizer.get() && otherGestureRecognizer != self;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer*)otherGestureRecognizer {
return otherGestureRecognizer == self;
}
- (void)touchesBegan:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
self.touchedEndedWithoutBlocking = NO;
[super touchesBegan:touches withEvent:event];
}
- (void)touchesEnded:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
if (self.shouldEndInNextTouchesEnded) {
self.state = UIGestureRecognizerStateEnded;
self.shouldEndInNextTouchesEnded = NO;
} else {
self.touchedEndedWithoutBlocking = YES;
}
[super touchesEnded:touches withEvent:event];
}
- (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
self.state = UIGestureRecognizerStateFailed;
}
@end
@implementation ForwardingGestureRecognizer {
// Weak reference to FlutterPlatformViewsController. The FlutterPlatformViewsController has
// a reference to the FlutterViewController, where we can dispatch pointer events to.
//
// The lifecycle of FlutterPlatformViewsController is bind to FlutterEngine, which should always
// outlives the FlutterViewController. And ForwardingGestureRecognizer is owned by a subview of
// FlutterView, so the ForwardingGestureRecognizer never out lives FlutterViewController.
// Therefore, `_platformViewsController` should never be nullptr.
fml::WeakPtr<flutter::FlutterPlatformViewsController> _platformViewsController;
// Counting the pointers that has started in one touch sequence.
NSInteger _currentTouchPointersCount;
// We can't dispatch events to the framework without this back pointer.
// This gesture recognizer retains the `FlutterViewController` until the
// end of a gesture sequence, that is all the touches in touchesBegan are concluded
// with |touchesCancelled| or |touchesEnded|.
fml::scoped_nsobject<UIViewController> _flutterViewController;
}
- (instancetype)initWithTarget:(id)target
platformViewsController:
(fml::WeakPtr<flutter::FlutterPlatformViewsController>)platformViewsController {
self = [super initWithTarget:target action:nil];
if (self) {
self.delegate = self;
FML_DCHECK(platformViewsController.get() != nullptr);
_platformViewsController = std::move(platformViewsController);
_currentTouchPointersCount = 0;
}
return self;
}
- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
FML_DCHECK(_currentTouchPointersCount >= 0);
if (_currentTouchPointersCount == 0) {
// At the start of each gesture sequence, we reset the `_flutterViewController`,
// so that all the touch events in the same sequence are forwarded to the same
// `_flutterViewController`.
_flutterViewController.reset([_platformViewsController->getFlutterViewController() retain]);
}
[_flutterViewController.get() touchesBegan:touches withEvent:event];
_currentTouchPointersCount += touches.count;
}
- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {
[_flutterViewController.get() touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
[_flutterViewController.get() touchesEnded:touches withEvent:event];
_currentTouchPointersCount -= touches.count;
// Touches in one touch sequence are sent to the touchesEnded method separately if different
// fingers stop touching the screen at different time. So one touchesEnded method triggering does
// not necessarially mean the touch sequence has ended. We Only set the state to
// UIGestureRecognizerStateFailed when all the touches in the current touch sequence is ended.
if (_currentTouchPointersCount == 0) {
self.state = UIGestureRecognizerStateFailed;
_flutterViewController.reset(nil);
}
}
- (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
// In the event of platform view is removed, iOS generates a "stationary" change type instead of
// "cancelled" change type.
// Flutter needs all the cancelled touches to be "cancelled" change types in order to correctly
// handle gesture sequence.
// We always override the change type to "cancelled".
[((FlutterViewController*)_flutterViewController.get()) forceTouchesCancelled:touches];
_currentTouchPointersCount -= touches.count;
if (_currentTouchPointersCount == 0) {
self.state = UIGestureRecognizerStateFailed;
_flutterViewController.reset(nil);
}
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
shouldRecognizeSimultaneouslyWithGestureRecognizer:
(UIGestureRecognizer*)otherGestureRecognizer {
return YES;
}
@end