mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Reland: Implement unobstructed Platform Views on iOS (flutter/engine#17237)
This commit is contained in:
parent
08495ab2d3
commit
fa2f49dbcb
@ -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<Mutator> element = std::make_shared<Mutator>(rect);
|
||||
vector_.push_back(element);
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -8,16 +8,77 @@
|
||||
#import "flutter/shell/platform/darwin/ios/ios_surface.h"
|
||||
#import "flutter/shell/platform/darwin/ios/ios_surface_gl.h"
|
||||
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#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<FlutterPlatformViewLayer> FlutterPlatformViewLayerPool::GetLayer(
|
||||
GrContext* gr_context,
|
||||
std::shared_ptr<IOSContext> ios_context) {
|
||||
if (available_layer_index_ >= layers_.size()) {
|
||||
std::shared_ptr<FlutterPlatformViewLayer> layer;
|
||||
|
||||
if (!gr_context) {
|
||||
fml::scoped_nsobject<FlutterOverlayView> overlay_view([[FlutterOverlayView alloc] init]);
|
||||
overlay_view.get().autoresizingMask =
|
||||
(UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight);
|
||||
std::unique_ptr<IOSSurface> ios_surface =
|
||||
[overlay_view.get() createSurface:std::move(ios_context)];
|
||||
std::unique_ptr<Surface> surface = ios_surface->CreateGPUSurface();
|
||||
|
||||
layer = std::make_shared<FlutterPlatformViewLayer>(
|
||||
std::move(overlay_view), std::move(ios_surface), std::move(surface));
|
||||
} else {
|
||||
CGFloat screenScale = [UIScreen mainScreen].scale;
|
||||
fml::scoped_nsobject<FlutterOverlayView> overlay_view(
|
||||
[[FlutterOverlayView alloc] initWithContentsScale:screenScale]);
|
||||
overlay_view.get().autoresizingMask =
|
||||
(UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight);
|
||||
std::unique_ptr<IOSSurface> ios_surface =
|
||||
[overlay_view.get() createSurface:std::move(ios_context)];
|
||||
std::unique_ptr<Surface> surface = ios_surface->CreateGPUSurface(gr_context);
|
||||
|
||||
layer = std::make_shared<FlutterPlatformViewLayer>(
|
||||
std::move(overlay_view), std::move(ios_surface), std::move(surface));
|
||||
layer->gr_context = gr_context;
|
||||
}
|
||||
layers_.push_back(layer);
|
||||
}
|
||||
auto 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]);
|
||||
}
|
||||
@ -83,6 +144,9 @@ void FlutterPlatformViewsController::OnCreate(FlutterMethodCall* call, FlutterRe
|
||||
NSObject<FlutterPlatformView>* 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<NSObject<FlutterPlatformView>>([embedded_view retain]);
|
||||
|
||||
FlutterTouchInterceptingView* touch_interceptor = [[[FlutterTouchInterceptingView alloc]
|
||||
@ -196,8 +260,11 @@ void FlutterPlatformViewsController::PrerollCompositeEmbeddedView(
|
||||
int view_id,
|
||||
std::unique_ptr<EmbeddedViewParams> params) {
|
||||
picture_recorders_[view_id] = std::make_unique<SkPictureRecorder>();
|
||||
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 &&
|
||||
@ -361,77 +428,182 @@ void FlutterPlatformViewsController::Reset() {
|
||||
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<IOSContext> ios_context) {
|
||||
std::shared_ptr<IOSContext> ios_context,
|
||||
SkCanvas* background_canvas) {
|
||||
DisposeViews();
|
||||
// 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;
|
||||
|
||||
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();
|
||||
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);
|
||||
// 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.
|
||||
auto 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);
|
||||
}
|
||||
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();
|
||||
|
||||
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 {
|
||||
[flutter_view addSubview:platform_view_root];
|
||||
[flutter_view addSubview:overlay];
|
||||
overlay.frame = flutter_view.bounds;
|
||||
}
|
||||
|
||||
active_composition_order_.push_back(view_id);
|
||||
}
|
||||
// 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::DetachUnusedLayers() {
|
||||
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++) {
|
||||
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 superview] != flutter_view) {
|
||||
[flutter_view addSubview:layer->overlay_view];
|
||||
} else {
|
||||
layer->overlay_view.get().layer.zPosition = zIndex++;
|
||||
}
|
||||
}
|
||||
active_composition_order_.push_back(platform_view_id);
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<FlutterPlatformViewLayer> FlutterPlatformViewsController::GetLayer(
|
||||
GrContext* gr_context,
|
||||
std::shared_ptr<IOSContext> ios_context,
|
||||
sk_sp<SkPicture> picture,
|
||||
SkRect rect,
|
||||
int64_t view_id,
|
||||
int64_t overlay_id) {
|
||||
auto layer = layer_pool_->GetLayer(gr_context, ios_context);
|
||||
auto screenScale = [UIScreen mainScreen].scale;
|
||||
// Set the size of the overlay UIView.
|
||||
layer->overlay_view.get().frame = CGRectMake(rect.x() / screenScale, //
|
||||
rect.y() / screenScale, //
|
||||
rect.width() / screenScale, //
|
||||
rect.height() / screenScale //
|
||||
);
|
||||
// Set a unique view identifier, so the overlay can be identified in unit tests.
|
||||
layer->overlay_view.get().accessibilityIdentifier =
|
||||
[NSString stringWithFormat:@"platform_view[%lld].overlay[%lld]", view_id, overlay_id];
|
||||
|
||||
std::unique_ptr<SurfaceFrame> frame =
|
||||
layer->surface->AcquireFrame(SkISize::Make(rect.width(), rect.height()));
|
||||
// If frame is null, AcquireFrame already printed out an error message.
|
||||
if (!frame) {
|
||||
return layer;
|
||||
}
|
||||
auto 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() {
|
||||
auto layers = layer_pool_->GetUnusedLayers();
|
||||
for (const std::shared_ptr<FlutterPlatformViewLayer>& layer : layers) {
|
||||
[layer->overlay_view 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()) {
|
||||
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];
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -458,56 +630,6 @@ void FlutterPlatformViewsController::DisposeViews() {
|
||||
views_to_dispose_.clear();
|
||||
}
|
||||
|
||||
void FlutterPlatformViewsController::EnsureOverlayInitialized(
|
||||
int64_t overlay_id,
|
||||
std::shared_ptr<IOSContext> 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<FlutterOverlayView> overlay_view([[FlutterOverlayView alloc] init]);
|
||||
overlay_view.get().frame = flutter_view_.get().bounds;
|
||||
overlay_view.get().autoresizingMask =
|
||||
(UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight);
|
||||
std::unique_ptr<IOSSurface> ios_surface =
|
||||
[overlay_view.get() createSurface:std::move(ios_context)];
|
||||
std::unique_ptr<Surface> surface = ios_surface->CreateGPUSurface();
|
||||
overlays_[overlay_id] = std::make_unique<FlutterPlatformViewLayer>(
|
||||
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> surface = ios_surface->CreateGPUSurface(gr_context);
|
||||
overlay_it->second->surface = std::move(surface);
|
||||
}
|
||||
return;
|
||||
}
|
||||
auto contentsScale = flutter_view_.get().layer.contentsScale;
|
||||
fml::scoped_nsobject<FlutterOverlayView> overlay_view(
|
||||
[[FlutterOverlayView alloc] initWithContentsScale:contentsScale]);
|
||||
overlay_view.get().frame = flutter_view_.get().bounds;
|
||||
overlay_view.get().autoresizingMask =
|
||||
(UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight);
|
||||
std::unique_ptr<IOSSurface> ios_surface =
|
||||
[overlay_view.get() createSurface:std::move(ios_context)];
|
||||
std::unique_ptr<Surface> surface = ios_surface->CreateGPUSurface(gr_context);
|
||||
overlays_[overlay_id] = std::make_unique<FlutterPlatformViewLayer>(
|
||||
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
|
||||
|
||||
@ -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"
|
||||
@ -68,12 +69,52 @@ struct FlutterPlatformViewLayer {
|
||||
std::unique_ptr<IOSSurface> ios_surface;
|
||||
std::unique_ptr<Surface> 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<FlutterPlatformViewLayer> GetLayer(GrContext* gr_context,
|
||||
std::shared_ptr<IOSContext> ios_context);
|
||||
|
||||
// Gets the layers in the pool that aren't currently used.
|
||||
// This method doesn't mark the layers as unused.
|
||||
std::vector<std::shared_ptr<FlutterPlatformViewLayer>> 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<std::shared_ptr<FlutterPlatformViewLayer>> layers_;
|
||||
|
||||
FML_DISALLOW_COPY_AND_ASSIGN(FlutterPlatformViewLayerPool);
|
||||
};
|
||||
|
||||
class FlutterPlatformViewsController {
|
||||
public:
|
||||
FlutterPlatformViewsController();
|
||||
@ -109,14 +150,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<IOSContext> ios_context);
|
||||
bool SubmitFrame(GrContext* gr_context,
|
||||
std::shared_ptr<IOSContext> ios_context,
|
||||
SkCanvas* background_canvas);
|
||||
|
||||
void OnMethodCall(FlutterMethodCall* call, FlutterResult& result);
|
||||
|
||||
private:
|
||||
static const size_t kMaxLayerAllocations = 2;
|
||||
|
||||
using LayersMap = std::map<int64_t, std::vector<std::shared_ptr<FlutterPlatformViewLayer>>>;
|
||||
|
||||
// The pool of reusable view layers. The pool allows to recycle layer in each frame.
|
||||
std::unique_ptr<FlutterPlatformViewLayerPool> 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<int64_t, sk_sp<RTree>> 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<int64_t, std::unique_ptr<SkPictureRecorder>> picture_recorders_;
|
||||
|
||||
fml::scoped_nsobject<FlutterMethodChannel> channel_;
|
||||
fml::scoped_nsobject<UIView> flutter_view_;
|
||||
fml::scoped_nsobject<UIViewController> flutter_view_controller_;
|
||||
@ -163,19 +227,12 @@ class FlutterPlatformViewsController {
|
||||
std::map<std::string, FlutterPlatformViewGestureRecognizersBlockingPolicy>
|
||||
gesture_recognizers_blocking_policies;
|
||||
|
||||
std::map<int64_t, std::unique_ptr<SkPictureRecorder>> 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<IOSContext> 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 +272,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<FlutterPlatformViewLayer> GetLayer(GrContext* gr_context,
|
||||
std::shared_ptr<IOSContext> ios_context,
|
||||
sk_sp<SkPicture> 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);
|
||||
};
|
||||
|
||||
|
||||
@ -20,7 +20,8 @@ FlutterPlatformViewLayer::FlutterPlatformViewLayer(fml::scoped_nsobject<UIView>
|
||||
|
||||
FlutterPlatformViewLayer::~FlutterPlatformViewLayer() = default;
|
||||
|
||||
FlutterPlatformViewsController::FlutterPlatformViewsController() = default;
|
||||
FlutterPlatformViewsController::FlutterPlatformViewsController()
|
||||
: layer_pool_(std::make_unique<FlutterPlatformViewLayerPool>()){};
|
||||
|
||||
FlutterPlatformViewsController::~FlutterPlatformViewsController() = default;
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -132,12 +132,18 @@ SkCanvas* IOSSurface::CompositeEmbeddedView(int view_id) {
|
||||
}
|
||||
|
||||
// |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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 = "<group>"; };
|
||||
59A97FD7236A49D300B4C066 /* golden_platform_view_multiple_iPhone SE_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_multiple_iPhone SE_simulator.png"; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
6402EBD024147BDA00987DCB /* UnobstructedPlatformViewTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UnobstructedPlatformViewTests.m; sourceTree = "<group>"; };
|
||||
6816DB9C231750ED00A51400 /* GoldenPlatformViewTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GoldenPlatformViewTests.h; sourceTree = "<group>"; };
|
||||
6816DB9D231750ED00A51400 /* GoldenPlatformViewTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GoldenPlatformViewTests.m; sourceTree = "<group>"; };
|
||||
6816DB9F2317573300A51400 /* GoldenImage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GoldenImage.h; sourceTree = "<group>"; };
|
||||
@ -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 */,
|
||||
|
||||
@ -29,6 +29,13 @@
|
||||
// the launchArgsMap should match the one in the `PlatformVieGoldenTestManager`.
|
||||
NSDictionary<NSString*, NSString*>* 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",
|
||||
|
||||
@ -25,7 +25,7 @@ static const NSInteger kSecondsToWaitForPlatformView = 30;
|
||||
[NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject,
|
||||
NSDictionary<NSString*, id>* _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 @@ static const NSInteger kSecondsToWaitForPlatformView = 30;
|
||||
[NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject,
|
||||
NSDictionary<NSString*, id>* _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 @@ static const NSInteger kSecondsToWaitForPlatformView = 30;
|
||||
[NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject,
|
||||
NSDictionary<NSString*, id>* _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]) {
|
||||
|
||||
@ -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 <XCTest/XCTest.h>
|
||||
|
||||
@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
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 20 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 22 KiB |
@ -19,6 +19,12 @@ import 'src/touches_scenario.dart';
|
||||
Map<String, Scenario> _scenarios = <String, Scenario>{
|
||||
'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),
|
||||
|
||||
@ -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),
|
||||
);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user