reverted view clipping changes
Summary: Reveting the recent view clipping changes, since it doesn't work well with modals and the fix is not super simple. Reviewed By: mmmulani Differential Revision: D4204490 fbshipit-source-id: 510f2b04c604b3f3a223dc4accb424b030876fbe
This commit is contained in:
parent
cac64f98a9
commit
a78ee4323b
|
@ -105,7 +105,6 @@
|
|||
2DD323EA1DA2DE3F000FE1B8 /* libReact-tvOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2DD323D91DA2DD8B000FE1B8 /* libReact-tvOS.a */; };
|
||||
3578590A1B28D2CF00341EDB /* libRCTLinking.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 357859011B28D2C500341EDB /* libRCTLinking.a */; };
|
||||
39AA31A41DC1DFDC000F7EBB /* RCTUnicodeDecodeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 39AA31A31DC1DFDC000F7EBB /* RCTUnicodeDecodeTests.m */; };
|
||||
397D6A731DB12C1100E99986 /* RCTSubviewClippingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 397D6A711DB12C1100E99986 /* RCTSubviewClippingTests.m */; };
|
||||
3D13F8481D6F6AF900E69E0E /* ImageInBundle.png in Resources */ = {isa = PBXBuildFile; fileRef = 3D13F8441D6F6AF200E69E0E /* ImageInBundle.png */; };
|
||||
3D13F84A1D6F6AFD00E69E0E /* OtherImages.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3D13F8451D6F6AF200E69E0E /* OtherImages.xcassets */; };
|
||||
3D299BAF1D33EBFA00FA1057 /* RCTLoggingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D299BAE1D33EBFA00FA1057 /* RCTLoggingTests.m */; };
|
||||
|
@ -391,7 +390,6 @@
|
|||
2DD323A51DA2DD8B000FE1B8 /* UIExplorer-tvOSUnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "UIExplorer-tvOSUnitTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
357858F81B28D2C400341EDB /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = ../../Libraries/LinkingIOS/RCTLinking.xcodeproj; sourceTree = "<group>"; };
|
||||
39AA31A31DC1DFDC000F7EBB /* RCTUnicodeDecodeTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTUnicodeDecodeTests.m; sourceTree = "<group>"; };
|
||||
397D6A711DB12C1100E99986 /* RCTSubviewClippingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSubviewClippingTests.m; sourceTree = "<group>"; };
|
||||
3D13F83E1D6F6AE000E69E0E /* UIExplorerBundle.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UIExplorerBundle.bundle; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3D13F8401D6F6AE000E69E0E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = ../Info.plist; sourceTree = "<group>"; };
|
||||
3D13F8441D6F6AF200E69E0E /* ImageInBundle.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = ImageInBundle.png; sourceTree = "<group>"; };
|
||||
|
@ -618,7 +616,6 @@
|
|||
143BC57C1B21E18100462512 /* UIExplorerUnitTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
397D6A711DB12C1100E99986 /* RCTSubviewClippingTests.m */,
|
||||
13B6C1A21C34225900D3FAF5 /* RCTURLUtilsTests.m */,
|
||||
68FF44371CF6111500720EFD /* RCTBundleURLProviderTests.m */,
|
||||
1497CFA41B21F5E400C1F8F2 /* RCTAllocationTests.m */,
|
||||
|
@ -1344,7 +1341,6 @@
|
|||
1497CFAE1B21F5E400C1F8F2 /* RCTJSCExecutorTests.m in Sources */,
|
||||
13129DD41C85F87C007D611C /* RCTModuleInitNotificationRaceTests.m in Sources */,
|
||||
1497CFAD1B21F5E400C1F8F2 /* RCTBridgeTests.m in Sources */,
|
||||
397D6A731DB12C1100E99986 /* RCTSubviewClippingTests.m in Sources */,
|
||||
134CB92A1C85A38800265FA6 /* RCTModuleInitTests.m in Sources */,
|
||||
1497CFB11B21F5E400C1F8F2 /* RCTEventDispatcherTests.m in Sources */,
|
||||
1497CFB31B21F5E400C1F8F2 /* RCTUIManagerTests.m in Sources */,
|
||||
|
|
|
@ -1,407 +0,0 @@
|
|||
/**
|
||||
* The examples provided by Facebook are for non-commercial testing and
|
||||
* evaluation purposes only.
|
||||
*
|
||||
* Facebook reserves all rights not expressly granted.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
|
||||
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#import <objc/message.h>
|
||||
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#import "UIView+React.h"
|
||||
#import "UIView+Private.h"
|
||||
#import "RCTView.h"
|
||||
#import "RCTScrollView.h"
|
||||
#import "RCTRootView.h"
|
||||
#import "RCTViewManager.h"
|
||||
#import "RCTComponentData.h"
|
||||
|
||||
|
||||
@interface RCTSubviewClippingTests : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation RCTSubviewClippingTests
|
||||
|
||||
- (void)testViewOverlappingBoundsOfClippingViewIsNotClipped
|
||||
{
|
||||
RCTView *clippingView = [RCTView new];
|
||||
[clippingView rct_setRemovesClippedSubviews:YES];
|
||||
[clippingView reactSetFrame:CGRectMake(0, 0, 50, 50)];
|
||||
|
||||
RCTView *childView = [RCTView new];
|
||||
[childView reactSetFrame:CGRectMake(25, 25, 50, 50)];
|
||||
[clippingView insertReactSubview:childView atIndex:0];
|
||||
[clippingView didUpdateReactSubviews];
|
||||
|
||||
XCTAssertEqual(clippingView.subviews.count, 1u);
|
||||
}
|
||||
|
||||
- (void)testViewOutsideBoundsOfClippingViewIsClipped
|
||||
{
|
||||
RCTView *clippingView = [RCTView new];
|
||||
[clippingView rct_setRemovesClippedSubviews:YES];
|
||||
[clippingView reactSetFrame:CGRectMake(0, 0, 50, 50)];
|
||||
|
||||
RCTView *childView = [RCTView new];
|
||||
[childView reactSetFrame:CGRectMake(50, 50, 50, 50)];
|
||||
[clippingView insertReactSubview:childView atIndex:0];
|
||||
[clippingView didUpdateReactSubviews];
|
||||
|
||||
XCTAssertEqual(clippingView.subviews.count, 0u);
|
||||
}
|
||||
|
||||
- (void)testTurningOnClippingShouldRemoveView
|
||||
{
|
||||
RCTView *clippingView = [RCTView new];
|
||||
[clippingView reactSetFrame:CGRectMake(0, 0, 50, 50)];
|
||||
|
||||
RCTView *childView = [RCTView new];
|
||||
[childView reactSetFrame:CGRectMake(50, 50, 50, 50)];
|
||||
[clippingView insertReactSubview:childView atIndex:0];
|
||||
[clippingView didUpdateReactSubviews];
|
||||
|
||||
XCTAssertEqual(clippingView.subviews.count, 1u);
|
||||
[clippingView rct_setRemovesClippedSubviews:YES];
|
||||
XCTAssertEqual(clippingView.subviews.count, 0u);
|
||||
}
|
||||
|
||||
- (void)testTurningOffClippingShouldAddViewBack
|
||||
{
|
||||
RCTView *clippingView = [RCTView new];
|
||||
[clippingView rct_setRemovesClippedSubviews:YES];
|
||||
[clippingView reactSetFrame:CGRectMake(0, 0, 50, 50)];
|
||||
|
||||
RCTView *childView = [RCTView new];
|
||||
[childView reactSetFrame:CGRectMake(50, 50, 50, 50)];
|
||||
[clippingView insertReactSubview:childView atIndex:0];
|
||||
[clippingView didUpdateReactSubviews];
|
||||
|
||||
XCTAssertEqual(clippingView.subviews.count, 0u);
|
||||
[clippingView rct_setRemovesClippedSubviews:NO];
|
||||
XCTAssertEqual(clippingView.subviews.count, 1u);
|
||||
}
|
||||
|
||||
- (void)testTransformedClippedViewBackToClippingViewAddsItBack
|
||||
{
|
||||
RCTView *clippingView = [RCTView new];
|
||||
[clippingView rct_setRemovesClippedSubviews:YES];
|
||||
[clippingView reactSetFrame:CGRectMake(0, 0, 50, 50)];
|
||||
|
||||
RCTView *childView = [RCTView new];
|
||||
[childView reactSetFrame:CGRectMake(50, 50, 50, 50)];
|
||||
[clippingView insertReactSubview:childView atIndex:0];
|
||||
[clippingView didUpdateReactSubviews];
|
||||
|
||||
XCTAssertEqual(clippingView.subviews.count, 0u);
|
||||
|
||||
// Setting the transform property on a view has to be done the same way RN from js would do it.
|
||||
// That unfortuantely involves some arbitrary-looking setup based on how RCTComponentData's internals works.
|
||||
id mockBridge = [OCMockObject mockForClass:[RCTBridge class]];
|
||||
[[[mockBridge stub] andReturn:[RCTViewManager new]] moduleForClass:OCMOCK_ANY];
|
||||
RCTComponentData *componentData = [[RCTComponentData alloc] initWithManagerClass:[RCTViewManager class] bridge:mockBridge];
|
||||
// this transform moves the childView to match bounds of its clippingView
|
||||
[componentData setProps:@{@"transform": @[@1,@0,@0,@0,@0,@1,@0,@0,@0,@0,@1,@0,@-50,@-50,@0,@1]} forView:childView];
|
||||
|
||||
XCTAssertEqual(clippingView.subviews.count, 1u);
|
||||
}
|
||||
|
||||
- (void)testMovingClippedViewBackToClippingViewAddsItBack
|
||||
{
|
||||
RCTView *clippingView = [RCTView new];
|
||||
[clippingView rct_setRemovesClippedSubviews:YES];
|
||||
[clippingView reactSetFrame:CGRectMake(0, 0, 50, 50)];
|
||||
|
||||
RCTView *childView = [RCTView new];
|
||||
[childView reactSetFrame:CGRectMake(50, 50, 50, 50)];
|
||||
[clippingView insertReactSubview:childView atIndex:0];
|
||||
[clippingView didUpdateReactSubviews];
|
||||
|
||||
XCTAssertEqual(clippingView.subviews.count, 0u);
|
||||
[childView reactSetFrame:CGRectMake(0, 0, 50, 50)];
|
||||
XCTAssertEqual(clippingView.subviews.count, 1u);
|
||||
}
|
||||
|
||||
- (void)testResizingClippingViewToContainClippedViewAddsTheClippedViewBack
|
||||
{
|
||||
RCTView *clippingView = [RCTView new];
|
||||
[clippingView rct_setRemovesClippedSubviews:YES];
|
||||
[clippingView reactSetFrame:CGRectMake(0, 0, 50, 50)];
|
||||
|
||||
RCTView *childView = [RCTView new];
|
||||
[childView reactSetFrame:CGRectMake(50, 50, 50, 50)];
|
||||
[clippingView insertReactSubview:childView atIndex:0];
|
||||
[clippingView didUpdateReactSubviews];
|
||||
|
||||
XCTAssertEqual(clippingView.subviews.count, 0u);
|
||||
[clippingView reactSetFrame:(CGRect){{0,0},{100,100}}];
|
||||
XCTAssertEqual(clippingView.subviews.count, 1u);
|
||||
}
|
||||
|
||||
#pragma mark - zIndex tests
|
||||
|
||||
/**
|
||||
This test case models a following setup:
|
||||
|
||||
+--------+
|
||||
| |
|
||||
| |
|
||||
***** | |
|
||||
*C * | |
|
||||
* +-*--+ |
|
||||
* | * | |
|
||||
***** | |
|
||||
| | z3|
|
||||
| +---+----+
|
||||
+----+ |
|
||||
| | |
|
||||
| | |
|
||||
| | z2|
|
||||
| +---+----+
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
| z1|
|
||||
+--------+
|
||||
|
||||
*/
|
||||
- (void)testZIndexOrderingIsPreservedAfterRetogglingClippingCase1
|
||||
{
|
||||
RCTView *clippingView = [RCTView new];
|
||||
[clippingView reactSetFrame:CGRectMake(0, 0, 50, 50)];
|
||||
|
||||
RCTView *childView1 = [RCTView new];
|
||||
[childView1 reactSetFrame:CGRectMake(-25, 75, 100, 100)];
|
||||
[childView1 setReactZIndex:1];
|
||||
[clippingView insertReactSubview:childView1 atIndex:0];
|
||||
|
||||
RCTView *childView2 = [RCTView new];
|
||||
[childView2 reactSetFrame:CGRectMake(25, 25, 100, 100)];
|
||||
[childView2 setReactZIndex:2];
|
||||
[clippingView insertReactSubview:childView2 atIndex:0];
|
||||
|
||||
RCTView *childView3 = [RCTView new];
|
||||
[childView3 reactSetFrame:CGRectMake(75, -25, 100, 100)];
|
||||
[childView3 setReactZIndex:3];
|
||||
[clippingView insertReactSubview:childView3 atIndex:0];
|
||||
[clippingView didUpdateReactSubviews];
|
||||
[clippingView clearSortedSubviews];
|
||||
|
||||
XCTAssert(([clippingView.subviews isEqualToArray:@[childView1, childView2, childView3]]));
|
||||
[clippingView rct_setRemovesClippedSubviews:YES];
|
||||
XCTAssert(([clippingView.subviews isEqualToArray:@[childView2]]));
|
||||
[clippingView rct_setRemovesClippedSubviews:NO];
|
||||
XCTAssert(([clippingView.subviews isEqualToArray:@[childView1, childView2, childView3]]));
|
||||
}
|
||||
|
||||
/**
|
||||
This test case models a following setup:
|
||||
|
||||
**********
|
||||
*C *
|
||||
* +-*-----------+
|
||||
* | * |
|
||||
* | * |
|
||||
* | * |
|
||||
* | * |
|
||||
* | * |
|
||||
* +----+ * |
|
||||
********** |
|
||||
| | |
|
||||
| | |
|
||||
| | +---+
|
||||
| | | |
|
||||
| | | |
|
||||
| |z3 | |
|
||||
| +---+---------+ |
|
||||
| | |
|
||||
| | |
|
||||
| | |
|
||||
| | |
|
||||
|z1 | |
|
||||
+--------+ |
|
||||
| |
|
||||
| |
|
||||
|z2 |
|
||||
+-------------+
|
||||
*/
|
||||
- (void)testZIndexOrderingIsPreservedAfterRetogglingClippingCase2
|
||||
{
|
||||
RCTView *clippingView = [RCTView new];
|
||||
[clippingView reactSetFrame:CGRectMake(0, 0, 100, 100)];
|
||||
|
||||
RCTView *childView1 = [RCTView new];
|
||||
[childView1 reactSetFrame:CGRectMake(25, 75, 150, 150)];
|
||||
[childView1 setReactZIndex:1];
|
||||
[clippingView insertReactSubview:childView1 atIndex:0];
|
||||
|
||||
RCTView *childView2 = [RCTView new];
|
||||
[childView2 reactSetFrame:CGRectMake(125, 125, 150, 150)];
|
||||
[childView2 setReactZIndex:2];
|
||||
[clippingView insertReactSubview:childView2 atIndex:0];
|
||||
|
||||
RCTView *childView3 = [RCTView new];
|
||||
[childView3 reactSetFrame:CGRectMake(75, 25, 150, 150)];
|
||||
[childView3 setReactZIndex:3];
|
||||
[clippingView insertReactSubview:childView3 atIndex:0];
|
||||
[clippingView didUpdateReactSubviews];
|
||||
[clippingView clearSortedSubviews];
|
||||
|
||||
XCTAssert(([clippingView.subviews isEqualToArray:@[childView1, childView2, childView3]]));
|
||||
[clippingView rct_setRemovesClippedSubviews:YES];
|
||||
XCTAssert(([clippingView.subviews isEqualToArray:@[childView1, childView3]]));
|
||||
[clippingView rct_setRemovesClippedSubviews:NO];
|
||||
XCTAssert(([clippingView.subviews isEqualToArray:@[childView1, childView2, childView3]]));
|
||||
}
|
||||
|
||||
#pragma mark - recursive clipping tests
|
||||
|
||||
- (void)testNotDirectSubviewIsClipped
|
||||
{
|
||||
RCTView *clippingView = [RCTView new];
|
||||
[clippingView rct_setRemovesClippedSubviews:YES];
|
||||
[clippingView reactSetFrame:CGRectMake(0, 0, 50, 50)];
|
||||
|
||||
RCTView *directChildView = [RCTView new];
|
||||
[directChildView reactSetFrame:CGRectMake(0, 0, 50, 50)];
|
||||
[clippingView insertReactSubview:directChildView atIndex:0];
|
||||
[clippingView didUpdateReactSubviews];
|
||||
|
||||
RCTView *deeperChildView = [RCTView new];
|
||||
[deeperChildView reactSetFrame:CGRectMake(0, 100, 50, 50)];
|
||||
[directChildView insertReactSubview:deeperChildView atIndex:0];
|
||||
[directChildView didUpdateReactSubviews];
|
||||
|
||||
XCTAssertEqual(directChildView.subviews.count, 0u);
|
||||
}
|
||||
|
||||
/** There are three views, top two ones clip and the bottom one is outside of the top one's bounds and in side of the middle one. */
|
||||
- (void)testUpperClippingViewClips
|
||||
{
|
||||
RCTView *upperClippingView = [RCTView new];
|
||||
[upperClippingView rct_setRemovesClippedSubviews:YES];
|
||||
[upperClippingView reactSetFrame:CGRectMake(0, 0, 50, 50)];
|
||||
|
||||
RCTView *lowerClippingView = [RCTView new];
|
||||
[lowerClippingView reactSetFrame:CGRectMake(0, 0, 50, 100)];
|
||||
[lowerClippingView rct_setRemovesClippedSubviews:YES];
|
||||
[upperClippingView insertReactSubview:lowerClippingView atIndex:0];
|
||||
[upperClippingView didUpdateReactSubviews];
|
||||
|
||||
RCTView *viewToBeClipped = [RCTView new];
|
||||
[viewToBeClipped reactSetFrame:CGRectMake(0, 50, 50, 50)];
|
||||
[lowerClippingView insertReactSubview:viewToBeClipped atIndex:0];
|
||||
[lowerClippingView didUpdateReactSubviews];
|
||||
|
||||
XCTAssertEqual(upperClippingView.subviews.count, 1u);
|
||||
XCTAssertEqual(lowerClippingView.subviews.count, 0u);
|
||||
}
|
||||
|
||||
#pragma mark - ScrollView tests
|
||||
|
||||
- (void)testScrollViewClips
|
||||
{
|
||||
RCTScrollView *scrollView = [[RCTScrollView alloc] initWithEventDispatcher:[OCMockObject mockForClass:[RCTEventDispatcher class]]];
|
||||
[scrollView reactSetFrame:CGRectMake(0, 0, 320, 480)];
|
||||
[scrollView rct_setRemovesClippedSubviews:YES];
|
||||
RCTView *contentView = [RCTView new];
|
||||
[contentView rct_setRemovesClippedSubviews:YES];
|
||||
// Content view is big enough to fit all rows. It's an implementation detail of ScrollView.js.
|
||||
[contentView reactSetFrame:CGRectMake(0, 0, 320, 550)];
|
||||
|
||||
[scrollView insertReactSubview:contentView atIndex:0];
|
||||
[scrollView didUpdateReactSubviews];
|
||||
|
||||
RCTView *rowView1 = [RCTView new];
|
||||
[rowView1 reactSetFrame:CGRectMake(0, 0, 320, 50)];
|
||||
RCTView *rowView2 = [RCTView new];
|
||||
[rowView2 reactSetFrame:CGRectMake(0, 200, 320, 50)];
|
||||
RCTView *rowView3 = [RCTView new];
|
||||
[rowView3 reactSetFrame:CGRectMake(0, 500, 320, 50)];
|
||||
[contentView insertReactSubview:rowView1 atIndex:0];
|
||||
[contentView insertReactSubview:rowView2 atIndex:0];
|
||||
[contentView insertReactSubview:rowView3 atIndex:0];
|
||||
[contentView didUpdateReactSubviews];
|
||||
// This makes sure the direct subview of scrollView gets frame too (implementation detial).
|
||||
[scrollView layoutSubviews];
|
||||
|
||||
XCTAssert([[NSSet setWithArray:contentView.subviews] isEqualToSet:[NSSet setWithArray:(@[rowView1, rowView2])]]);
|
||||
}
|
||||
|
||||
- (void)testScrollViewClipsDuringScrolling
|
||||
{
|
||||
// Scrollview will try to emit events during scrolling, so we need to use a "nice" mock.
|
||||
RCTScrollView *scrollView = [[RCTScrollView alloc] initWithEventDispatcher:[OCMockObject niceMockForClass:[RCTEventDispatcher class]]];
|
||||
[scrollView reactSetFrame:CGRectMake(0, 0, 320, 480)];
|
||||
[scrollView rct_setRemovesClippedSubviews:YES];
|
||||
scrollView.reactTag = @2;
|
||||
RCTView *contentView = [RCTView new];
|
||||
[contentView rct_setRemovesClippedSubviews:YES];
|
||||
// Content view is big enough to fit all rows. It's an implementation detail of ScrollView.js.
|
||||
[contentView reactSetFrame:CGRectMake(0, 0, 320, 550)];
|
||||
|
||||
[scrollView insertReactSubview:contentView atIndex:0];
|
||||
[scrollView didUpdateReactSubviews];
|
||||
|
||||
RCTView *rowView1 = [RCTView new];
|
||||
[rowView1 reactSetFrame:CGRectMake(0, 0, 320, 50)];
|
||||
RCTView *rowView2 = [RCTView new];
|
||||
[rowView2 reactSetFrame:CGRectMake(0, 200, 320, 50)];
|
||||
RCTView *rowView3 = [RCTView new];
|
||||
[rowView3 reactSetFrame:CGRectMake(0, 500, 320, 50)];
|
||||
[contentView insertReactSubview:rowView1 atIndex:0];
|
||||
[contentView insertReactSubview:rowView2 atIndex:0];
|
||||
[contentView insertReactSubview:rowView3 atIndex:0];
|
||||
[contentView didUpdateReactSubviews];
|
||||
// This makes sure the direct subview of scrollView gets frame too (implementation detial).
|
||||
[scrollView layoutSubviews];
|
||||
|
||||
[scrollView scrollToOffset:CGPointMake(0, 100)];
|
||||
|
||||
XCTAssert([[NSSet setWithArray:contentView.subviews] isEqualToSet:[NSSet setWithArray:(@[rowView2, rowView3])]]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
In this test case the react view hiearchy is
|
||||
clippingView -> directReactChildView -> deeperChildView
|
||||
while the uiview hierarchy is
|
||||
clippingView -> nonReactChildView -> directReactChildView -> deeperChildView
|
||||
*/
|
||||
- (void)testClippingWhenReactHierarchyDoesntMatchUIHierarchy
|
||||
{
|
||||
RCTView *clippingView = [RCTView new];
|
||||
[clippingView reactSetFrame:CGRectMake(0, 0, 50, 50)];
|
||||
|
||||
RCTView *directReactChildView = [RCTView new];
|
||||
[directReactChildView reactSetFrame:CGRectMake(-50, 0, 100, 50)];
|
||||
[clippingView insertReactSubview:directReactChildView atIndex:0];
|
||||
[clippingView didUpdateReactSubviews];
|
||||
|
||||
RCTView *deeperChildView = [RCTView new];
|
||||
[deeperChildView reactSetFrame:CGRectMake(0, 0, 50, 50)];
|
||||
[directReactChildView insertReactSubview:deeperChildView atIndex:0];
|
||||
[directReactChildView didUpdateReactSubviews];
|
||||
|
||||
UIView *nonReactChildView = [UIView new];
|
||||
[nonReactChildView setFrame:CGRectMake(50, 0, 50, 50)];
|
||||
[clippingView addSubview:nonReactChildView];
|
||||
[nonReactChildView addSubview:directReactChildView];
|
||||
|
||||
[clippingView rct_setRemovesClippedSubviews:YES];
|
||||
[directReactChildView reactSetFrame:CGRectMake(-50, 0, 99, 50)];
|
||||
|
||||
XCTAssertEqual(clippingView.subviews.count, 1u);
|
||||
XCTAssertEqual(nonReactChildView.subviews.count, 1u);
|
||||
XCTAssertEqual(directReactChildView.subviews.count, 1u);
|
||||
}
|
||||
|
||||
@end
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
@interface RCTNavigator : UIView <RCTFrameUpdateObserver>
|
||||
|
||||
@property (nonatomic, strong) UIView *reactNavSuperviewLink;
|
||||
@property (nonatomic, assign) NSInteger requestedTopOfStack;
|
||||
@property (nonatomic, assign) BOOL interactivePopGestureEnabled;
|
||||
|
||||
|
|
|
@ -499,6 +499,17 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
|
|||
[_bridge.eventDispatcher sendFakeScrollEvent:self.reactTag];
|
||||
}
|
||||
|
||||
/**
|
||||
* Must be overridden because UIKit removes the view's superview when used
|
||||
* as a navigator - it's considered outside the view hierarchy.
|
||||
*/
|
||||
- (UIView *)reactSuperview
|
||||
{
|
||||
RCTAssert(!_bridge.isValid || self.superview != nil, @"put reactNavSuperviewLink back");
|
||||
UIView *superview = [super reactSuperview];
|
||||
return superview ?: self.reactNavSuperviewLink;
|
||||
}
|
||||
|
||||
- (void)reactBridgeDidFinishTransaction
|
||||
{
|
||||
// we can't hook up the VC hierarchy in 'init' because the subviews aren't
|
||||
|
|
|
@ -450,6 +450,11 @@ static inline BOOL isRectInvalid(CGRect rect) {
|
|||
RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame)
|
||||
RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
|
||||
|
||||
- (void)setRemoveClippedSubviews:(__unused BOOL)removeClippedSubviews
|
||||
{
|
||||
// Does nothing
|
||||
}
|
||||
|
||||
- (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex
|
||||
{
|
||||
[super insertReactSubview:view atIndex:atIndex];
|
||||
|
@ -481,19 +486,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
|
|||
|
||||
- (void)didUpdateReactSubviews
|
||||
{
|
||||
// Do nothing for subview management, since it's done by `insertReactSubview:atIndex:`
|
||||
|
||||
// Unfortunately we have to copy paste clipping related logic from superclass.
|
||||
// Ideally subview management and clipping wouldn't happen in a single place.
|
||||
if (self.rct_nextClippingView || self.rct_removesClippedSubviews) {
|
||||
UIView *rct_nextClippingViewForSubviews = self.rct_removesClippedSubviews ? self : self.rct_nextClippingView;
|
||||
[self rct_updateSubviewsWithNextClippingView:rct_nextClippingViewForSubviews];
|
||||
|
||||
CGRect clippingRect = [self rct_activeClippingRect];
|
||||
if (!CGRectIsNull(clippingRect)) {
|
||||
[self rct_clipSubviewsWithAncestralClipRect:clippingRect];
|
||||
}
|
||||
}
|
||||
// Do nothing, as subviews are managed by `insertReactSubview:atIndex:`
|
||||
}
|
||||
|
||||
- (BOOL)centerContent
|
||||
|
@ -536,10 +529,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
|
|||
RCTAssert([self.subviews lastObject] == _scrollView, @"our only subview should be a scrollview");
|
||||
|
||||
CGPoint originalOffset = _scrollView.contentOffset;
|
||||
if (!CGRectEqualToRect(_scrollView.frame, self.bounds)) {
|
||||
_scrollView.frame = self.bounds;
|
||||
[self updateClippedSubviews];
|
||||
}
|
||||
_scrollView.frame = self.bounds;
|
||||
_scrollView.contentOffset = originalOffset;
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
|
@ -549,10 +539,18 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
|
|||
refreshControl.frame = (CGRect){_scrollView.contentOffset, {_scrollView.frame.size.width, refreshControl.frame.size.height}};
|
||||
}
|
||||
#endif
|
||||
|
||||
[self updateClippedSubviews];
|
||||
}
|
||||
|
||||
- (void)updateClippedSubviews
|
||||
{
|
||||
// Find a suitable view to use for clipping
|
||||
UIView *clipView = [self react_findClipView];
|
||||
if (!clipView) {
|
||||
return;
|
||||
}
|
||||
|
||||
static const CGFloat leeway = 1.0;
|
||||
|
||||
const CGSize contentSize = _scrollView.contentSize;
|
||||
|
@ -567,7 +565,8 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
|
|||
(scrollsVertically && (bounds.size.height < leeway || fabs(_lastClippedToRect.origin.y - bounds.origin.y) >= leeway));
|
||||
|
||||
if (shouldClipAgain) {
|
||||
[self rct_reclip];
|
||||
const CGRect clipRect = CGRectInset(clipView.bounds, -leeway, -leeway);
|
||||
[self react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView];
|
||||
_lastClippedToRect = bounds;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,6 +48,22 @@
|
|||
*/
|
||||
@property (nonatomic, assign) NSInteger reactZIndex;
|
||||
|
||||
/**
|
||||
* This is an optimization used to improve performance
|
||||
* for large scrolling views with many subviews, such as a
|
||||
* list or table. If set to YES, any clipped subviews will
|
||||
* be removed from the view hierarchy whenever -updateClippedSubviews
|
||||
* is called. This would typically be triggered by a scroll event
|
||||
*/
|
||||
@property (nonatomic, assign) BOOL removeClippedSubviews;
|
||||
|
||||
/**
|
||||
* Hide subviews if they are outside the view bounds.
|
||||
* This is an optimisation used predominantly with RKScrollViews
|
||||
* but it is applied recursively to all subviews that have
|
||||
* removeClippedSubviews set to YES
|
||||
*/
|
||||
- (void)updateClippedSubviews;
|
||||
|
||||
/**
|
||||
* Border radii.
|
||||
|
|
|
@ -15,8 +15,69 @@
|
|||
#import "RCTLog.h"
|
||||
#import "RCTUtils.h"
|
||||
#import "UIView+React.h"
|
||||
#import "UIView+Private.h"
|
||||
|
||||
@implementation UIView (RCTViewUnmounting)
|
||||
|
||||
- (void)react_remountAllSubviews
|
||||
{
|
||||
// Normal views don't support unmounting, so all
|
||||
// this does is forward message to our subviews,
|
||||
// in case any of those do support it
|
||||
|
||||
for (UIView *subview in self.subviews) {
|
||||
[subview react_remountAllSubviews];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView
|
||||
{
|
||||
// Even though we don't support subview unmounting
|
||||
// we do support clipsToBounds, so if that's enabled
|
||||
// we'll update the clipping
|
||||
|
||||
if (self.clipsToBounds && self.subviews.count > 0) {
|
||||
clipRect = [clipView convertRect:clipRect toView:self];
|
||||
clipRect = CGRectIntersection(clipRect, self.bounds);
|
||||
clipView = self;
|
||||
}
|
||||
|
||||
// Normal views don't support unmounting, so all
|
||||
// this does is forward message to our subviews,
|
||||
// in case any of those do support it
|
||||
|
||||
for (UIView *subview in self.subviews) {
|
||||
[subview react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView];
|
||||
}
|
||||
}
|
||||
|
||||
- (UIView *)react_findClipView
|
||||
{
|
||||
UIView *testView = self;
|
||||
UIView *clipView = nil;
|
||||
CGRect clipRect = self.bounds;
|
||||
// We will only look for a clipping view up the view hierarchy until we hit the root view.
|
||||
while (testView) {
|
||||
if (testView.clipsToBounds) {
|
||||
if (clipView) {
|
||||
CGRect testRect = [clipView convertRect:clipRect toView:testView];
|
||||
if (!CGRectContainsRect(testView.bounds, testRect)) {
|
||||
clipView = testView;
|
||||
clipRect = CGRectIntersection(testView.bounds, testRect);
|
||||
}
|
||||
} else {
|
||||
clipView = testView;
|
||||
clipRect = [self convertRect:self.bounds toView:clipView];
|
||||
}
|
||||
}
|
||||
if ([testView isReactRootView]) {
|
||||
break;
|
||||
}
|
||||
testView = testView.superview;
|
||||
}
|
||||
return clipView ?: self.window;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
static NSString *RCTRecursiveAccessibilityLabel(UIView *view)
|
||||
{
|
||||
|
@ -154,22 +215,6 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:unused)
|
|||
}
|
||||
}
|
||||
|
||||
- (void)didUpdateReactSubviews
|
||||
{
|
||||
if (!self.rct_nextClippingView && !self.rct_removesClippedSubviews) {
|
||||
[super didUpdateReactSubviews];
|
||||
return;
|
||||
}
|
||||
|
||||
UIView *rct_nextClippingViewForSubviews = self.rct_removesClippedSubviews ? self : self.rct_nextClippingView;
|
||||
[self rct_updateSubviewsWithNextClippingView:rct_nextClippingViewForSubviews];
|
||||
|
||||
CGRect clippingRect = [self rct_activeClippingRect];
|
||||
if (!CGRectIsNull(clippingRect)) {
|
||||
[self rct_clipSubviewsWithAncestralClipRect:clippingRect];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)description
|
||||
{
|
||||
NSString *superDescription = super.description;
|
||||
|
@ -226,6 +271,113 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:unused)
|
|||
return UIEdgeInsetsZero;
|
||||
}
|
||||
|
||||
#pragma mark - View unmounting
|
||||
|
||||
- (void)react_remountAllSubviews
|
||||
{
|
||||
if (_removeClippedSubviews) {
|
||||
for (UIView *view in self.sortedReactSubviews) {
|
||||
if (view.superview != self) {
|
||||
[self addSubview:view];
|
||||
[view react_remountAllSubviews];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If _removeClippedSubviews is false, we must already be showing all subviews
|
||||
[super react_remountAllSubviews];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView
|
||||
{
|
||||
// TODO (#5906496): for scrollviews (the primary use-case) we could
|
||||
// optimize this by only doing a range check along the scroll axis,
|
||||
// instead of comparing the whole frame
|
||||
|
||||
if (!_removeClippedSubviews) {
|
||||
// Use default behavior if unmounting is disabled
|
||||
return [super react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView];
|
||||
}
|
||||
|
||||
if (self.reactSubviews.count == 0) {
|
||||
// Do nothing if we have no subviews
|
||||
return;
|
||||
}
|
||||
|
||||
if (CGSizeEqualToSize(self.bounds.size, CGSizeZero)) {
|
||||
// Do nothing if layout hasn't happened yet
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert clipping rect to local coordinates
|
||||
clipRect = [clipView convertRect:clipRect toView:self];
|
||||
clipRect = CGRectIntersection(clipRect, self.bounds);
|
||||
clipView = self;
|
||||
|
||||
// Mount / unmount views
|
||||
for (UIView *view in self.sortedReactSubviews) {
|
||||
if (!CGRectIsEmpty(CGRectIntersection(clipRect, view.frame))) {
|
||||
|
||||
// View is at least partially visible, so remount it if unmounted
|
||||
[self addSubview:view];
|
||||
|
||||
// Then test its subviews
|
||||
if (CGRectContainsRect(clipRect, view.frame)) {
|
||||
// View is fully visible, so remount all subviews
|
||||
[view react_remountAllSubviews];
|
||||
} else {
|
||||
// View is partially visible, so update clipped subviews
|
||||
[view react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView];
|
||||
}
|
||||
|
||||
} else if (view.superview) {
|
||||
|
||||
// View is completely outside the clipRect, so unmount it
|
||||
[view removeFromSuperview];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setRemoveClippedSubviews:(BOOL)removeClippedSubviews
|
||||
{
|
||||
if (!removeClippedSubviews && _removeClippedSubviews) {
|
||||
[self react_remountAllSubviews];
|
||||
}
|
||||
_removeClippedSubviews = removeClippedSubviews;
|
||||
}
|
||||
|
||||
- (void)didUpdateReactSubviews
|
||||
{
|
||||
if (_removeClippedSubviews) {
|
||||
[self updateClippedSubviews];
|
||||
} else {
|
||||
[super didUpdateReactSubviews];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updateClippedSubviews
|
||||
{
|
||||
// Find a suitable view to use for clipping
|
||||
UIView *clipView = [self react_findClipView];
|
||||
if (clipView) {
|
||||
[self react_updateClippedSubviewsWithClipRect:clipView.bounds relativeToView:clipView];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
// TODO (#5906496): this a nasty performance drain, but necessary
|
||||
// to prevent gaps appearing when the loading spinner disappears.
|
||||
// We might be able to fix this another way by triggering a call
|
||||
// to updateClippedSubviews manually after loading
|
||||
|
||||
[super layoutSubviews];
|
||||
|
||||
if (_removeClippedSubviews) {
|
||||
[self updateClippedSubviews];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Borders
|
||||
|
||||
- (UIColor *)backgroundColor
|
||||
|
@ -297,9 +449,6 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:unused)
|
|||
// TODO: detect up-front if re-rendering is necessary
|
||||
CGSize oldSize = self.bounds.size;
|
||||
[super reactSetFrame:frame];
|
||||
// When the frame changes, our view needs to reclip itself with its parent,
|
||||
// and also clip any of its own subviews if `rct_removesClippedSubviews` is turned on.
|
||||
[self rct_reclip];
|
||||
if (!CGSizeEqualToSize(self.bounds.size, oldSize)) {
|
||||
[self.layer setNeedsDisplay];
|
||||
}
|
||||
|
|
|
@ -101,10 +101,6 @@ RCT_EXPORT_MODULE()
|
|||
RCT_EXPORT_VIEW_PROPERTY(accessibilityLabel, NSString)
|
||||
RCT_EXPORT_VIEW_PROPERTY(accessibilityTraits, UIAccessibilityTraits)
|
||||
RCT_EXPORT_VIEW_PROPERTY(backgroundColor, UIColor)
|
||||
RCT_CUSTOM_VIEW_PROPERTY(removeClippedSubviews, BOOL, UIView)
|
||||
{
|
||||
view.rct_removesClippedSubviews = [RCTConvert BOOL:json] ;
|
||||
}
|
||||
RCT_REMAP_VIEW_PROPERTY(accessible, isAccessibilityElement, BOOL)
|
||||
RCT_REMAP_VIEW_PROPERTY(testID, accessibilityIdentifier, NSString)
|
||||
RCT_REMAP_VIEW_PROPERTY(backfaceVisibility, layer.doubleSided, css_backface_visibility_t)
|
||||
|
@ -113,7 +109,7 @@ RCT_REMAP_VIEW_PROPERTY(shadowColor, layer.shadowColor, CGColor)
|
|||
RCT_REMAP_VIEW_PROPERTY(shadowOffset, layer.shadowOffset, CGSize)
|
||||
RCT_REMAP_VIEW_PROPERTY(shadowOpacity, layer.shadowOpacity, float)
|
||||
RCT_REMAP_VIEW_PROPERTY(shadowRadius, layer.shadowRadius, CGFloat)
|
||||
RCT_CUSTOM_VIEW_PROPERTY(overflow, CSSOverflow, UIView)
|
||||
RCT_CUSTOM_VIEW_PROPERTY(overflow, CSSOverflow, RCTView)
|
||||
{
|
||||
if (json) {
|
||||
view.clipsToBounds = [RCTConvert CSSOverflow:json] != CSSOverflowVisible;
|
||||
|
@ -121,25 +117,23 @@ RCT_CUSTOM_VIEW_PROPERTY(overflow, CSSOverflow, UIView)
|
|||
view.clipsToBounds = defaultView.clipsToBounds;
|
||||
}
|
||||
}
|
||||
RCT_CUSTOM_VIEW_PROPERTY(shouldRasterizeIOS, BOOL, UIView)
|
||||
RCT_CUSTOM_VIEW_PROPERTY(shouldRasterizeIOS, BOOL, RCTView)
|
||||
{
|
||||
view.layer.shouldRasterize = json ? [RCTConvert BOOL:json] : defaultView.layer.shouldRasterize;
|
||||
view.layer.rasterizationScale = view.layer.shouldRasterize ? [UIScreen mainScreen].scale : defaultView.layer.rasterizationScale;
|
||||
}
|
||||
// TODO: t11041683 Remove this duplicate property name.
|
||||
RCT_CUSTOM_VIEW_PROPERTY(transformMatrix, CATransform3D, UIView)
|
||||
RCT_CUSTOM_VIEW_PROPERTY(transformMatrix, CATransform3D, RCTView)
|
||||
{
|
||||
view.layer.transform = json ? [RCTConvert CATransform3D:json] : defaultView.layer.transform;
|
||||
// TODO: Improve this by enabling edge antialiasing only for transforms with rotation or skewing
|
||||
view.layer.allowsEdgeAntialiasing = !CATransform3DIsIdentity(view.layer.transform);
|
||||
[view rct_reclip];
|
||||
}
|
||||
RCT_CUSTOM_VIEW_PROPERTY(transform, CATransform3D, UIView)
|
||||
RCT_CUSTOM_VIEW_PROPERTY(transform, CATransform3D, RCTView)
|
||||
{
|
||||
view.layer.transform = json ? [RCTConvert CATransform3D:json] : defaultView.layer.transform;
|
||||
// TODO: Improve this by enabling edge antialiasing only for transforms with rotation or skewing
|
||||
view.layer.allowsEdgeAntialiasing = !CATransform3DIsIdentity(view.layer.transform);
|
||||
[view rct_reclip];
|
||||
}
|
||||
RCT_CUSTOM_VIEW_PROPERTY(pointerEvents, RCTPointerEvents, RCTView)
|
||||
{
|
||||
|
@ -168,6 +162,12 @@ RCT_CUSTOM_VIEW_PROPERTY(pointerEvents, RCTPointerEvents, RCTView)
|
|||
RCTLogError(@"UIView base class does not support pointerEvent value: %@", json);
|
||||
}
|
||||
}
|
||||
RCT_CUSTOM_VIEW_PROPERTY(removeClippedSubviews, BOOL, RCTView)
|
||||
{
|
||||
if ([view respondsToSelector:@selector(setRemoveClippedSubviews:)]) {
|
||||
view.removeClippedSubviews = json ? [RCTConvert BOOL:json] : defaultView.removeClippedSubviews;
|
||||
}
|
||||
}
|
||||
RCT_CUSTOM_VIEW_PROPERTY(borderRadius, CGFloat, RCTView) {
|
||||
if ([view respondsToSelector:@selector(setBorderRadius:)]) {
|
||||
view.borderRadius = json ? [RCTConvert CGFloat:json] : defaultView.borderRadius;
|
||||
|
|
|
@ -11,12 +11,12 @@
|
|||
|
||||
@interface UIView (Private)
|
||||
|
||||
// remove clipped subviews implementation
|
||||
- (void)react_remountAllSubviews;
|
||||
- (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView;
|
||||
- (UIView *)react_findClipView;
|
||||
|
||||
// zIndex sorting
|
||||
- (void)clearSortedSubviews;
|
||||
|
||||
- (CGRect)rct_activeClippingRect;
|
||||
- (UIView *)rct_nextClippingView;
|
||||
- (void)rct_updateSubviewsWithNextClippingView:(UIView *)clippingView;
|
||||
- (void)rct_clipSubviewsWithAncestralClipRect:(CGRect)clipRect;
|
||||
|
||||
@end
|
||||
|
|
|
@ -81,19 +81,4 @@
|
|||
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Having views in view hierarchy that are not visible wastes resources.
|
||||
* That's why we have implemented view clipping. The key idea is simple:
|
||||
* When a view has clipping turned on, its subview is removed as long as it is outside of the view's bounds.
|
||||
*
|
||||
* Few clarifications:
|
||||
* 1/ All subviews are affected, not just the direct ones.
|
||||
* 2/ If there are multiple ancestors with a view clipping turned on then intersection of their bounds will be used for clipping.
|
||||
* 3/ All UIViews are affected, not only RCTViews. Alhough this behavior is never triggered outside of React Native.
|
||||
* 4/ Position in a UIWindow is not used for cliping.
|
||||
*/
|
||||
@property (nonatomic, assign, setter=rct_setRemovesClippedSubviews:) BOOL rct_removesClippedSubviews;
|
||||
/** Recomputes clipping for a view and its subviews. You should call this if you move views manually in your view manager. */
|
||||
- (void)rct_reclip;
|
||||
|
||||
@end
|
||||
|
|
|
@ -15,13 +15,6 @@
|
|||
#import "RCTLog.h"
|
||||
#import "RCTShadowView.h"
|
||||
|
||||
@interface RCTWeakObjectContainer : NSObject
|
||||
@property (nonatomic, weak) id object;
|
||||
@end
|
||||
|
||||
@implementation RCTWeakObjectContainer
|
||||
@end
|
||||
|
||||
@implementation UIView (React)
|
||||
|
||||
- (NSNumber *)reactTag
|
||||
|
@ -34,18 +27,6 @@
|
|||
objc_setAssociatedObject(self, @selector(reactTag), reactTag, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
}
|
||||
|
||||
- (UIView *)reactSuperview
|
||||
{
|
||||
return [(RCTWeakObjectContainer *)objc_getAssociatedObject(self, @selector(reactSuperview)) object];
|
||||
}
|
||||
|
||||
- (void)setReactSuperview:(UIView *)reactSuperview
|
||||
{
|
||||
RCTWeakObjectContainer *wrapper = [RCTWeakObjectContainer new];
|
||||
wrapper.object = reactSuperview;
|
||||
objc_setAssociatedObject(self, @selector(reactSuperview), wrapper, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
}
|
||||
|
||||
#if RCT_DEV
|
||||
|
||||
- (RCTShadowView *)_DEBUG_reactShadowView
|
||||
|
@ -80,6 +61,11 @@
|
|||
return objc_getAssociatedObject(self, _cmd);
|
||||
}
|
||||
|
||||
- (UIView *)reactSuperview
|
||||
{
|
||||
return self.superview;
|
||||
}
|
||||
|
||||
- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex
|
||||
{
|
||||
// We access the associated object directly here in case someone overrides
|
||||
|
@ -90,7 +76,6 @@
|
|||
objc_setAssociatedObject(self, @selector(reactSubviews), subviews, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
}
|
||||
[subviews insertObject:subview atIndex:atIndex];
|
||||
[subview setReactSuperview:self];
|
||||
}
|
||||
|
||||
- (void)removeReactSubview:(UIView *)subview
|
||||
|
@ -100,7 +85,6 @@
|
|||
NSMutableArray *subviews = objc_getAssociatedObject(self, @selector(reactSubviews));
|
||||
[subviews removeObject:subview];
|
||||
[subview removeFromSuperview];
|
||||
[subview setReactSuperview:nil];
|
||||
}
|
||||
|
||||
- (NSInteger)reactZIndex
|
||||
|
@ -216,191 +200,4 @@
|
|||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark - view clipping
|
||||
|
||||
/**
|
||||
* How does view clipping works?
|
||||
*
|
||||
* Each view knows if it has clipping turned on and its closest ancestor that has clipping turned on (if any). That helps with effective clipping evaluation.
|
||||
|
||||
* There are four standard cases when we have to evaluate view clipping:
|
||||
* 1. a view has clipping turned off:
|
||||
* - we have to update NCV for its complete subtree
|
||||
* - we have to add back all clipped views
|
||||
* 2. a view has clipping turned on:
|
||||
* - we have to update NCV for its complete subtree
|
||||
* - we have to reclip it
|
||||
* 3. a react subview is added:
|
||||
* - we have to set it and all its subviews NCV
|
||||
* - if it has NVC or clipping turned on we have to reclip it
|
||||
* 4. a view is moved (new frame, tranformation, is a cell in a scrolling scrollview):
|
||||
* - if it has NCV or clipping turned on we have to reclip it
|
||||
*/
|
||||
|
||||
- (BOOL)rct_removesClippedSubviews
|
||||
{
|
||||
return [objc_getAssociatedObject(self, @selector(rct_removesClippedSubviews)) boolValue];
|
||||
}
|
||||
|
||||
- (void)rct_setRemovesClippedSubviews:(BOOL)removeClippedSubviews
|
||||
{
|
||||
objc_setAssociatedObject(self, @selector(rct_removesClippedSubviews), @(removeClippedSubviews), OBJC_ASSOCIATION_ASSIGN);
|
||||
[self rct_updateSubviewsWithNextClippingView:removeClippedSubviews ? self : nil];
|
||||
if (removeClippedSubviews) {
|
||||
[self rct_reclip];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a closest ancestor view which has view clipping turned on.
|
||||
* `nil` is returned if there is no such view.
|
||||
*/
|
||||
- (UIView *)rct_nextClippingView
|
||||
{
|
||||
return [(RCTWeakObjectContainer *)objc_getAssociatedObject(self, @selector(rct_nextClippingView)) object];
|
||||
}
|
||||
|
||||
- (void)rct_setNextClippingView:(UIView *)rct_nextClippingView
|
||||
{
|
||||
RCTAssert(self != rct_nextClippingView, @"A view cannot be next clipping view for itself.");
|
||||
RCTWeakObjectContainer *wrapper = [RCTWeakObjectContainer new];
|
||||
wrapper.object = rct_nextClippingView;
|
||||
objc_setAssociatedObject(self, @selector(rct_nextClippingView), wrapper, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reevaluates clipping for itself and recursively for its subviews,
|
||||
* going as deep as the first clipped subview is.
|
||||
*
|
||||
* It works like this:
|
||||
* 1/ Is any of our ancestores already clipped? If yes lets do nothing.
|
||||
* 2/ Get clipping rect that applies here.
|
||||
* 3/ Does our bounds intersect with the rect? If no clip ourself and we are done.
|
||||
* 4/ If there is an intersection make sure we are not clipped and recurse into subviews.
|
||||
*
|
||||
* We do 1/ and 2/ in one step by retrieving "active clip rect" (see method `activeClipRect`).
|
||||
*/
|
||||
- (void)rct_reclip
|
||||
{
|
||||
// If we are not clipping or have a view that clips us there is nothing to do.
|
||||
if (!self.rct_nextClippingView && !self.rct_removesClippedSubviews) {
|
||||
return;
|
||||
}
|
||||
// If we are currently clipped our active clipping rect would be null rect. That's why we ask for our superview's.
|
||||
// Actually it's not that simple. Clipping logic operates on uiview hierarchy. If the current view is clipped we cannot use its superview, since its nil.
|
||||
// Fortunately we can use `reactSuperview`. UI and React view hierachies doesn't have to match, but when they don't match we don't clip.
|
||||
// Therefore because this view is clipped it means its reactSuperview is the same as its (clipped) UI superview.
|
||||
UIView *clippingRectSource = self.superview ? self.superview : self.reactSuperview;
|
||||
CGRect clippingRectForSuperview = clippingRectSource ? [clippingRectSource rct_activeClippingRect] : CGRectInfinite;
|
||||
if (CGRectIsNull(clippingRectForSuperview)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!CGRectIntersectsRect(self.frame, clippingRectForSuperview)) {
|
||||
// we are clipped
|
||||
clipView(self);
|
||||
} else {
|
||||
// we are not clipped
|
||||
if (!self.superview) {
|
||||
// We need to make sure we keep zIndex ordering when adding back a clipped view.
|
||||
NSUInteger position = 0;
|
||||
for (UIView *view in self.reactSuperview.sortedReactSubviews) {
|
||||
if (view.superview) {
|
||||
position += 1;
|
||||
}
|
||||
if (view == self) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
[self.reactSuperview insertSubview:self atIndex:position];
|
||||
}
|
||||
// Potential optimisation: We don't have to reevaluate clipping for subviews if the whole view was visible before and is still visible now.
|
||||
CGRect clipRect = [self convertRect:clippingRectForSuperview fromView:self.superview];
|
||||
[self rct_clipSubviewsWithAncestralClipRect:clipRect];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is not the same as `reclip`, since we reevaluate clipping for all subviews at once.
|
||||
* It enables us to insert the not clipped ones into right position effectively.
|
||||
*/
|
||||
- (void)rct_clipSubviewsWithAncestralClipRect:(CGRect)clipRect
|
||||
{
|
||||
UIView *lastSubview = nil;
|
||||
if (self.rct_removesClippedSubviews) {
|
||||
clipRect = CGRectIntersection(clipRect, self.bounds);
|
||||
}
|
||||
for (UIView *subview in self.sortedReactSubviews) {
|
||||
if (CGRectIntersectsRect(subview.frame, clipRect)) {
|
||||
if (!subview.superview) {
|
||||
if (lastSubview) {
|
||||
[self insertSubview:subview aboveSubview:lastSubview];
|
||||
} else {
|
||||
[self insertSubview:subview atIndex:0];
|
||||
}
|
||||
}
|
||||
lastSubview = subview;
|
||||
[subview rct_clipSubviewsWithAncestralClipRect:[self convertRect:clipRect toView:subview]];
|
||||
} else {
|
||||
clipView(subview);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void clipView(UIView *view)
|
||||
{
|
||||
// we are clipped
|
||||
if (view.superview) {
|
||||
// We don't clip if react hierarchy doesn't match uiview hierarchy, since we could get into inconsistent state.
|
||||
if (view.reactSuperview == view.superview) {
|
||||
[view removeFromSuperview];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If this view is not clipped:
|
||||
* Returns a rect that is used to clip this view, in the view's coordinate space.
|
||||
* If this view has clipping turned on it's bounds are accounted for in the returned clipping rect.
|
||||
*
|
||||
* Returns CGRectNull if this view is clipped or none of its ancestors has clipping turned on.
|
||||
*/
|
||||
- (CGRect)rct_activeClippingRect
|
||||
{
|
||||
UIView *clippingParent = self.rct_nextClippingView;
|
||||
CGRect resultRect = CGRectInfinite;
|
||||
|
||||
if (clippingParent) {
|
||||
if (![self isDescendantOfView:clippingParent]) {
|
||||
return CGRectNull;
|
||||
}
|
||||
resultRect = [self convertRect:[clippingParent rct_activeClippingRect] fromView:clippingParent];
|
||||
}
|
||||
|
||||
if (self.rct_removesClippedSubviews) {
|
||||
resultRect = CGRectIntersection(resultRect, self.bounds);
|
||||
}
|
||||
|
||||
return resultRect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the next clipping view for all subviews if they are not already being clipped, recursively.
|
||||
* Using a `nil` clipping view will result in adding clipped subviews back.
|
||||
*/
|
||||
- (void)rct_updateSubviewsWithNextClippingView:(UIView *)clippingView
|
||||
{
|
||||
for (UIView *subview in self.sortedReactSubviews) {
|
||||
if (!clippingView) {
|
||||
[self addSubview:subview];
|
||||
}
|
||||
[subview rct_setNextClippingView:clippingView];
|
||||
// We don't have to recurse if the subview either clips itself or it already has correct next clipping view set.
|
||||
if (!subview.rct_removesClippedSubviews && !(subview.rct_nextClippingView == clippingView)) {
|
||||
[subview rct_updateSubviewsWithNextClippingView:clippingView];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
Loading…
Reference in New Issue