mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
IOS Platform view transform/clipping (#9075)
This commit is contained in:
parent
13145e90cd
commit
ebb5b909fb
@ -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
|
||||
|
||||
@ -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",
|
||||
]
|
||||
|
||||
|
||||
@ -9,4 +9,34 @@ namespace flutter {
|
||||
bool ExternalViewEmbedder::SubmitFrame(GrContext* context) {
|
||||
return false;
|
||||
};
|
||||
|
||||
void MutatorsStack::pushClipRect(const SkRect& rect) {
|
||||
std::shared_ptr<Mutator> element = std::make_shared<Mutator>(rect);
|
||||
vector_.push_back(element);
|
||||
};
|
||||
|
||||
void MutatorsStack::pushClipRRect(const SkRRect& rrect) {
|
||||
std::shared_ptr<Mutator> element = std::make_shared<Mutator>(rrect);
|
||||
vector_.push_back(element);
|
||||
};
|
||||
|
||||
void MutatorsStack::pushTransform(const SkMatrix& matrix) {
|
||||
std::shared_ptr<Mutator> element = std::make_shared<Mutator>(matrix);
|
||||
vector_.push_back(element);
|
||||
};
|
||||
|
||||
void MutatorsStack::pop() {
|
||||
vector_.pop_back();
|
||||
};
|
||||
|
||||
const std::vector<std::shared_ptr<Mutator>>::const_reverse_iterator
|
||||
MutatorsStack::top() const {
|
||||
return vector_.rend();
|
||||
};
|
||||
|
||||
const std::vector<std::shared_ptr<Mutator>>::const_reverse_iterator
|
||||
MutatorsStack::bottom() const {
|
||||
return vector_.rbegin();
|
||||
};
|
||||
|
||||
} // namespace flutter
|
||||
|
||||
@ -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<std::shared_ptr<Mutator>>::const_reverse_iterator top()
|
||||
const;
|
||||
// Returns an iterator pointing to the bottom of the stack.
|
||||
const std::vector<std::shared_ptr<Mutator>>::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<std::shared_ptr<Mutator>> 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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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<SkPicture> 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<SkPicture> 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)
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
180
flow/mutators_stack_unittests.cc
Normal file
180
flow/mutators_stack_unittests.cc
Normal file
@ -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);
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -91,6 +91,7 @@ void FlutterPlatformViewsController::OnCreate(FlutterMethodCall* call, FlutterRe
|
||||
|
||||
touch_interceptors_[viewId] =
|
||||
fml::scoped_nsobject<FlutterTouchInterceptingView>([touch_interceptor retain]);
|
||||
root_views_[viewId] = fml::scoped_nsobject<UIView>([touch_interceptor retain]);
|
||||
|
||||
result(nil);
|
||||
}
|
||||
@ -182,6 +183,117 @@ std::vector<SkCanvas*> FlutterPlatformViewsController::GetCurrentCanvases() {
|
||||
return canvases;
|
||||
}
|
||||
|
||||
int FlutterPlatformViewsController::CountClips(const MutatorsStack& mutators_stack) {
|
||||
std::vector<std::shared_ptr<Mutator>>::const_reverse_iterator iter = mutators_stack.bottom();
|
||||
int clipCount = 0;
|
||||
while (iter != mutators_stack.top()) {
|
||||
if ((*iter)->isClipType()) {
|
||||
clipCount++;
|
||||
}
|
||||
++iter;
|
||||
}
|
||||
return clipCount;
|
||||
}
|
||||
|
||||
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<std::shared_ptr<Mutator>>::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<UIView>([newPlatformViewRoot retain]);
|
||||
}
|
||||
ApplyMutators(params.mutatorsStack, touchInterceptor);
|
||||
}
|
||||
|
||||
SkCanvas* FlutterPlatformViewsController::CompositeEmbeddedView(
|
||||
int view_id,
|
||||
const flutter::EmbeddedViewParams& params) {
|
||||
@ -193,15 +305,8 @@ SkCanvas* FlutterPlatformViewsController::CompositeEmbeddedView(
|
||||
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 @@ void FlutterPlatformViewsController::Reset() {
|
||||
active_composition_order_.clear();
|
||||
picture_recorders_.clear();
|
||||
current_composition_params_.clear();
|
||||
clip_count_.clear();
|
||||
}
|
||||
|
||||
bool FlutterPlatformViewsController::SubmitFrame(bool gl_rendering,
|
||||
@ -249,15 +355,18 @@ bool FlutterPlatformViewsController::SubmitFrame(bool gl_rendering,
|
||||
|
||||
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 @@ void FlutterPlatformViewsController::DetachUnusedLayers() {
|
||||
|
||||
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 @@ void FlutterPlatformViewsController::DisposeViews() {
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@ -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<std::string, fml::scoped_nsobject<NSObject<FlutterPlatformViewFactory>>> factories_;
|
||||
std::map<int64_t, fml::scoped_nsobject<NSObject<FlutterPlatformView>>> views_;
|
||||
std::map<int64_t, fml::scoped_nsobject<FlutterTouchInterceptingView>> 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<int64_t, fml::scoped_nsobject<UIView>> root_views_;
|
||||
// Mapping a platform view ID to its latest composition params.
|
||||
std::map<int64_t, EmbeddedViewParams> 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<int64_t, int64_t> clip_count_;
|
||||
std::map<int64_t, std::unique_ptr<FlutterPlatformViewLayer>> 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<IOSGLContext> 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);
|
||||
};
|
||||
|
||||
@ -21,4 +21,144 @@ FlutterPlatformViewsController::FlutterPlatformViewsController() = default;
|
||||
|
||||
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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user