diff --git a/flow/embedded_views.cc b/flow/embedded_views.cc index c660f4691318b..5234bf1e50c8c 100644 --- a/flow/embedded_views.cc +++ b/flow/embedded_views.cc @@ -6,10 +6,13 @@ namespace flutter { -bool ExternalViewEmbedder::SubmitFrame(GrContext* context) { +bool ExternalViewEmbedder::SubmitFrame(GrContext* context, + SkCanvas* background_canvas) { return false; }; +void ExternalViewEmbedder::FinishFrame(){}; + void MutatorsStack::PushClipRect(const SkRect& rect) { std::shared_ptr element = std::make_shared(rect); vector_.push_back(element); diff --git a/flow/embedded_views.h b/flow/embedded_views.h index 030eb88c8a06d..7a491a8a152ef 100644 --- a/flow/embedded_views.h +++ b/flow/embedded_views.h @@ -248,7 +248,10 @@ class ExternalViewEmbedder { // Must be called on the UI thread. virtual SkCanvas* CompositeEmbeddedView(int view_id) = 0; - virtual bool SubmitFrame(GrContext* context); + virtual bool SubmitFrame(GrContext* context, SkCanvas* background_canvas); + + // This is called after submitting the embedder frame and the surface frame. + virtual void FinishFrame(); FML_DISALLOW_COPY_AND_ASSIGN(ExternalViewEmbedder); diff --git a/flow/layers/picture_layer.cc b/flow/layers/picture_layer.cc index 3bc7e394c1033..08c09cc9e833b 100644 --- a/flow/layers/picture_layer.cc +++ b/flow/layers/picture_layer.cc @@ -59,7 +59,7 @@ void PictureLayer::Paint(PaintContext& context) const { return; } } - context.leaf_nodes_canvas->drawPicture(picture()); + picture()->playback(context.leaf_nodes_canvas); } } // namespace flutter diff --git a/flow/layers/picture_layer_unittests.cc b/flow/layers/picture_layer_unittests.cc index 687c870eeac66..4f565cf500ecc 100644 --- a/flow/layers/picture_layer_unittests.cc +++ b/flow/layers/picture_layer_unittests.cc @@ -94,9 +94,6 @@ TEST_F(PictureLayerTest, SimplePicture) { 1, MockCanvas::SetMatrixData{RasterCache::GetIntegralTransCTM( layer_offset_matrix)}}, #endif - MockCanvas::DrawCall{ - 1, MockCanvas::DrawPictureData{mock_picture->serialize(), SkPaint(), - SkMatrix()}}, MockCanvas::DrawCall{1, MockCanvas::RestoreData{0}}}); EXPECT_EQ(mock_canvas().draw_calls(), expected_draw_calls); } diff --git a/shell/common/rasterizer.cc b/shell/common/rasterizer.cc index 811898b5a42d2..f7e4350fe9c97 100644 --- a/shell/common/rasterizer.cc +++ b/shell/common/rasterizer.cc @@ -342,9 +342,17 @@ RasterStatus Rasterizer::DrawToSurface(flutter::LayerTree& layer_tree) { if (raster_status == RasterStatus::kFailed) { return raster_status; } - frame->Submit(); if (external_view_embedder != nullptr) { - external_view_embedder->SubmitFrame(surface_->GetContext()); + external_view_embedder->SubmitFrame(surface_->GetContext(), + root_surface_canvas); + // The external view embedder may mutate the root surface canvas while + // submitting the frame. + // Therefore, submit the final frame after asking the external view + // embedder to submit the frame. + frame->Submit(); + external_view_embedder->FinishFrame(); + } else { + frame->Submit(); } FireNextFrameCallbackIfPresent(); diff --git a/shell/platform/darwin/ios/framework/Source/FlutterOverlayView.mm b/shell/platform/darwin/ios/framework/Source/FlutterOverlayView.mm index 11c0d60618886..4127061f3e7de 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterOverlayView.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterOverlayView.mm @@ -39,6 +39,7 @@ - (instancetype)init { if (self) { self.layer.opaque = NO; self.userInteractionEnabled = NO; + self.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight); } return self; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index b86c4623fa7a7..c2cbef18cc167 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -8,16 +8,96 @@ #import "flutter/shell/platform/darwin/ios/ios_surface.h" #import "flutter/shell/platform/darwin/ios/ios_surface_gl.h" +#include #include #include #include #include "FlutterPlatformViews_Internal.h" +#include "flutter/flow/rtree.h" #include "flutter/fml/platform/darwin/scoped_nsobject.h" +#include "flutter/shell/common/persistent_cache.h" #include "flutter/shell/platform/darwin/common/framework/Headers/FlutterChannels.h" namespace flutter { +std::shared_ptr FlutterPlatformViewLayerPool::GetLayer( + GrContext* gr_context, + std::shared_ptr ios_context) { + if (available_layer_index_ >= layers_.size()) { + std::shared_ptr layer; + fml::scoped_nsobject overlay_view; + fml::scoped_nsobject overlay_view_wrapper; + + if (!gr_context) { + overlay_view.reset([[FlutterOverlayView alloc] init]); + overlay_view_wrapper.reset([[FlutterOverlayView alloc] init]); + + std::unique_ptr ios_surface = + [overlay_view.get() createSurface:std::move(ios_context)]; + std::unique_ptr surface = ios_surface->CreateGPUSurface(); + + layer = std::make_shared( + 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]); + + std::unique_ptr ios_surface = + [overlay_view.get() createSurface:std::move(ios_context)]; + std::unique_ptr surface = ios_surface->CreateGPUSurface(gr_context); + + layer = std::make_shared( + 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 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 = ios_surface->CreateGPUSurface(gr_context); + layer->surface = std::move(surface); + } + available_layer_index_++; + return layer; +} + +void FlutterPlatformViewLayerPool::RecycleLayers() { + available_layer_index_ = 0; +} + +std::vector> +FlutterPlatformViewLayerPool::GetUnusedLayers() { + std::vector> 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]); } @@ -83,6 +163,9 @@ NSObject* embedded_view = [factory createWithFrame:CGRectZero viewIdentifier:viewId arguments:params]; + // Set a unique view identifier, so the platform view can be identified in unit tests. + [embedded_view view].accessibilityIdentifier = + [NSString stringWithFormat:@"platform_view[%ld]", viewId]; views_[viewId] = fml::scoped_nsobject>([embedded_view retain]); FlutterTouchInterceptingView* touch_interceptor = [[[FlutterTouchInterceptingView alloc] @@ -196,8 +279,11 @@ int view_id, std::unique_ptr params) { picture_recorders_[view_id] = std::make_unique(); - picture_recorders_[view_id]->beginRecording(SkRect::Make(frame_size_)); - picture_recorders_[view_id]->getRecordingCanvas()->clear(SK_ColorTRANSPARENT); + + 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 && @@ -357,81 +443,196 @@ [sub_view removeFromSuperview]; } views_.clear(); - overlays_.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 = [views_[view_id].get() view]; + 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(GrContext* gr_context, - std::shared_ptr ios_context) { + std::shared_ptr ios_context, + SkCanvas* background_canvas) { DisposeViews(); - bool did_submit = true; - for (int64_t view_id : composition_order_) { - EnsureOverlayInitialized(view_id, ios_context, gr_context); - auto frame = overlays_[view_id]->surface->AcquireFrame(frame_size_); - // If frame is null, AcquireFrame already printed out an error message. - if (frame) { - SkCanvas* canvas = frame->SkiaCanvas(); - canvas->drawPicture(picture_recorders_[view_id]->finishRecordingAsPicture()); - canvas->flush(); - did_submit &= frame->Submit(); + // 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 = platform_view_rtrees_[platform_view_id]; + sk_sp 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 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 slighly 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 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++; + } } - } - picture_recorders_.clear(); - if (composition_order_ == active_composition_order_) { - composition_order_.clear(); - return did_submit; - } - DetachUnusedLayers(); - active_composition_order_.clear(); - UIView* flutter_view = flutter_view_.get(); + 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(); + // Reset the composition order, so next frame starts empty. + composition_order_.clear(); + return did_submit; +} + +void FlutterPlatformViewsController::BringLayersIntoView(LayersMap layer_map) { + UIView* flutter_view = flutter_view_.get(); + auto zIndex = 0; for (size_t i = 0; i < composition_order_.size(); i++) { - int view_id = composition_order_[i]; - // We added a chain of super views to the platform view to handle clipping. - // The `platform_view_root` is the view at the top of the chain which is a direct subview of the - // `FlutterView`. - UIView* platform_view_root = root_views_[view_id].get(); - UIView* overlay = overlays_[view_id]->overlay_view; - FML_CHECK(platform_view_root.superview == overlay.superview); - if (platform_view_root.superview == flutter_view) { - [flutter_view bringSubviewToFront:platform_view_root]; - [flutter_view bringSubviewToFront:overlay]; - } else { + int64_t platform_view_id = composition_order_[i]; + std::vector> 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]; - [flutter_view addSubview:overlay]; - overlay.frame = flutter_view.bounds; + } else { + platform_view_root.layer.zPosition = zIndex++; } - - active_composition_order_.push_back(view_id); + for (const std::shared_ptr& 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); } - composition_order_.clear(); - return did_submit; } -void FlutterPlatformViewsController::DetachUnusedLayers() { - std::unordered_set composition_order_set; +std::shared_ptr FlutterPlatformViewsController::GetLayer( + GrContext* gr_context, + std::shared_ptr ios_context, + sk_sp picture, + SkRect rect, + int64_t view_id, + int64_t overlay_id) { + std::shared_ptr 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 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> layers = layer_pool_->GetUnusedLayers(); + for (const std::shared_ptr& layer : layers) { + [layer->overlay_view_wrapper removeFromSuperview]; + } + std::unordered_set 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()) { - if (root_views_.find(view_id) == root_views_.end()) { - continue; - } - // We added a chain of super views to the platform view to handle clipping. - // The `platform_view_root` is the view at the top of the chain which is a direct subview of - // the `FlutterView`. UIView* platform_view_root = root_views_[view_id].get(); [platform_view_root removeFromSuperview]; - [overlays_[view_id]->overlay_view.get() removeFromSuperview]; } } } @@ -447,10 +648,6 @@ views_.erase(viewId); touch_interceptors_.erase(viewId); root_views_.erase(viewId); - if (overlays_.find(viewId) != overlays_.end()) { - [overlays_[viewId]->overlay_view.get() removeFromSuperview]; - } - overlays_.erase(viewId); current_composition_params_.erase(viewId); clip_count_.erase(viewId); views_to_recomposite_.erase(viewId); @@ -458,56 +655,6 @@ views_to_dispose_.clear(); } -void FlutterPlatformViewsController::EnsureOverlayInitialized( - int64_t overlay_id, - std::shared_ptr ios_context, - GrContext* gr_context) { - FML_DCHECK(flutter_view_); - - auto overlay_it = overlays_.find(overlay_id); - - if (!gr_context) { - if (overlays_.count(overlay_id) != 0) { - return; - } - fml::scoped_nsobject overlay_view([[FlutterOverlayView alloc] init]); - overlay_view.get().frame = flutter_view_.get().bounds; - overlay_view.get().autoresizingMask = - (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight); - std::unique_ptr ios_surface = - [overlay_view.get() createSurface:std::move(ios_context)]; - std::unique_ptr surface = ios_surface->CreateGPUSurface(); - overlays_[overlay_id] = std::make_unique( - std::move(overlay_view), std::move(ios_surface), std::move(surface)); - return; - } - - if (overlay_it != overlays_.end()) { - FlutterPlatformViewLayer* overlay = overlay_it->second.get(); - if (gr_context != overlay->gr_context) { - overlay->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 = overlay_it->second->ios_surface.get(); - std::unique_ptr surface = ios_surface->CreateGPUSurface(gr_context); - overlay_it->second->surface = std::move(surface); - } - return; - } - auto contentsScale = flutter_view_.get().layer.contentsScale; - fml::scoped_nsobject overlay_view( - [[FlutterOverlayView alloc] initWithContentsScale:contentsScale]); - overlay_view.get().frame = flutter_view_.get().bounds; - overlay_view.get().autoresizingMask = - (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight); - std::unique_ptr ios_surface = - [overlay_view.get() createSurface:std::move(ios_context)]; - std::unique_ptr surface = ios_surface->CreateGPUSurface(gr_context); - overlays_[overlay_id] = std::make_unique( - std::move(overlay_view), std::move(ios_surface), std::move(surface)); - overlays_[overlay_id]->gr_context = gr_context; -} - } // namespace flutter // This recognizers delays touch events from being dispatched to the responder chain until it failed diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h index d135d7d2ac290..c5ebd9a479b1f 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h @@ -6,6 +6,7 @@ #define FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERPLATFORMVIEWS_INTERNAL_H_ #include "flutter/flow/embedded_views.h" +#include "flutter/flow/rtree.h" #include "flutter/fml/platform/darwin/scoped_nsobject.h" #include "flutter/shell/common/shell.h" #include "flutter/shell/platform/darwin/common/framework/Headers/FlutterBinaryMessenger.h" @@ -59,21 +60,63 @@ class IOSSurface; struct FlutterPlatformViewLayer { FlutterPlatformViewLayer(fml::scoped_nsobject overlay_view, + fml::scoped_nsobject overlay_view_wrapper, std::unique_ptr ios_surface, std::unique_ptr surface); ~FlutterPlatformViewLayer(); fml::scoped_nsobject overlay_view; + fml::scoped_nsobject overlay_view_wrapper; std::unique_ptr ios_surface; std::unique_ptr surface; + // Whether a frame for this layer was submitted. + bool did_submit_last_frame; + // The GrContext that is currently used by the overlay surfaces. // We track this to know when the GrContext for the Flutter app has changed // so we can update the overlay with the new context. GrContext* gr_context; }; +// This class isn't thread safe. +class FlutterPlatformViewLayerPool { + public: + FlutterPlatformViewLayerPool() = default; + ~FlutterPlatformViewLayerPool() = default; + + // Gets a layer from the pool if available, or allocates a new one. + // Finally, it marks the layer as used. That is, it increments `available_layer_index_`. + std::shared_ptr GetLayer(GrContext* gr_context, + std::shared_ptr ios_context); + + // Gets the layers in the pool that aren't currently used. + // This method doesn't mark the layers as unused. + std::vector> GetUnusedLayers(); + + // Marks the layers in the pool as available for reuse. + void RecycleLayers(); + + private: + // The index of the entry in the layers_ vector that determines the beginning of the unused + // layers. For example, consider the following vector: + // _____ + // | 0 | + /// |---| + /// | 1 | <-- available_layer_index_ + /// |---| + /// | 2 | + /// |---| + /// + /// This indicates that entries starting from 1 can be reused meanwhile the entry at position 0 + /// cannot be reused. + size_t available_layer_index_ = 0; + std::vector> layers_; + + FML_DISALLOW_COPY_AND_ASSIGN(FlutterPlatformViewLayerPool); +}; + class FlutterPlatformViewsController { public: FlutterPlatformViewsController(); @@ -109,14 +152,37 @@ class FlutterPlatformViewsController { SkCanvas* CompositeEmbeddedView(int view_id); + // The rect of the platform view at index view_id. This rect has been translated into the + // host view coordinate system. Units are device screen pixels. + SkRect GetPlatformViewRect(int view_id); + // Discards all platform views instances and auxiliary resources. void Reset(); - bool SubmitFrame(GrContext* gr_context, std::shared_ptr ios_context); + bool SubmitFrame(GrContext* gr_context, + std::shared_ptr ios_context, + SkCanvas* background_canvas); void OnMethodCall(FlutterMethodCall* call, FlutterResult& result); private: + static const size_t kMaxLayerAllocations = 2; + + using LayersMap = std::map>>; + + // The pool of reusable view layers. The pool allows to recycle layer in each frame. + std::unique_ptr layer_pool_; + + // The platform view's R-tree keyed off the view id, which contains any subsequent + // draw operation until the next platform view or the last leaf node in the layer tree. + // + // The R-trees are deleted by the FlutterPlatformViewsController.reset(). + std::map> platform_view_rtrees_; + + // The platform view's picture recorder keyed off the view id, which contains any subsequent + // operation until the next platform view or the end of the last leaf node in the layer tree. + std::map> picture_recorders_; + fml::scoped_nsobject channel_; fml::scoped_nsobject flutter_view_; fml::scoped_nsobject flutter_view_controller_; @@ -134,7 +200,6 @@ class FlutterPlatformViewsController { // Mapping a platform view ID to the count of the clipping operations that were applied to the // platform view last time it was composited. std::map clip_count_; - std::map> overlays_; SkISize frame_size_; // This is the number of frames the task runners will stay @@ -163,19 +228,12 @@ class FlutterPlatformViewsController { std::map gesture_recognizers_blocking_policies; - std::map> picture_recorders_; - void OnCreate(FlutterMethodCall* call, FlutterResult& result); void OnDispose(FlutterMethodCall* call, FlutterResult& result); void OnAcceptGesture(FlutterMethodCall* call, FlutterResult& result); void OnRejectGesture(FlutterMethodCall* call, FlutterResult& result); - - void DetachUnusedLayers(); // Dispose the views in `views_to_dispose_`. void DisposeViews(); - void EnsureOverlayInitialized(int64_t overlay_id, - std::shared_ptr ios_context, - GrContext* gr_context); // This will return true after pre-roll if any of the embedded views // have mutated for last layer tree. @@ -215,6 +273,20 @@ class FlutterPlatformViewsController { void ApplyMutators(const MutatorsStack& mutators_stack, UIView* embedded_view); void CompositeWithParams(int view_id, const EmbeddedViewParams& params); + // Allocates a new FlutterPlatformViewLayer if needed, draws the pixels within the rect from + // the picture on the layer's canvas. + std::shared_ptr GetLayer(GrContext* gr_context, + std::shared_ptr ios_context, + sk_sp picture, + SkRect rect, + int64_t view_id, + int64_t overlay_id); + // Removes overlay views and platform views that aren't needed in the current frame. + void RemoveUnusedLayers(); + // Appends the overlay views and platform view and sets their z index based on the composition + // order. + void BringLayersIntoView(LayersMap layer_map); + FML_DISALLOW_COPY_AND_ASSIGN(FlutterPlatformViewsController); }; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index 9310fa1803f11..551535a2c7faf 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -11,16 +11,20 @@ namespace flutter { -FlutterPlatformViewLayer::FlutterPlatformViewLayer(fml::scoped_nsobject overlay_view, - std::unique_ptr ios_surface, - std::unique_ptr surface) +FlutterPlatformViewLayer::FlutterPlatformViewLayer( + fml::scoped_nsobject overlay_view, + fml::scoped_nsobject overlay_view_wrapper, + std::unique_ptr ios_surface, + std::unique_ptr surface) : overlay_view(std::move(overlay_view)), + overlay_view_wrapper(std::move(overlay_view_wrapper)), ios_surface(std::move(ios_surface)), surface(std::move(surface)){}; FlutterPlatformViewLayer::~FlutterPlatformViewLayer() = default; -FlutterPlatformViewsController::FlutterPlatformViewsController() = default; +FlutterPlatformViewsController::FlutterPlatformViewsController() + : layer_pool_(std::make_unique()){}; FlutterPlatformViewsController::~FlutterPlatformViewsController() = default; diff --git a/shell/platform/darwin/ios/ios_surface.h b/shell/platform/darwin/ios/ios_surface.h index 58233d8a2f656..8cba9285cfb02 100644 --- a/shell/platform/darwin/ios/ios_surface.h +++ b/shell/platform/darwin/ios/ios_surface.h @@ -77,7 +77,10 @@ class IOSSurface : public ExternalViewEmbedder { SkCanvas* CompositeEmbeddedView(int view_id) override; // |ExternalViewEmbedder| - bool SubmitFrame(GrContext* context) override; + bool SubmitFrame(GrContext* context, SkCanvas* background_canvas) override; + + // |ExternalViewEmbedder| + void FinishFrame() override; public: FML_DISALLOW_COPY_AND_ASSIGN(IOSSurface); diff --git a/shell/platform/darwin/ios/ios_surface.mm b/shell/platform/darwin/ios/ios_surface.mm index fe85c77f12c2d..f51160e94e471 100644 --- a/shell/platform/darwin/ios/ios_surface.mm +++ b/shell/platform/darwin/ios/ios_surface.mm @@ -132,12 +132,18 @@ bool IsIosEmbeddedViewsPreviewEnabled() { } // |ExternalViewEmbedder| -bool IOSSurface::SubmitFrame(GrContext* context) { +bool IOSSurface::SubmitFrame(GrContext* context, SkCanvas* background_canvas) { TRACE_EVENT0("flutter", "IOSSurface::SubmitFrame"); FML_CHECK(platform_views_controller_ != nullptr); - bool submitted = platform_views_controller_->SubmitFrame(std::move(context), ios_context_); - [CATransaction commit]; + bool submitted = + platform_views_controller_->SubmitFrame(std::move(context), ios_context_, background_canvas); return submitted; } +// |ExternalViewEmbedder| +void IOSSurface::FinishFrame() { + TRACE_EVENT0("flutter", "IOSSurface::DidSubmitFrame"); + [CATransaction commit]; +} + } // namespace flutter diff --git a/shell/platform/embedder/embedder_external_view_embedder.cc b/shell/platform/embedder/embedder_external_view_embedder.cc index 5e77073e7ff47..23658888f7caa 100644 --- a/shell/platform/embedder/embedder_external_view_embedder.cc +++ b/shell/platform/embedder/embedder_external_view_embedder.cc @@ -129,7 +129,8 @@ static FlutterBackingStoreConfig MakeBackingStoreConfig( } // |ExternalViewEmbedder| -bool EmbedderExternalViewEmbedder::SubmitFrame(GrContext* context) { +bool EmbedderExternalViewEmbedder::SubmitFrame(GrContext* context, + SkCanvas* background_canvas) { auto [matched_render_targets, pending_keys] = render_target_cache_.GetExistingTargetsInCache(pending_views_); @@ -265,4 +266,7 @@ bool EmbedderExternalViewEmbedder::SubmitFrame(GrContext* context) { return true; } +// |ExternalViewEmbedder| +void EmbedderExternalViewEmbedder::FinishFrame() {} + } // namespace flutter diff --git a/shell/platform/embedder/embedder_external_view_embedder.h b/shell/platform/embedder/embedder_external_view_embedder.h index 7000d2cde04cd..63c944a88d7ef 100644 --- a/shell/platform/embedder/embedder_external_view_embedder.h +++ b/shell/platform/embedder/embedder_external_view_embedder.h @@ -89,7 +89,10 @@ class EmbedderExternalViewEmbedder final : public ExternalViewEmbedder { SkCanvas* CompositeEmbeddedView(int view_id) override; // |ExternalViewEmbedder| - bool SubmitFrame(GrContext* context) override; + bool SubmitFrame(GrContext* context, SkCanvas* background_canvas) override; + + // |ExternalViewEmbedder| + void FinishFrame() override; // |ExternalViewEmbedder| SkCanvas* GetRootCanvas() override; diff --git a/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj b/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj index c24333a3a8a7f..818d902b3e2e9 100644 --- a/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj +++ b/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj @@ -43,6 +43,7 @@ 3DEF491A23C3BE6500184216 /* golden_platform_view_transform_iPhone 8_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 3DE09E9123C010BD006C9851 /* golden_platform_view_transform_iPhone 8_simulator.png */; }; 59A97FD8236A49D300B4C066 /* golden_platform_view_multiple_iPhone SE_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 59A97FD7236A49D300B4C066 /* golden_platform_view_multiple_iPhone SE_simulator.png */; }; 59A97FDA236B984300B4C066 /* golden_platform_view_multiple_background_foreground_iPhone SE_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 59A97FD9236B984300B4C066 /* golden_platform_view_multiple_background_foreground_iPhone SE_simulator.png */; }; + 6402EBD124147BDA00987DCB /* UnobstructedPlatformViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 6402EBD024147BDA00987DCB /* UnobstructedPlatformViewTests.m */; }; 6816DB9E231750ED00A51400 /* GoldenPlatformViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 6816DB9D231750ED00A51400 /* GoldenPlatformViewTests.m */; }; 6816DBA12317573300A51400 /* GoldenImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 6816DBA02317573300A51400 /* GoldenImage.m */; }; 6816DBA42318358200A51400 /* PlatformViewGoldenTestManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 6816DBA32318358200A51400 /* PlatformViewGoldenTestManager.m */; }; @@ -149,6 +150,7 @@ 3DE09E9223C010BD006C9851 /* golden_platform_view_cliprect_iPhone 8_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_cliprect_iPhone 8_simulator.png"; sourceTree = ""; }; 59A97FD7236A49D300B4C066 /* golden_platform_view_multiple_iPhone SE_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_multiple_iPhone SE_simulator.png"; sourceTree = ""; }; 59A97FD9236B984300B4C066 /* golden_platform_view_multiple_background_foreground_iPhone SE_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_multiple_background_foreground_iPhone SE_simulator.png"; sourceTree = ""; }; + 6402EBD024147BDA00987DCB /* UnobstructedPlatformViewTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UnobstructedPlatformViewTests.m; sourceTree = ""; }; 6816DB9C231750ED00A51400 /* GoldenPlatformViewTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GoldenPlatformViewTests.h; sourceTree = ""; }; 6816DB9D231750ED00A51400 /* GoldenPlatformViewTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GoldenPlatformViewTests.m; sourceTree = ""; }; 6816DB9F2317573300A51400 /* GoldenImage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GoldenImage.h; sourceTree = ""; }; @@ -245,6 +247,7 @@ 248D76ED22E388380012F0C1 /* ScenariosUITests */ = { isa = PBXGroup; children = ( + 6402EBD024147BDA00987DCB /* UnobstructedPlatformViewTests.m */, 0D14A3FD239743190013D873 /* golden_platform_view_rotate_iPhone SE_simulator.png */, 3DE09E8B23C010BC006C9851 /* golden_platform_view_clippath_iPhone 8_simulator.png */, 3DE09E9223C010BD006C9851 /* golden_platform_view_cliprect_iPhone 8_simulator.png */, @@ -488,6 +491,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 6402EBD124147BDA00987DCB /* UnobstructedPlatformViewTests.m in Sources */, 68A5B63423EB71D300BDBCDB /* PlatformViewGestureRecognizerTests.m in Sources */, 6816DBA12317573300A51400 /* GoldenImage.m in Sources */, 6816DB9E231750ED00A51400 /* GoldenPlatformViewTests.m in Sources */, diff --git a/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m b/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m index 348889b19b856..9bd732f647039 100644 --- a/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m +++ b/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m @@ -29,6 +29,13 @@ - (BOOL)application:(UIApplication*)application // the launchArgsMap should match the one in the `PlatformVieGoldenTestManager`. NSDictionary* launchArgsMap = @{ @"--platform-view" : @"platform_view", + @"--platform-view-no-overlay-intersection" : @"platform_view_no_overlay_intersection", + @"--platform-view-two-intersecting-overlays" : @"platform_view_two_intersecting_overlays", + @"--platform-view-partial-intersection" : @"platform_view_partial_intersection", + @"--platform-view-one-overlay-two-intersecting-overlays" : + @"platform_view_one_overlay_two_intersecting_overlays", + @"--platform-view-multiple-without-overlays" : @"platform_view_multiple_without_overlays", + @"--platform-view-max-overlays" : @"platform_view_max_overlays", @"--platform-view-multiple" : @"platform_view_multiple", @"--platform-view-multiple-background-foreground" : @"platform_view_multiple_background_foreground", diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewGestureRecognizerTests.m b/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewGestureRecognizerTests.m index d791210f22707..3d583e1d5e824 100644 --- a/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewGestureRecognizerTests.m +++ b/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewGestureRecognizerTests.m @@ -25,7 +25,7 @@ - (void)testRejectPolicyUtilTouchesEnded { [NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject, NSDictionary* _Nullable bindings) { XCUIElement* element = evaluatedObject; - return [element.identifier isEqualToString:@"platform_view"]; + return [element.identifier hasPrefix:@"platform_view"]; }]; XCUIElement* platformView = [app.textViews elementMatchingPredicate:predicateToFindPlatformView]; if (![platformView waitForExistenceWithTimeout:kSecondsToWaitForPlatformView]) { @@ -56,7 +56,7 @@ - (void)testRejectPolicyEager { [NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject, NSDictionary* _Nullable bindings) { XCUIElement* element = evaluatedObject; - return [element.identifier isEqualToString:@"platform_view"]; + return [element.identifier hasPrefix:@"platform_view"]; }]; XCUIElement* platformView = [app.textViews elementMatchingPredicate:predicateToFindPlatformView]; if (![platformView waitForExistenceWithTimeout:kSecondsToWaitForPlatformView]) { @@ -91,7 +91,7 @@ - (void)testAccept { [NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject, NSDictionary* _Nullable bindings) { XCUIElement* element = evaluatedObject; - return [element.identifier isEqualToString:@"platform_view"]; + return [element.identifier hasPrefix:@"platform_view"]; }]; XCUIElement* platformView = [app.textViews elementMatchingPredicate:predicateToFindPlatformView]; if (![platformView waitForExistenceWithTimeout:kSecondsToWaitForPlatformView]) { diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/UnobstructedPlatformViewTests.m b/testing/scenario_app/ios/Scenarios/ScenariosUITests/UnobstructedPlatformViewTests.m new file mode 100644 index 0000000000000..02e7eee35f098 --- /dev/null +++ b/testing/scenario_app/ios/Scenarios/ScenariosUITests/UnobstructedPlatformViewTests.m @@ -0,0 +1,254 @@ +// 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 + +@interface UnobstructedPlatformViewTests : XCTestCase + +@end + +@implementation UnobstructedPlatformViewTests + +- (void)setUp { + self.continueAfterFailure = NO; +} + +// A is the layer, which z index is higher than the platform view. +// +--------+ +// | PV | +---+ +// +--------+ | A | +// +---+ +- (void)testNoOverlay { + XCUIApplication* app = [[XCUIApplication alloc] init]; + app.launchArguments = @[ @"--platform-view-no-overlay-intersection" ]; + [app launch]; + + XCUIElement* platform_view = app.textViews[@"platform_view[0]"]; + XCTAssertTrue(platform_view.exists); + XCTAssertEqual(platform_view.frame.origin.x, 25); + XCTAssertEqual(platform_view.frame.origin.y, 25); + XCTAssertEqual(platform_view.frame.size.width, 250); + XCTAssertEqual(platform_view.frame.size.height, 250); + + XCUIElement* overlay = app.otherElements[@"platform_view[0].overlay[0]"]; + XCTAssertFalse(overlay.exists); +} + +// A is the layer above the platform view. +// +-----------------+ +// | PV +---+ | +// | | A | | +// | +---+ | +// +-----------------+ +- (void)testOneOverlay { + XCUIApplication* app = [[XCUIApplication alloc] init]; + app.launchArguments = @[ @"--platform-view" ]; + [app launch]; + + XCUIElement* platform_view = app.textViews[@"platform_view[0]"]; + XCTAssertTrue(platform_view.exists); + XCTAssertEqual(platform_view.frame.origin.x, 25); + XCTAssertEqual(platform_view.frame.origin.y, 25); + XCTAssertEqual(platform_view.frame.size.width, 250); + XCTAssertEqual(platform_view.frame.size.height, 250); + + XCUIElement* overlay = app.otherElements[@"platform_view[0].overlay[0]"]; + XCTAssertTrue(overlay.exists); + XCTAssertEqual(overlay.frame.origin.x, 150); + XCTAssertEqual(overlay.frame.origin.y, 150); + XCTAssertEqual(overlay.frame.size.width, 50); + XCTAssertEqual(overlay.frame.size.height, 50); +} + +// A is the layer above the platform view. +// +-----------------+ +// | PV +---+ | +// +-----------| A |-+ +// +---+ +- (void)testOneOverlayPartialIntersection { + XCUIApplication* app = [[XCUIApplication alloc] init]; + app.launchArguments = @[ @"--platform-view-partial-intersection" ]; + [app launch]; + + XCUIElement* platform_view = app.textViews[@"platform_view[0]"]; + XCTAssertTrue(platform_view.exists); + XCTAssertEqual(platform_view.frame.origin.x, 25); + XCTAssertEqual(platform_view.frame.origin.y, 25); + XCTAssertEqual(platform_view.frame.size.width, 250); + XCTAssertEqual(platform_view.frame.size.height, 250); + + XCUIElement* overlay = app.otherElements[@"platform_view[0].overlay[0]"]; + XCTAssertTrue(overlay.exists); + XCTAssertEqual(overlay.frame.origin.x, 200); + XCTAssertEqual(overlay.frame.origin.y, 250); + XCTAssertEqual(overlay.frame.size.width, 50); + // Half the height of the overlay. + XCTAssertEqual(overlay.frame.size.height, 25); +} + +// A and B are the layers above the platform view. +// +--------------------+ +// | PV +------------+ | +// | | B +-----+ | | +// | +---| A |-+ | +// +----------| |---+ +// +-----+ +- (void)testTwoIntersectingOverlays { + XCUIApplication* app = [[XCUIApplication alloc] init]; + app.launchArguments = @[ @"--platform-view-two-intersecting-overlays" ]; + [app launch]; + + XCUIElement* platform_view = app.textViews[@"platform_view[0]"]; + XCTAssertTrue(platform_view.exists); + XCTAssertEqual(platform_view.frame.origin.x, 25); + XCTAssertEqual(platform_view.frame.origin.y, 25); + XCTAssertEqual(platform_view.frame.size.width, 250); + XCTAssertEqual(platform_view.frame.size.height, 250); + + XCUIElement* overlay = app.otherElements[@"platform_view[0].overlay[0]"]; + XCTAssertTrue(overlay.exists); + XCTAssertEqual(overlay.frame.origin.x, 150); + XCTAssertEqual(overlay.frame.origin.y, 150); + XCTAssertEqual(overlay.frame.size.width, 75); + XCTAssertEqual(overlay.frame.size.height, 75); + + XCTAssertFalse(app.otherElements[@"platform_view[0].overlay[1]"].exists); +} + +// A, B, and C are the layers above the platform view. +// +-------------------------+ +// | PV +-----------+ | +// | +---+ | B +-----+ | | +// | | C | +---| A |-+ | +// | +---+ +-----+ | +// +-------------------------+ +- (void)testOneOverlayAndTwoIntersectingOverlays { + XCUIApplication* app = [[XCUIApplication alloc] init]; + app.launchArguments = @[ @"--platform-view-one-overlay-two-intersecting-overlays" ]; + [app launch]; + + XCUIElement* platform_view = app.textViews[@"platform_view[0]"]; + XCTAssertTrue(platform_view.exists); + XCTAssertEqual(platform_view.frame.origin.x, 25); + XCTAssertEqual(platform_view.frame.origin.y, 25); + XCTAssertEqual(platform_view.frame.size.width, 250); + XCTAssertEqual(platform_view.frame.size.height, 250); + + XCUIElement* overlay1 = app.otherElements[@"platform_view[0].overlay[0]"]; + XCTAssertTrue(overlay1.exists); + XCTAssertEqual(overlay1.frame.origin.x, 150); + XCTAssertEqual(overlay1.frame.origin.y, 150); + XCTAssertEqual(overlay1.frame.size.width, 75); + XCTAssertEqual(overlay1.frame.size.height, 75); + + XCUIElement* overlay2 = app.otherElements[@"platform_view[0].overlay[1]"]; + XCTAssertTrue(overlay2.exists); + XCTAssertEqual(overlay2.frame.origin.x, 75); + XCTAssertEqual(overlay2.frame.origin.y, 225); + XCTAssertEqual(overlay2.frame.size.width, 50); + XCTAssertEqual(overlay2.frame.size.height, 50); +} + +// A is the layer, which z index is higher than the platform view. +// +--------+ +// | PV | +---+ +// +--------+ | A | +// +--------+ +---+ +// | PV | +// +--------+ +- (void)testMultiplePlatformViewsWithoutOverlays { + XCUIApplication* app = [[XCUIApplication alloc] init]; + app.launchArguments = @[ @"--platform-view-multiple-without-overlays" ]; + [app launch]; + + XCUIElement* platform_view1 = app.textViews[@"platform_view[0]"]; + XCTAssertTrue(platform_view1.exists); + XCTAssertEqual(platform_view1.frame.origin.x, 25); + XCTAssertEqual(platform_view1.frame.origin.y, 325); + XCTAssertEqual(platform_view1.frame.size.width, 250); + XCTAssertEqual(platform_view1.frame.size.height, 250); + + XCUIElement* platform_view2 = app.textViews[@"platform_view[1]"]; + XCTAssertTrue(platform_view2.exists); + XCTAssertEqual(platform_view2.frame.origin.x, 25); + XCTAssertEqual(platform_view2.frame.origin.y, 25); + XCTAssertEqual(platform_view2.frame.size.width, 250); + XCTAssertEqual(platform_view2.frame.size.height, 250); + + XCTAssertFalse(app.otherElements[@"platform_view[0].overlay[0]"].exists); + XCTAssertFalse(app.otherElements[@"platform_view[1].overlay[0]"].exists); +} + +// A is the layer above both platform view. +// +------------+ +// | PV +----+ | +// +-----| A |-+ +// +-----| |-+ +// | PV +----+ | +// +------------+ +- (void)testMultiplePlatformViewsWithOverlays { + XCUIApplication* app = [[XCUIApplication alloc] init]; + app.launchArguments = @[ @"--platform-view-multiple-background-foreground" ]; + [app launch]; + + XCUIElement* platform_view1 = app.textViews[@"platform_view[8]"]; + XCTAssertTrue(platform_view1.exists); + XCTAssertEqual(platform_view1.frame.origin.x, 25); + XCTAssertEqual(platform_view1.frame.origin.y, 325); + XCTAssertEqual(platform_view1.frame.size.width, 250); + XCTAssertEqual(platform_view1.frame.size.height, 250); + + XCUIElement* platform_view2 = app.textViews[@"platform_view[9]"]; + XCTAssertTrue(platform_view2.exists); + XCTAssertEqual(platform_view2.frame.origin.x, 25); + XCTAssertEqual(platform_view2.frame.origin.y, 25); + XCTAssertEqual(platform_view2.frame.size.width, 250); + XCTAssertEqual(platform_view2.frame.size.height, 250); + + XCUIElement* overlay1 = app.otherElements[@"platform_view[8].overlay[0]"]; + XCTAssertTrue(overlay1.exists); + XCTAssertEqual(overlay1.frame.origin.x, 25); + XCTAssertEqual(overlay1.frame.origin.y, 325); + XCTAssertEqual(overlay1.frame.size.width, 225); + XCTAssertEqual(overlay1.frame.size.height, 175); + + XCUIElement* overlay2 = app.otherElements[@"platform_view[9].overlay[0]"]; + XCTAssertTrue(overlay2.exists); + XCTAssertEqual(overlay2.frame.origin.x, 25); + XCTAssertEqual(overlay2.frame.origin.y, 25); + XCTAssertEqual(overlay2.frame.size.width, 225); + XCTAssertEqual(overlay2.frame.size.height, 250); +} + +// More then two overlays are merged into a single layer. +// +---------------------+ +// | +---+ +---+ +---+ | +// | | A | | B | | C | | +// | +---+ +---+ +---+ | +// | +-------+ | +// +-| D |-----------+ +// +-------+ +- (void)testPlatformViewsMaxOverlays { + XCUIApplication* app = [[XCUIApplication alloc] init]; + app.launchArguments = @[ @"--platform-view-max-overlays" ]; + [app launch]; + + XCUIElement* platform_view = app.textViews[@"platform_view[0]"]; + XCTAssertTrue(platform_view.exists); + XCTAssertEqual(platform_view.frame.origin.x, 25); + XCTAssertEqual(platform_view.frame.origin.y, 25); + XCTAssertEqual(platform_view.frame.size.width, 250); + XCTAssertEqual(platform_view.frame.size.height, 250); + + XCUIElement* overlay = app.otherElements[@"platform_view[0].overlay[0]"]; + XCTAssertTrue(overlay.exists); + XCTAssertEqual(overlay.frame.origin.x, 75); + XCTAssertEqual(overlay.frame.origin.y, 85); + XCTAssertEqual(overlay.frame.size.width, 150); + XCTAssertEqual(overlay.frame.size.height, 190); + + XCTAssertFalse(app.otherElements[@"platform_view[0].overlay[1]"].exists); +} + +@end diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_iPhone 8_simulator.png b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_iPhone 8_simulator.png index a193faeb04022..9ec19ab474f03 100644 Binary files a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_iPhone 8_simulator.png and b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_iPhone 8_simulator.png differ diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_transform_iPhone 8_simulator.png b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_transform_iPhone 8_simulator.png index 793082f8f0f7c..0db030ed20983 100644 Binary files a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_transform_iPhone 8_simulator.png and b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_transform_iPhone 8_simulator.png differ diff --git a/testing/scenario_app/lib/main.dart b/testing/scenario_app/lib/main.dart index 494d6585aa7fe..0ab3f7c5352f1 100644 --- a/testing/scenario_app/lib/main.dart +++ b/testing/scenario_app/lib/main.dart @@ -19,6 +19,12 @@ import 'src/touches_scenario.dart'; Map _scenarios = { 'animated_color_square': AnimatedColorSquareScenario(window), 'platform_view': PlatformViewScenario(window, 'Hello from Scenarios (Platform View)', id: 0), + 'platform_view_no_overlay_intersection': PlatformViewNoOverlayIntersectionScenario(window, 'Hello from Scenarios (Platform View)', id: 0), + 'platform_view_partial_intersection': PlatformViewPartialIntersectionScenario(window, 'Hello from Scenarios (Platform View)', id: 0), + 'platform_view_two_intersecting_overlays': PlatformViewTwoIntersectingOverlaysScenario(window, 'Hello from Scenarios (Platform View)', id: 0), + 'platform_view_one_overlay_two_intersecting_overlays': PlatformViewOneOverlayTwoIntersectingOverlaysScenario(window, 'Hello from Scenarios (Platform View)', id: 0), + 'platform_view_multiple_without_overlays': MultiPlatformViewWithoutOverlaysScenario(window, 'Hello from Scenarios (Platform View)', id: 0), + 'platform_view_max_overlays': PlatformViewMaxOverlaysScenario(window, 'Hello from Scenarios (Platform View)', id: 0), 'platform_view_cliprect': PlatformViewClipRectScenario(window, 'PlatformViewClipRect', id: 1), 'platform_view_cliprrect': PlatformViewClipRRectScenario(window, 'PlatformViewClipRRect', id: 2), 'platform_view_clippath': PlatformViewClipPathScenario(window, 'PlatformViewClipPath', id: 3), diff --git a/testing/scenario_app/lib/src/platform_view.dart b/testing/scenario_app/lib/src/platform_view.dart index 0dde98fd9eebb..fcea41a65f7d5 100644 --- a/testing/scenario_app/lib/src/platform_view.dart +++ b/testing/scenario_app/lib/src/platform_view.dart @@ -48,6 +48,224 @@ class PlatformViewScenario extends Scenario with _BasePlatformViewScenarioMixin } } +/// A simple platform view with overlay that doesn't intersect with the platform view. +class PlatformViewNoOverlayIntersectionScenario extends Scenario with _BasePlatformViewScenarioMixin { + /// Creates the PlatformView scenario. + /// + /// The [window] parameter must not be null. + PlatformViewNoOverlayIntersectionScenario(Window window, String text, {int id = 0}) + : assert(window != null), + super(window) { + createPlatformView(window, text, id); + } + + @override + void onBeginFrame(Duration duration) { + final SceneBuilder builder = SceneBuilder(); + + builder.pushOffset(0, 0); + + finishBuilderByAddingPlatformViewAndPicture( + builder, + 0, + overlayOffset: const Offset(150, 350), + ); + } +} + +/// A simple platform view with an overlay that partially intersects with the platform view. +class PlatformViewPartialIntersectionScenario extends Scenario with _BasePlatformViewScenarioMixin { + /// Creates the PlatformView scenario. + /// + /// The [window] parameter must not be null. + PlatformViewPartialIntersectionScenario(Window window, String text, {int id = 0}) + : assert(window != null), + super(window) { + createPlatformView(window, text, id); + } + + @override + void onBeginFrame(Duration duration) { + final SceneBuilder builder = SceneBuilder(); + + builder.pushOffset(0, 0); + + finishBuilderByAddingPlatformViewAndPicture( + builder, + 0, + overlayOffset: const Offset(150, 250), + ); + } +} + +/// A simple platform view with two overlays that intersect with each other and the platform view. +class PlatformViewTwoIntersectingOverlaysScenario extends Scenario with _BasePlatformViewScenarioMixin { + /// Creates the PlatformView scenario. + /// + /// The [window] parameter must not be null. + PlatformViewTwoIntersectingOverlaysScenario(Window window, String text, {int id = 0}) + : assert(window != null), + super(window) { + createPlatformView(window, text, id); + } + + @override + void onBeginFrame(Duration duration) { + final SceneBuilder builder = SceneBuilder(); + + builder.pushOffset(0, 0); + + _addPlatformViewtoScene(builder, 0, 500, 500); + final PictureRecorder recorder = PictureRecorder(); + final Canvas canvas = Canvas(recorder); + canvas.drawCircle( + const Offset(50, 50), + 50, + Paint()..color = const Color(0xFFABCDEF), + ); + canvas.drawCircle( + const Offset(100, 100), + 50, + Paint()..color = const Color(0xFFABCDEF), + ); + final Picture picture = recorder.endRecording(); + builder.addPicture(const Offset(300, 300), picture); + final Scene scene = builder.build(); + window.render(scene); + scene.dispose(); + } +} + +/// A simple platform view with one overlay and two overlays that intersect with each other and the platform view. +class PlatformViewOneOverlayTwoIntersectingOverlaysScenario extends Scenario with _BasePlatformViewScenarioMixin { + /// Creates the PlatformView scenario. + /// + /// The [window] parameter must not be null. + PlatformViewOneOverlayTwoIntersectingOverlaysScenario(Window window, String text, {int id = 0}) + : assert(window != null), + super(window) { + createPlatformView(window, text, id); + } + + @override + void onBeginFrame(Duration duration) { + final SceneBuilder builder = SceneBuilder(); + + builder.pushOffset(0, 0); + + _addPlatformViewtoScene(builder, 0, 500, 500); + final PictureRecorder recorder = PictureRecorder(); + final Canvas canvas = Canvas(recorder); + canvas.drawCircle( + const Offset(50, 50), + 50, + Paint()..color = const Color(0xFFABCDEF), + ); + canvas.drawCircle( + const Offset(100, 100), + 50, + Paint()..color = const Color(0xFFABCDEF), + ); + canvas.drawCircle( + const Offset(-100, 200), + 50, + Paint()..color = const Color(0xFFABCDEF), + ); + final Picture picture = recorder.endRecording(); + builder.addPicture(const Offset(300, 300), picture); + final Scene scene = builder.build(); + window.render(scene); + scene.dispose(); + } +} + +/// Two platform views without an overlay intersecting either platform view. +class MultiPlatformViewWithoutOverlaysScenario extends Scenario with _BasePlatformViewScenarioMixin { + /// Creates the PlatformView scenario. + /// + /// The [window] parameter must not be null. + MultiPlatformViewWithoutOverlaysScenario(Window window, String text, {int id = 0}) + : assert(window != null), + super(window) { + createPlatformView(window, text, id); + } + + @override + void onBeginFrame(Duration duration) { + final SceneBuilder builder = SceneBuilder(); + + builder.pushOffset(0, 0); + + builder.pushOffset(0, 600); + _addPlatformViewtoScene(builder, 0, 500, 500); + builder.pop(); + + _addPlatformViewtoScene(builder, 1, 500, 500); + + final PictureRecorder recorder = PictureRecorder(); + final Canvas canvas = Canvas(recorder); + canvas.drawRect( + const Rect.fromLTRB(0, 0, 100, 1000), + Paint()..color = const Color(0xFFFF0000), + ); + final Picture picture = recorder.endRecording(); + builder.addPicture(const Offset(580, 0), picture); + + builder.pop(); + final Scene scene = builder.build(); + window.render(scene); + scene.dispose(); + } +} + +/// A simple platform view with too many overlays result in a single native view. +class PlatformViewMaxOverlaysScenario extends Scenario with _BasePlatformViewScenarioMixin { + /// Creates the PlatformView scenario. + /// + /// The [window] parameter must not be null. + PlatformViewMaxOverlaysScenario(Window window, String text, {int id = 0}) + : assert(window != null), + super(window) { + createPlatformView(window, text, id); + } + + @override + void onBeginFrame(Duration duration) { + final SceneBuilder builder = SceneBuilder(); + + builder.pushOffset(0, 0); + + _addPlatformViewtoScene(builder, 0, 500, 500); + final PictureRecorder recorder = PictureRecorder(); + final Canvas canvas = Canvas(recorder); + canvas.drawCircle( + const Offset(50, 50), + 50, + Paint()..color = const Color(0xFFABCDEF), + ); + canvas.drawCircle( + const Offset(100, 100), + 50, + Paint()..color = const Color(0xFFABCDEF), + ); + canvas.drawCircle( + const Offset(-100, 200), + 50, + Paint()..color = const Color(0xFFABCDEF), + ); + canvas.drawCircle( + const Offset(-100, -80), + 50, + Paint()..color = const Color(0xFFABCDEF), + ); + final Picture picture = recorder.endRecording(); + builder.addPicture(const Offset(300, 300), picture); + final Scene scene = builder.build(); + window.render(scene); + scene.dispose(); + } +} + /// Builds a scene with 2 platform views. class MultiPlatformViewScenario extends Scenario with _BasePlatformViewScenarioMixin { /// Creates the PlatformView scenario. @@ -424,12 +642,17 @@ mixin _BasePlatformViewScenarioMixin on Scenario { } // Add a platform view and a picture to the scene, then finish the `sceneBuilder`. - void finishBuilderByAddingPlatformViewAndPicture(SceneBuilder sceneBuilder, int viewId) { + void finishBuilderByAddingPlatformViewAndPicture( + SceneBuilder sceneBuilder, + int viewId, { + Offset overlayOffset, + }) { + overlayOffset ??= const Offset(50, 50); _addPlatformViewtoScene(sceneBuilder, viewId, 500, 500); final PictureRecorder recorder = PictureRecorder(); final Canvas canvas = Canvas(recorder); canvas.drawCircle( - const Offset(50, 50), + overlayOffset, 50, Paint()..color = const Color(0xFFABCDEF), );