diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index d877f2acf507a..053832d1bd1cd 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -71,6 +71,7 @@ FILE: ../../../flutter/flow/layers/transform_layer.h FILE: ../../../flutter/flow/matrix_decomposition.cc FILE: ../../../flutter/flow/matrix_decomposition.h FILE: ../../../flutter/flow/matrix_decomposition_unittests.cc +FILE: ../../../flutter/flow/mutators_stack_unittests.cc FILE: ../../../flutter/flow/paint_utils.cc FILE: ../../../flutter/flow/paint_utils.h FILE: ../../../flutter/flow/raster_cache.cc diff --git a/flow/BUILD.gn b/flow/BUILD.gn index c578a95bd6a18..133cddca4fcd7 100644 --- a/flow/BUILD.gn +++ b/flow/BUILD.gn @@ -114,6 +114,7 @@ executable("flow_unittests") { "layers/performance_overlay_layer_unittests.cc", "layers/physical_shape_layer_unittests.cc", "matrix_decomposition_unittests.cc", + "mutators_stack_unittests.cc", "raster_cache_unittests.cc", ] diff --git a/flow/embedded_views.cc b/flow/embedded_views.cc index e37a837deec0e..9bbd913748079 100644 --- a/flow/embedded_views.cc +++ b/flow/embedded_views.cc @@ -9,4 +9,34 @@ namespace flutter { bool ExternalViewEmbedder::SubmitFrame(GrContext* context) { return false; }; + +void MutatorsStack::pushClipRect(const SkRect& rect) { + std::shared_ptr element = std::make_shared(rect); + vector_.push_back(element); +}; + +void MutatorsStack::pushClipRRect(const SkRRect& rrect) { + std::shared_ptr element = std::make_shared(rrect); + vector_.push_back(element); +}; + +void MutatorsStack::pushTransform(const SkMatrix& matrix) { + std::shared_ptr element = std::make_shared(matrix); + vector_.push_back(element); +}; + +void MutatorsStack::pop() { + vector_.pop_back(); +}; + +const std::vector>::const_reverse_iterator +MutatorsStack::top() const { + return vector_.rend(); +}; + +const std::vector>::const_reverse_iterator +MutatorsStack::bottom() const { + return vector_.rbegin(); +}; + } // namespace flutter diff --git a/flow/embedded_views.h b/flow/embedded_views.h index d37c18f383221..88cfac45d28b2 100644 --- a/flow/embedded_views.h +++ b/flow/embedded_views.h @@ -9,18 +9,169 @@ #include "flutter/fml/memory/ref_counted.h" #include "third_party/skia/include/core/SkCanvas.h" +#include "third_party/skia/include/core/SkPath.h" #include "third_party/skia/include/core/SkPoint.h" +#include "third_party/skia/include/core/SkRRect.h" +#include "third_party/skia/include/core/SkRect.h" #include "third_party/skia/include/core/SkSize.h" namespace flutter { +enum MutatorType { clip_rect, clip_rrect, clip_path, transform }; + +// Stores mutation information like clipping or transform. +// +// The `type` indicates the type of the mutation: clip_rect, transform and etc. +// Each `type` is paired with an object that supports the mutation. For example, +// if the `type` is clip_rect, `rect()` is used the represent the rect to be +// clipped. One mutation object must only contain one type of mutation. +class Mutator { + public: + Mutator(const Mutator& other) { + type_ = other.type_; + switch (other.type_) { + case clip_rect: + rect_ = other.rect_; + break; + case clip_rrect: + rrect_ = other.rrect_; + break; + case clip_path: + path_ = new SkPath(*other.path_); + break; + case transform: + matrix_ = other.matrix_; + break; + default: + break; + } + } + + explicit Mutator(const SkRect& rect) : type_(clip_rect), rect_(rect) {} + explicit Mutator(const SkRRect& rrect) : type_(clip_rrect), rrect_(rrect) {} + explicit Mutator(const SkPath& path) + : type_(clip_path), path_(new SkPath(path)) {} + explicit Mutator(const SkMatrix& matrix) + : type_(transform), matrix_(matrix) {} + + const MutatorType& type() const { return type_; } + const SkRect& rect() const { return rect_; } + const SkRRect& rrect() const { return rrect_; } + const SkPath& path() const { return *path_; } + const SkMatrix& matrix() const { return matrix_; } + + bool operator==(const Mutator& other) const { + if (type_ != other.type_) { + return false; + } + if (type_ == clip_rect && rect_ == other.rect_) { + return true; + } + if (type_ == clip_rrect && rrect_ == other.rrect_) { + return true; + } + if (type_ == clip_path && *path_ == *other.path_) { + return true; + } + if (type_ == transform && matrix_ == other.matrix_) { + return true; + } + + return false; + } + + bool operator!=(const Mutator& other) const { return !operator==(other); } + + bool isClipType() { + return type_ == clip_rect || type_ == clip_rrect || type_ == clip_path; + } + + ~Mutator() { + if (type_ == clip_path) { + delete path_; + } + }; + + private: + MutatorType type_; + + union { + SkRect rect_; + SkRRect rrect_; + SkMatrix matrix_; + SkPath* path_; + }; + +}; // Mutator + +// A stack of mutators that can be applied to an embedded platform view. +// +// The stack may include mutators like transforms and clips, each mutator +// applies to all the mutators that are below it in the stack and to the +// embedded view. +// +// For example consider the following stack: [T1, T2, T3], where T1 is the top +// of the stack and T3 is the bottom of the stack. Applying this mutators stack +// to a platform view P1 will result in T1(T2(T2(P1))). +class MutatorsStack { + public: + MutatorsStack() = default; + + void pushClipRect(const SkRect& rect); + void pushClipRRect(const SkRRect& rrect); + void pushClipPath(const SkPath& path); + + void pushTransform(const SkMatrix& matrix); + + // Removes the `Mutator` on the top of the stack + // and destroys it. + void pop(); + + // Returns an iterator pointing to the top of the stack. + const std::vector>::const_reverse_iterator top() + const; + // Returns an iterator pointing to the bottom of the stack. + const std::vector>::const_reverse_iterator bottom() + const; + + bool operator==(const MutatorsStack& other) const { + if (vector_.size() != other.vector_.size()) { + return false; + } + for (size_t i = 0; i < vector_.size(); i++) { + if (*vector_[i] != *other.vector_[i]) { + return false; + } + } + return true; + } + + bool operator!=(const MutatorsStack& other) const { + return !operator==(other); + } + + private: + std::vector> vector_; +}; // MutatorsStack + class EmbeddedViewParams { public: + EmbeddedViewParams() = default; + + EmbeddedViewParams(const EmbeddedViewParams& other) { + offsetPixels = other.offsetPixels; + sizePoints = other.sizePoints; + mutatorsStack = other.mutatorsStack; + }; + SkPoint offsetPixels; SkSize sizePoints; + MutatorsStack mutatorsStack; bool operator==(const EmbeddedViewParams& other) const { - return offsetPixels == other.offsetPixels && sizePoints == other.sizePoints; + return offsetPixels == other.offsetPixels && + sizePoints == other.sizePoints && + mutatorsStack == other.mutatorsStack; } }; @@ -28,6 +179,8 @@ class EmbeddedViewParams { // in this case ExternalViewEmbedder is a reference to the // FlutterPlatformViewsController which is owned by FlutterViewController. class ExternalViewEmbedder { + // TODO(cyanglaz): Make embedder own the `EmbeddedViewParams`. + public: ExternalViewEmbedder() = default; @@ -46,7 +199,8 @@ class ExternalViewEmbedder { virtual ~ExternalViewEmbedder() = default; FML_DISALLOW_COPY_AND_ASSIGN(ExternalViewEmbedder); -}; + +}; // ExternalViewEmbedder } // namespace flutter diff --git a/flow/layers/clip_rect_layer.cc b/flow/layers/clip_rect_layer.cc index 103e2e021a92e..31562649e1829 100644 --- a/flow/layers/clip_rect_layer.cc +++ b/flow/layers/clip_rect_layer.cc @@ -50,6 +50,8 @@ void ClipRectLayer::Paint(PaintContext& context) const { SkAutoCanvasRestore save(context.internal_nodes_canvas, true); context.internal_nodes_canvas->clipRect(clip_rect_, clip_behavior_ != Clip::hardEdge); + context.mutators_stack.pushClipRect(clip_rect_); + if (clip_behavior_ == Clip::antiAliasWithSaveLayer) { context.internal_nodes_canvas->saveLayer(clip_rect_, nullptr); } @@ -57,6 +59,7 @@ void ClipRectLayer::Paint(PaintContext& context) const { if (clip_behavior_ == Clip::antiAliasWithSaveLayer) { context.internal_nodes_canvas->restore(); } + context.mutators_stack.pop(); } } // namespace flutter diff --git a/flow/layers/clip_rrect_layer.cc b/flow/layers/clip_rrect_layer.cc index cf866089b84b0..5308b806a1e88 100644 --- a/flow/layers/clip_rrect_layer.cc +++ b/flow/layers/clip_rrect_layer.cc @@ -58,6 +58,8 @@ void ClipRRectLayer::Paint(PaintContext& context) const { SkAutoCanvasRestore save(context.internal_nodes_canvas, true); context.internal_nodes_canvas->clipRRect(clip_rrect_, clip_behavior_ != Clip::hardEdge); + context.mutators_stack.pushClipRRect(clip_rrect_); + if (clip_behavior_ == Clip::antiAliasWithSaveLayer) { context.internal_nodes_canvas->saveLayer(paint_bounds(), nullptr); } @@ -65,6 +67,7 @@ void ClipRRectLayer::Paint(PaintContext& context) const { if (clip_behavior_ == Clip::antiAliasWithSaveLayer) { context.internal_nodes_canvas->restore(); } + context.mutators_stack.pop(); } } // namespace flutter diff --git a/flow/layers/layer.h b/flow/layers/layer.h index 7400af5468214..5e30a50c907f9 100644 --- a/flow/layers/layer.h +++ b/flow/layers/layer.h @@ -83,6 +83,7 @@ class Layer { SkCanvas* leaf_nodes_canvas; GrContext* gr_context; ExternalViewEmbedder* view_embedder; + MutatorsStack& mutators_stack; const Stopwatch& raster_time; const Stopwatch& ui_time; TextureRegistry& texture_registry; diff --git a/flow/layers/layer_tree.cc b/flow/layers/layer_tree.cc index 5de5f9a48fba9..57bc87838980b 100644 --- a/flow/layers/layer_tree.cc +++ b/flow/layers/layer_tree.cc @@ -83,11 +83,13 @@ void LayerTree::Paint(CompositorContext::ScopedFrame& frame, } } + MutatorsStack stack; Layer::PaintContext context = { (SkCanvas*)&internal_nodes_canvas, frame.canvas(), frame.gr_context(), frame.view_embedder(), + stack, frame.context().raster_time(), frame.context().ui_time(), frame.context().texture_registry(), @@ -108,6 +110,7 @@ sk_sp LayerTree::Flatten(const SkRect& bounds) { return nullptr; } + MutatorsStack unused_stack; const Stopwatch unused_stopwatch; TextureRegistry unused_texture_registry; SkMatrix root_surface_transformation; @@ -135,6 +138,7 @@ sk_sp LayerTree::Flatten(const SkRect& bounds) { canvas, // canvas nullptr, nullptr, + unused_stack, unused_stopwatch, // frame time (dont care) unused_stopwatch, // engine time (dont care) unused_texture_registry, // texture registry (not supported) diff --git a/flow/layers/performance_overlay_layer_unittests.cc b/flow/layers/performance_overlay_layer_unittests.cc index 0de07c9a70878..f82b0e2963869 100644 --- a/flow/layers/performance_overlay_layer_unittests.cc +++ b/flow/layers/performance_overlay_layer_unittests.cc @@ -47,10 +47,13 @@ TEST(PerformanceOverlayLayer, Gold) { ASSERT_TRUE(surface != nullptr); flutter::TextureRegistry unused_texture_registry; - + flutter::MutatorsStack unused_stack; flutter::Layer::PaintContext paintContext = { - nullptr, surface->getCanvas(), nullptr, nullptr, mock_stopwatch, - mock_stopwatch, unused_texture_registry, nullptr, false}; + nullptr, surface->getCanvas(), + nullptr, nullptr, + unused_stack, mock_stopwatch, + mock_stopwatch, unused_texture_registry, + nullptr, false}; // Specify font file to ensure the same font across different operation // systems. diff --git a/flow/layers/platform_view_layer.cc b/flow/layers/platform_view_layer.cc index 9fde621a6a22c..c7845a867de61 100644 --- a/flow/layers/platform_view_layer.cc +++ b/flow/layers/platform_view_layer.cc @@ -37,6 +37,7 @@ void PlatformViewLayer::Paint(PaintContext& context) const { params.offsetPixels = SkPoint::Make(transform.getTranslateX(), transform.getTranslateY()); params.sizePoints = size_; + params.mutatorsStack = context.mutators_stack; SkCanvas* canvas = context.view_embedder->CompositeEmbeddedView(view_id_, params); diff --git a/flow/layers/transform_layer.cc b/flow/layers/transform_layer.cc index f0a925dcd2f50..1a63ff8a48431 100644 --- a/flow/layers/transform_layer.cc +++ b/flow/layers/transform_layer.cc @@ -66,7 +66,10 @@ void TransformLayer::Paint(PaintContext& context) const { SkAutoCanvasRestore save(context.internal_nodes_canvas, true); context.internal_nodes_canvas->concat(transform_); + context.mutators_stack.pushTransform(transform_); + PaintChildren(context); + context.mutators_stack.pop(); } } // namespace flutter diff --git a/flow/mutators_stack_unittests.cc b/flow/mutators_stack_unittests.cc new file mode 100644 index 0000000000000..16d4b3d5a8ba0 --- /dev/null +++ b/flow/mutators_stack_unittests.cc @@ -0,0 +1,180 @@ +// 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. + +#include "flutter/flow/embedded_views.h" +#include "gtest/gtest.h" + +TEST(MutatorsStack, Initialization) { + flutter::MutatorsStack stack; + ASSERT_TRUE(true); +} + +TEST(MutatorsStack, CopyConstructor) { + flutter::MutatorsStack stack; + SkRRect rrect; + SkRect rect; + stack.pushClipRect(rect); + stack.pushClipRRect(rrect); + flutter::MutatorsStack copy = flutter::MutatorsStack(stack); + ASSERT_TRUE(copy == stack); +} + +TEST(MutatorsStack, PushClipRect) { + flutter::MutatorsStack stack; + SkRect rect; + stack.pushClipRect(rect); + auto iter = stack.bottom(); + ASSERT_TRUE(iter->get()->type() == flutter::MutatorType::clip_rect); + ASSERT_TRUE(iter->get()->rect() == rect); +} + +TEST(MutatorsStack, PushClipRRect) { + flutter::MutatorsStack stack; + SkRRect rrect; + stack.pushClipRRect(rrect); + auto iter = stack.bottom(); + ASSERT_TRUE(iter->get()->type() == flutter::MutatorType::clip_rrect); + ASSERT_TRUE(iter->get()->rrect() == rrect); +} + +TEST(MutatorsStack, PushTransform) { + flutter::MutatorsStack stack; + SkMatrix matrix; + stack.pushTransform(matrix); + auto iter = stack.bottom(); + ASSERT_TRUE(iter->get()->type() == flutter::MutatorType::transform); + ASSERT_TRUE(iter->get()->matrix() == matrix); +} + +TEST(MutatorsStack, Pop) { + flutter::MutatorsStack stack; + SkMatrix matrix; + stack.pushTransform(matrix); + stack.pop(); + auto iter = stack.bottom(); + ASSERT_TRUE(iter == stack.top()); +} + +TEST(MutatorsStack, Traversal) { + flutter::MutatorsStack stack; + SkMatrix matrix; + stack.pushTransform(matrix); + SkRect rect; + stack.pushClipRect(rect); + SkRRect rrect; + stack.pushClipRRect(rrect); + auto iter = stack.bottom(); + int index = 0; + while (iter != stack.top()) { + switch (index) { + case 0: + ASSERT_TRUE(iter->get()->type() == flutter::MutatorType::clip_rrect); + ASSERT_TRUE(iter->get()->rrect() == rrect); + break; + case 1: + ASSERT_TRUE(iter->get()->type() == flutter::MutatorType::clip_rect); + ASSERT_TRUE(iter->get()->rect() == rect); + break; + case 2: + ASSERT_TRUE(iter->get()->type() == flutter::MutatorType::transform); + ASSERT_TRUE(iter->get()->matrix() == matrix); + break; + default: + break; + } + ++iter; + ++index; + } +} + +TEST(MutatorsStack, Equality) { + flutter::MutatorsStack stack; + SkMatrix matrix = SkMatrix::MakeScale(1, 1); + stack.pushTransform(matrix); + SkRect rect = SkRect::MakeEmpty(); + stack.pushClipRect(rect); + SkRRect rrect = SkRRect::MakeEmpty(); + stack.pushClipRRect(rrect); + + flutter::MutatorsStack stackOther; + SkMatrix matrixOther = SkMatrix::MakeScale(1, 1); + stackOther.pushTransform(matrixOther); + SkRect rectOther = SkRect::MakeEmpty(); + stackOther.pushClipRect(rectOther); + SkRRect rrectOther = SkRRect::MakeEmpty(); + stackOther.pushClipRRect(rrectOther); + + ASSERT_TRUE(stack == stackOther); +} + +TEST(Mutator, Initialization) { + SkRect rect = SkRect::MakeEmpty(); + flutter::Mutator mutator = flutter::Mutator(rect); + ASSERT_TRUE(mutator.type() == flutter::MutatorType::clip_rect); + ASSERT_TRUE(mutator.rect() == rect); + + SkRRect rrect; + flutter::Mutator mutator2 = flutter::Mutator(rrect); + ASSERT_TRUE(mutator2.type() == flutter::MutatorType::clip_rrect); + ASSERT_TRUE(mutator2.rrect() == rrect); + + SkPath path; + flutter::Mutator mutator3 = flutter::Mutator(path); + ASSERT_TRUE(mutator3.type() == flutter::MutatorType::clip_path); + ASSERT_TRUE(mutator3.path() == path); + + SkMatrix matrix; + flutter::Mutator mutator4 = flutter::Mutator(matrix); + ASSERT_TRUE(mutator4.type() == flutter::MutatorType::transform); + ASSERT_TRUE(mutator4.matrix() == matrix); +} + +TEST(Mutator, CopyConstructor) { + SkRect rect = SkRect::MakeEmpty(); + flutter::Mutator mutator = flutter::Mutator(rect); + flutter::Mutator copy = flutter::Mutator(mutator); + ASSERT_TRUE(mutator == copy); + + SkRRect rrect; + flutter::Mutator mutator2 = flutter::Mutator(rrect); + flutter::Mutator copy2 = flutter::Mutator(mutator2); + ASSERT_TRUE(mutator2 == copy2); + + SkPath path; + flutter::Mutator mutator3 = flutter::Mutator(path); + flutter::Mutator copy3 = flutter::Mutator(mutator3); + ASSERT_TRUE(mutator3 == copy3); + + SkMatrix matrix; + flutter::Mutator mutator4 = flutter::Mutator(matrix); + flutter::Mutator copy4 = flutter::Mutator(mutator4); + ASSERT_TRUE(mutator4 == copy4); +} + +TEST(Mutator, Equality) { + SkMatrix matrix; + flutter::Mutator mutator = flutter::Mutator(matrix); + flutter::Mutator otherMutator = flutter::Mutator(matrix); + ASSERT_TRUE(mutator == otherMutator); + + SkRect rect = SkRect::MakeEmpty(); + flutter::Mutator mutator2 = flutter::Mutator(rect); + flutter::Mutator otherMutator2 = flutter::Mutator(rect); + ASSERT_TRUE(mutator2 == otherMutator2); + + SkRRect rrect; + flutter::Mutator mutator3 = flutter::Mutator(rrect); + flutter::Mutator otherMutator3 = flutter::Mutator(rrect); + ASSERT_TRUE(mutator3 == otherMutator3); + + ASSERT_FALSE(mutator2 == mutator); +} + +TEST(Mutator, UnEquality) { + SkRect rect = SkRect::MakeEmpty(); + flutter::Mutator mutator = flutter::Mutator(rect); + SkMatrix matrix; + flutter::Mutator notEqualMutator = flutter::Mutator(matrix); + ASSERT_TRUE(notEqualMutator != mutator); +} diff --git a/flow/raster_cache.cc b/flow/raster_cache.cc index 4ad18adf172e4..be0c88d5cc7e8 100644 --- a/flow/raster_cache.cc +++ b/flow/raster_cache.cc @@ -159,6 +159,7 @@ void RasterCache::Prepare(PrerollContext* context, entry.image = Rasterize(context->gr_context, ctm, context->dst_color_space, checkerboard_images_, layer->paint_bounds(), [layer, context](SkCanvas* canvas) { + MutatorsStack stack; SkISize canvas_size = canvas->getBaseLayerSize(); SkNWayCanvas internal_nodes_canvas( canvas_size.width(), canvas_size.height()); @@ -168,6 +169,7 @@ void RasterCache::Prepare(PrerollContext* context, canvas, context->gr_context, nullptr, + stack, context->raster_time, context->ui_time, context->texture_registry, diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index 4a530922fa89e..ea838e3817298 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -91,6 +91,7 @@ touch_interceptors_[viewId] = fml::scoped_nsobject([touch_interceptor retain]); + root_views_[viewId] = fml::scoped_nsobject([touch_interceptor retain]); result(nil); } @@ -182,6 +183,117 @@ return canvases; } +int FlutterPlatformViewsController::CountClips(const MutatorsStack& mutators_stack) { + std::vector>::const_reverse_iterator iter = mutators_stack.bottom(); + int clipCount = 0; + while (iter != mutators_stack.top()) { + if ((*iter)->isClipType()) { + clipCount++; + } + ++iter; + } + return clipCount; +} + +UIView* FlutterPlatformViewsController::ReconstructClipViewsChain(int number_of_clips, + UIView* platform_view, + UIView* head_clip_view) { + NSInteger indexInFlutterView = -1; + if (head_clip_view.superview) { + // TODO(cyanglaz): potentially cache the index of oldPlatformViewRoot to make this a O(1). + // https://github.com/flutter/flutter/issues/35023 + indexInFlutterView = [flutter_view_.get().subviews indexOfObject:head_clip_view]; + [head_clip_view removeFromSuperview]; + } + UIView* head = platform_view; + int clipIndex = 0; + // Re-use as much existing clip views as needed. + while (head != head_clip_view && clipIndex < number_of_clips) { + head = head.superview; + clipIndex++; + } + // If there were not enough existing clip views, add more. + while (clipIndex < number_of_clips) { + ChildClippingView* clippingView = [ChildClippingView new]; + [clippingView addSubview:head]; + head = clippingView; + clipIndex++; + } + [head removeFromSuperview]; + + if (indexInFlutterView > -1) { + // The chain was previously attached; attach it to the same position. + [flutter_view_.get() insertSubview:head atIndex:indexInFlutterView]; + } + return head; +} + +void FlutterPlatformViewsController::ApplyMutators(const MutatorsStack& mutators_stack, + UIView* embedded_view) { + UIView* head = embedded_view; + head.clipsToBounds = YES; + head.layer.transform = CATransform3DIdentity; + ResetAnchor(head.layer); + + std::vector>::const_reverse_iterator iter = mutators_stack.bottom(); + while (iter != mutators_stack.top()) { + switch ((*iter)->type()) { + case transform: { + CATransform3D transform = GetCATransform3DFromSkMatrix((*iter)->matrix()); + head.layer.transform = CATransform3DConcat(head.layer.transform, transform); + break; + } + case clip_rect: + case clip_rrect: + case clip_path: { + ChildClippingView* clipView = (ChildClippingView*)head.superview; + clipView.layer.transform = CATransform3DIdentity; + [clipView setClip:(*iter)->type() + rect:(*iter)->rect() + rrect:(*iter)->rrect() + path:(*iter)->path()]; + head.clipsToBounds = YES; + ResetAnchor(clipView.layer); + head = clipView; + break; + } + } + ++iter; + } + + // Reverse scale based on screen scale. + // + // The UIKit frame is set based on the logical resolution instead of physical. + // (https://developer.apple.com/library/archive/documentation/DeviceInformation/Reference/iOSDeviceCompatibility/Displays/Displays.html). + // However, flow is based on the physical resolution. For eaxmple, 1000 pixels in flow equals + // 500 points in UIKit. And until this point, we did all the calculation based on the flow + // resolution. So we need to scale down to match UIKit's logical resolution. + CGFloat screenScale = [UIScreen mainScreen].scale; + head.layer.transform = CATransform3DConcat( + head.layer.transform, CATransform3DMakeScale(1 / screenScale, 1 / screenScale, 1)); +} + +void FlutterPlatformViewsController::CompositeWithParams( + int view_id, + const flutter::EmbeddedViewParams& params) { + CGRect frame = CGRectMake(0, 0, params.sizePoints.width(), params.sizePoints.height()); + UIView* touchInterceptor = touch_interceptors_[view_id].get(); + touchInterceptor.frame = frame; + + int currentClippingCount = CountClips(params.mutatorsStack); + int previousClippingCount = clip_count_[view_id]; + if (currentClippingCount != previousClippingCount) { + clip_count_[view_id] = currentClippingCount; + // If we have a different clipping count in this frame, we need to reconstruct the + // ClippingChildView chain to prepare for `ApplyMutators`. + UIView* oldPlatformViewRoot = root_views_[view_id].get(); + UIView* newPlatformViewRoot = + ReconstructClipViewsChain(currentClippingCount, touchInterceptor, oldPlatformViewRoot); + root_views_[view_id] = fml::scoped_nsobject([newPlatformViewRoot retain]); + } + ApplyMutators(params.mutatorsStack, touchInterceptor); +} + SkCanvas* FlutterPlatformViewsController::CompositeEmbeddedView( int view_id, const flutter::EmbeddedViewParams& params) { @@ -193,15 +305,8 @@ current_composition_params_[view_id] == params) { return picture_recorders_[view_id]->getRecordingCanvas(); } - current_composition_params_[view_id] = params; - - CGFloat screenScale = [[UIScreen mainScreen] scale]; - CGRect rect = - CGRectMake(params.offsetPixels.x() / screenScale, params.offsetPixels.y() / screenScale, - params.sizePoints.width(), params.sizePoints.height()); - - UIView* touch_interceptor = touch_interceptors_[view_id].get(); - [touch_interceptor setFrame:rect]; + current_composition_params_[view_id] = EmbeddedViewParams(params); + CompositeWithParams(view_id, params); return picture_recorders_[view_id]->getRecordingCanvas(); } @@ -217,6 +322,7 @@ active_composition_order_.clear(); picture_recorders_.clear(); current_composition_params_.clear(); + clip_count_.clear(); } bool FlutterPlatformViewsController::SubmitFrame(bool gl_rendering, @@ -249,15 +355,18 @@ for (size_t i = 0; i < composition_order_.size(); i++) { int view_id = composition_order_[i]; - UIView* intercepter = touch_interceptors_[view_id].get(); + // 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(intercepter.superview == overlay.superview); + FML_CHECK(platform_view_root.superview == overlay.superview); - if (intercepter.superview == flutter_view) { - [flutter_view bringSubviewToFront:intercepter]; + if (platform_view_root.superview == flutter_view) { + [flutter_view bringSubviewToFront:platform_view_root]; [flutter_view bringSubviewToFront:overlay]; } else { - [flutter_view addSubview:intercepter]; + [flutter_view addSubview:platform_view_root]; [flutter_view addSubview:overlay]; } @@ -276,10 +385,14 @@ for (int64_t view_id : active_composition_order_) { if (composition_order_set.find(view_id) == composition_order_set.end()) { - if (touch_interceptors_.find(view_id) == touch_interceptors_.end()) { + if (root_views_.find(view_id) == root_views_.end()) { continue; } - [touch_interceptors_[view_id].get() removeFromSuperview]; + // 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]; } } @@ -291,12 +404,14 @@ } for (int64_t viewId : views_to_dispose_) { - UIView* touch_interceptor = touch_interceptors_[viewId].get(); - [touch_interceptor removeFromSuperview]; + UIView* root_view = root_views_[viewId].get(); + [root_view removeFromSuperview]; views_.erase(viewId); touch_interceptors_.erase(viewId); + root_views_.erase(viewId); overlays_.erase(viewId); current_composition_params_.erase(viewId); + clip_count_.erase(viewId); } views_to_dispose_.clear(); } diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h index 8c76a45916b58..dfa4816f48d38 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h @@ -28,8 +28,29 @@ - (void)blockGesture; @end +// The parent view handles clipping to its subviews. +@interface ChildClippingView : UIView + +// Performs the clipping based on the type. +// +// The `type` must be one of the 3: clip_rect, clip_rrect, clip_path. +- (void)setClip:(flutter::MutatorType)type + rect:(const SkRect&)rect + rrect:(const SkRRect&)rrect + path:(const SkPath&)path; + +@end + namespace flutter { +// Converts a SkMatrix to CATransform3D. +// Certain fields are ignored in CATransform3D since SkMatrix is 3x3 and CATransform3D is 4x4. +CATransform3D GetCATransform3DFromSkMatrix(const SkMatrix& matrix); + +// Reset the anchor of `layer` to match the tranform operation from flow. +// The position of the `layer` should be unchanged after resetting the anchor. +void ResetAnchor(CALayer* layer); + class IOSGLContext; class IOSSurface; @@ -88,8 +109,17 @@ class FlutterPlatformViewsController { std::map>> factories_; std::map>> views_; std::map> touch_interceptors_; + // Mapping a platform view ID to the top most parent view (root_view) who is a direct child to the + // `flutter_view_`. + // + // The platform view with the view ID is a child of the root view; If the platform view is not + // clipped, and no clipping view is added, the root view will be the intercepting view. + std::map> root_views_; // Mapping a platform view ID to its latest composition params. std::map current_composition_params_; + // 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_; // The GrContext that is currently used by all of the overlay surfaces. // We track this to know when the GrContext for the Flutter app has changed @@ -122,6 +152,40 @@ class FlutterPlatformViewsController { void EnsureGLOverlayInitialized(int64_t overlay_id, std::shared_ptr gl_context, GrContext* gr_context); + // Traverse the `mutators_stack` and return the number of clip operations. + int CountClips(const MutatorsStack& mutators_stack); + + // Make sure that platform_view has exactly clip_count ChildClippingView ancestors. + // + // Existing ChildClippingViews are re-used. If there are currently more ChildClippingView + // ancestors than needed, the extra views are detached. If there are less ChildClippingView + // ancestors than needed, new ChildClippingViews will be added. + // + // If head_clip_view was attached as a subview to FlutterView, the head of the newly constructed + // ChildClippingViews chain is attached to FlutterView in the same position. + // + // Returns the new head of the clip views chain. + UIView* ReconstructClipViewsChain(int number_of_clips, + UIView* platform_view, + UIView* head_clip_view); + + // Applies the mutators in the mutators_stack to the UIView chain that was constructed by + // `ReconstructClipViewsChain` + // + // Clips are applied to the super view with a CALayer mask. Transforms are applied to the current + // view that's at the head of the chain. For example the following mutators stack [T_1, C_2, T_3, + // T_4, C_5, T_6] where T denotes a transform and C denotes a clip, will result in the following + // UIView tree: + // + // C_2 -> C_5 -> PLATFORM_VIEW + // (PLATFORM_VIEW is a subview of C_5 which is a subview of C_2) + // + // T_1 is applied to C_2, T_3 and T_4 are applied to C_5, and T_6 is applied to PLATFORM_VIEW. + // + // After each clip operation, we update the head to the super view of the current head. + void ApplyMutators(const MutatorsStack& mutators_stack, UIView* embedded_view); + + void CompositeWithParams(int view_id, const flutter::EmbeddedViewParams& params); 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 7c2fb3e6ca5a2..7afb8c3314478 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -21,4 +21,144 @@ FlutterPlatformViewsController::~FlutterPlatformViewsController() = default; +CATransform3D GetCATransform3DFromSkMatrix(const SkMatrix& matrix) { + // Skia only supports 2D transform so we don't map z. + CATransform3D transform = CATransform3DIdentity; + transform.m11 = matrix.getScaleX(); + transform.m21 = matrix.getSkewX(); + transform.m41 = matrix.getTranslateX(); + transform.m14 = matrix.getPerspX(); + + transform.m12 = matrix.getSkewY(); + transform.m22 = matrix.getScaleY(); + transform.m42 = matrix.getTranslateY(); + transform.m24 = matrix.getPerspY(); + return transform; +} + +void ResetAnchor(CALayer* layer) { + // Flow uses (0, 0) to apply transform matrix so we need to match that in Quartz. + layer.anchorPoint = CGPointZero; + layer.position = CGPointZero; +} + } // namespace flutter + +@implementation ChildClippingView + ++ (CGRect)getCGRectFromSkRect:(const SkRect&)clipSkRect { + return CGRectMake(clipSkRect.fLeft, clipSkRect.fTop, clipSkRect.fRight - clipSkRect.fLeft, + clipSkRect.fBottom - clipSkRect.fTop); +} + +- (void)clipRect:(const SkRect&)clipSkRect { + CGRect clipRect = [ChildClippingView getCGRectFromSkRect:clipSkRect]; + CGPathRef pathRef = CGPathCreateWithRect(clipRect, nil); + CAShapeLayer* clip = [[CAShapeLayer alloc] init]; + clip.path = pathRef; + self.layer.mask = clip; + CGPathRelease(pathRef); +} + +- (void)clipRRect:(const SkRRect&)clipSkRRect { + CGPathRef pathRef = nullptr; + switch (clipSkRRect.getType()) { + case SkRRect::kEmpty_Type: { + break; + } + case SkRRect::kRect_Type: { + [self clipRect:clipSkRRect.rect()]; + return; + } + case SkRRect::kOval_Type: + case SkRRect::kSimple_Type: { + CGRect clipRect = [ChildClippingView getCGRectFromSkRect:clipSkRRect.rect()]; + pathRef = CGPathCreateWithRoundedRect(clipRect, clipSkRRect.getSimpleRadii().x(), + clipSkRRect.getSimpleRadii().y(), nil); + break; + } + case SkRRect::kNinePatch_Type: + case SkRRect::kComplex_Type: { + CGMutablePathRef mutablePathRef = CGPathCreateMutable(); + // Complex types, we manually add each corner. + SkRect clipSkRect = clipSkRRect.rect(); + SkVector topLeftRadii = clipSkRRect.radii(SkRRect::kUpperLeft_Corner); + SkVector topRightRadii = clipSkRRect.radii(SkRRect::kUpperRight_Corner); + SkVector bottomRightRadii = clipSkRRect.radii(SkRRect::kLowerRight_Corner); + SkVector bottomLeftRadii = clipSkRRect.radii(SkRRect::kLowerLeft_Corner); + + // Start drawing RRect + // Move point to the top left corner adding the top left radii's x. + CGPathMoveToPoint(mutablePathRef, nil, clipSkRect.fLeft + topLeftRadii.x(), clipSkRect.fTop); + // Move point horizontally right to the top right corner and add the top right curve. + CGPathAddLineToPoint(mutablePathRef, nil, clipSkRect.fRight - topRightRadii.x(), + clipSkRect.fTop); + CGPathAddCurveToPoint(mutablePathRef, nil, clipSkRect.fRight, clipSkRect.fTop, + clipSkRect.fRight, clipSkRect.fTop + topRightRadii.y(), + clipSkRect.fRight, clipSkRect.fTop + topRightRadii.y()); + // Move point vertically down to the bottom right corner and add the bottom right curve. + CGPathAddLineToPoint(mutablePathRef, nil, clipSkRect.fRight, + clipSkRect.fBottom - bottomRightRadii.y()); + CGPathAddCurveToPoint(mutablePathRef, nil, clipSkRect.fRight, clipSkRect.fBottom, + clipSkRect.fRight - bottomRightRadii.x(), clipSkRect.fBottom, + clipSkRect.fRight - bottomRightRadii.x(), clipSkRect.fBottom); + // Move point horizontally left to the bottom left corner and add the bottom left curve. + CGPathAddLineToPoint(mutablePathRef, nil, clipSkRect.fLeft + bottomLeftRadii.x(), + clipSkRect.fBottom); + CGPathAddCurveToPoint(mutablePathRef, nil, clipSkRect.fLeft, clipSkRect.fBottom, + clipSkRect.fLeft, clipSkRect.fBottom - bottomLeftRadii.y(), + clipSkRect.fLeft, clipSkRect.fBottom - bottomLeftRadii.y()); + // Move point vertically up to the top left corner and add the top left curve. + CGPathAddLineToPoint(mutablePathRef, nil, clipSkRect.fLeft, + clipSkRect.fTop + topLeftRadii.y()); + CGPathAddCurveToPoint(mutablePathRef, nil, clipSkRect.fLeft, clipSkRect.fTop, + clipSkRect.fLeft + topLeftRadii.x(), clipSkRect.fTop, + clipSkRect.fLeft + topLeftRadii.x(), clipSkRect.fTop); + CGPathCloseSubpath(mutablePathRef); + + pathRef = mutablePathRef; + break; + } + } + // TODO(cyanglaz): iOS does not seem to support hard edge on CAShapeLayer. It clearly stated that + // the CAShaperLayer will be drawn antialiased. Need to figure out a way to do the hard edge + // clipping on iOS. + CAShapeLayer* clip = [[CAShapeLayer alloc] init]; + clip.path = pathRef; + self.layer.mask = clip; + CGPathRelease(pathRef); +} + +- (void)setClip:(flutter::MutatorType)type + rect:(const SkRect&)rect + rrect:(const SkRRect&)rrect + path:(const SkPath&)path { + FML_CHECK(type == flutter::clip_rect || type == flutter::clip_rrect || + type == flutter::clip_path); + switch (type) { + case flutter::clip_rect: + [self clipRect:rect]; + break; + case flutter::clip_rrect: + [self clipRRect:rrect]; + break; + case flutter::clip_path: + // TODO(cyanglaz): Add clip path + break; + default: + break; + } +} + +// The ChildClippingView is as big as the FlutterView, we only want touches to be hit tested and +// consumed by this view if they are inside the smaller child view. +- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event { + for (UIView* view in self.subviews) { + if ([view pointInside:[self convertPoint:point toView:view] withEvent:event]) { + return YES; + } + } + return NO; +} + +@end