Add suggested ‘view size’ powers to -[RCTShadowView setFrame:]

Summary:The UICollectionView example is actually my use-case, which is discussed in a
bit more detail [here](https://github.com/alloy/ReactNativeExperiments/issues/2).

----

This is useful when wrapping native iOS components that determine their
own suggested size and which would be too hard/unnecessary to replicate
in the shadow view. For instance a `UICollectionView` that after layout
will update its `contentSize`, which could be used to suggest a size to
the shadow view.

The reason for adding it to -[RCTShadowView setFrame:] is mainly so it
can be used via the existing -[RCTUIManager setFrame:forView:] API and
because it might not be a feature you want to expose too prominently.

An origin of `{ NAN, NAN }` is used as a sentinel to indicate that the
frame should be used as a size suggestion. The size portion of the rect
may contain a `NAN` to skip that dimension or a suggested value for the
dimension which will be used if no explicit styling has been assigned.

Examples:

* Without any expl
Closes https://github.com/facebook/react-native/pull/6114

Differential Revision: D2994796

Pulled By: nicklockwood

fb-gh-sync-id: 6dd3dd86a352ca7d31a0da38bc38a2859ed0a410
shipit-source-id: 6dd3dd86a352ca7d31a0da38bc38a2859ed0a410
This commit is contained in:
Eloy Durán 2016-03-01 10:13:22 -08:00 committed by Facebook Github Bot 5
parent b4dc5e3e81
commit 6bae7f93f5
6 changed files with 140 additions and 31 deletions

View File

@ -17,11 +17,23 @@
#import "RCTShadowView.h" #import "RCTShadowView.h"
@interface RCTShadowViewTests : XCTestCase @interface RCTShadowViewTests : XCTestCase
@property (nonatomic, strong) RCTShadowView *parentView;
@end @end
@implementation RCTShadowViewTests @implementation RCTShadowViewTests
- (void)setUp
{
[super setUp];
self.parentView = [self _shadowViewWithStyle:^(css_style_t *style) {
style->flex_direction = CSS_FLEX_DIRECTION_COLUMN;
style->dimensions[0] = 440;
style->dimensions[1] = 440;
}];
self.parentView.reactTag = @1; // must be valid rootView tag
}
// Just a basic sanity test to ensure css-layout is applied correctly in the context of our shadow view hierarchy. // Just a basic sanity test to ensure css-layout is applied correctly in the context of our shadow view hierarchy.
// //
// ==================================== // ====================================
@ -69,33 +81,87 @@
style->flex = 1; style->flex = 1;
}]; }];
RCTShadowView *parentView = [self _shadowViewWithStyle:^(css_style_t *style) { self.parentView.cssNode->style.padding[0] = 10;
style->flex_direction = CSS_FLEX_DIRECTION_COLUMN; self.parentView.cssNode->style.padding[1] = 10;
style->padding[0] = 10; self.parentView.cssNode->style.padding[2] = 10;
style->padding[1] = 10; self.parentView.cssNode->style.padding[3] = 10;
style->padding[2] = 10;
style->padding[3] = 10;
style->dimensions[0] = 440;
style->dimensions[1] = 440;
}];
[parentView insertReactSubview:headerView atIndex:0]; [self.parentView insertReactSubview:headerView atIndex:0];
[parentView insertReactSubview:mainView atIndex:1]; [self.parentView insertReactSubview:mainView atIndex:1];
[parentView insertReactSubview:footerView atIndex:2]; [self.parentView insertReactSubview:footerView atIndex:2];
parentView.reactTag = @1; // must be valid rootView tag [self.parentView collectRootUpdatedFrames];
[parentView collectRootUpdatedFrames];
XCTAssertTrue(CGRectEqualToRect([parentView measureLayoutRelativeToAncestor:parentView], CGRectMake(0, 0, 440, 440))); XCTAssertTrue(CGRectEqualToRect([self.parentView measureLayoutRelativeToAncestor:self.parentView], CGRectMake(0, 0, 440, 440)));
XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets([parentView paddingAsInsets], UIEdgeInsetsMake(10, 10, 10, 10))); XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets([self.parentView paddingAsInsets], UIEdgeInsetsMake(10, 10, 10, 10)));
XCTAssertTrue(CGRectEqualToRect([headerView measureLayoutRelativeToAncestor:parentView], CGRectMake(10, 10, 420, 100))); XCTAssertTrue(CGRectEqualToRect([headerView measureLayoutRelativeToAncestor:self.parentView], CGRectMake(10, 10, 420, 100)));
XCTAssertTrue(CGRectEqualToRect([mainView measureLayoutRelativeToAncestor:parentView], CGRectMake(10, 120, 420, 200))); XCTAssertTrue(CGRectEqualToRect([mainView measureLayoutRelativeToAncestor:self.parentView], CGRectMake(10, 120, 420, 200)));
XCTAssertTrue(CGRectEqualToRect([footerView measureLayoutRelativeToAncestor:parentView], CGRectMake(10, 330, 420, 100))); XCTAssertTrue(CGRectEqualToRect([footerView measureLayoutRelativeToAncestor:self.parentView], CGRectMake(10, 330, 420, 100)));
XCTAssertTrue(CGRectEqualToRect([leftView measureLayoutRelativeToAncestor:parentView], CGRectMake(10, 120, 100, 200))); XCTAssertTrue(CGRectEqualToRect([leftView measureLayoutRelativeToAncestor:self.parentView], CGRectMake(10, 120, 100, 200)));
XCTAssertTrue(CGRectEqualToRect([centerView measureLayoutRelativeToAncestor:parentView], CGRectMake(120, 120, 200, 200))); XCTAssertTrue(CGRectEqualToRect([centerView measureLayoutRelativeToAncestor:self.parentView], CGRectMake(120, 120, 200, 200)));
XCTAssertTrue(CGRectEqualToRect([rightView measureLayoutRelativeToAncestor:parentView], CGRectMake(330, 120, 100, 200))); XCTAssertTrue(CGRectEqualToRect([rightView measureLayoutRelativeToAncestor:self.parentView], CGRectMake(330, 120, 100, 200)));
}
- (void)testAssignsSuggestedWidthDimension
{
[self _withShadowViewWithStyle:^(css_style_t *style) {
style->position[CSS_LEFT] = 0;
style->position[CSS_TOP] = 0;
style->dimensions[CSS_HEIGHT] = 10;
}
assertRelativeLayout:CGRectMake(0, 0, 3, 10)
withIntrinsicContentSize:CGSizeMake(3, UIViewNoIntrinsicMetric)];
}
- (void)testAssignsSuggestedHeightDimension
{
[self _withShadowViewWithStyle:^(css_style_t *style) {
style->position[CSS_LEFT] = 0;
style->position[CSS_TOP] = 0;
style->dimensions[CSS_WIDTH] = 10;
}
assertRelativeLayout:CGRectMake(0, 0, 10, 4)
withIntrinsicContentSize:CGSizeMake(UIViewNoIntrinsicMetric, 4)];
}
- (void)testDoesNotOverrideDimensionStyleWithSuggestedDimensions
{
[self _withShadowViewWithStyle:^(css_style_t *style) {
style->position[CSS_LEFT] = 0;
style->position[CSS_TOP] = 0;
style->dimensions[CSS_WIDTH] = 10;
style->dimensions[CSS_HEIGHT] = 10;
}
assertRelativeLayout:CGRectMake(0, 0, 10, 10)
withIntrinsicContentSize:CGSizeMake(3, 4)];
}
- (void)testDoesNotAssignSuggestedDimensionsWhenStyledWithFlexAttribute
{
float parentWidth = self.parentView.cssNode->style.dimensions[CSS_WIDTH];
float parentHeight = self.parentView.cssNode->style.dimensions[CSS_HEIGHT];
[self _withShadowViewWithStyle:^(css_style_t *style) {
style->flex = 1;
}
assertRelativeLayout:CGRectMake(0, 0, parentWidth, parentHeight)
withIntrinsicContentSize:CGSizeMake(3, 4)];
}
- (void)_withShadowViewWithStyle:(void(^)(css_style_t *style))styleBlock
assertRelativeLayout:(CGRect)expectedRect
withIntrinsicContentSize:(CGSize)contentSize
{
RCTShadowView *view = [self _shadowViewWithStyle:styleBlock];
[self.parentView insertReactSubview:view atIndex:0];
view.intrinsicContentSize = contentSize;
[self.parentView collectRootUpdatedFrames];
CGRect actualRect = [view measureLayoutRelativeToAncestor:self.parentView];
XCTAssertTrue(CGRectEqualToRect(expectedRect, actualRect),
@"Expected layout to be %@, got %@",
NSStringFromCGRect(expectedRect),
NSStringFromCGRect(actualRect));
} }
- (RCTShadowView *)_shadowViewWithStyle:(void(^)(css_style_t *style))styleBlock - (RCTShadowView *)_shadowViewWithStyle:(void(^)(css_style_t *style))styleBlock

View File

@ -60,6 +60,12 @@ RCT_EXTERN NSString *const RCTUIManagerRootViewKey;
*/ */
- (void)setFrame:(CGRect)frame forView:(UIView *)view; - (void)setFrame:(CGRect)frame forView:(UIView *)view;
/**
* Set the natural size of a view, which is used when no explicit size is set.
* Use UIViewNoIntrinsicMetric to ignore a dimension.
*/
- (void)setIntrinsicContentSize:(CGSize)size forView:(UIView *)view;
/** /**
* Update the background color of a root view. This is usually triggered by * Update the background color of a root view. This is usually triggered by
* manually setting the background color of the root view with native code. * manually setting the background color of the root view with native code.

View File

@ -399,6 +399,21 @@ extern NSString *RCTBridgeModuleNameForClass(Class cls);
}); });
} }
- (void)setIntrinsicContentSize:(CGSize)size forView:(UIView *)view
{
RCTAssertMainThread();
NSNumber *reactTag = view.reactTag;
dispatch_async(_shadowQueue, ^{
RCTShadowView *shadowView = _shadowViewRegistry[reactTag];
RCTAssert(shadowView != nil, @"Could not locate root view with tag #%@", reactTag);
shadowView.intrinsicContentSize = size;
[self batchDidComplete];
});
}
- (void)setBackgroundColor:(UIColor *)color forRootView:(UIView *)rootView - (void)setBackgroundColor:(UIColor *)color forRootView:(UIView *)rootView
{ {
RCTAssertMainThread(); RCTAssertMainThread();

View File

@ -68,6 +68,12 @@ typedef void (^RCTApplierBlock)(NSDictionary<NSNumber *, UIView *> *viewRegistry
- (void)setTopLeft:(CGPoint)topLeft; - (void)setTopLeft:(CGPoint)topLeft;
- (void)setSize:(CGSize)size; - (void)setSize:(CGSize)size;
/**
* Set the natural size of the view, which is used when no explicit size is set.
* Use UIViewNoIntrinsicMetric to ignore a dimension.
*/
- (void)setIntrinsicContentSize:(CGSize)size;
/** /**
* Size flexibility type used to find size constraints. * Size flexibility type used to find size constraints.
* Default to RCTRootViewSizeFlexibilityNone * Default to RCTRootViewSizeFlexibilityNone

View File

@ -535,6 +535,29 @@ RCT_POSITION_PROPERTY(Left, left, LEFT)
[self dirtyLayout]; [self dirtyLayout];
} }
static inline BOOL
RCTAssignSuggestedDimension(css_node_t *css_node, int dimension, CGFloat amount)
{
if (amount != UIViewNoIntrinsicMetric
&& isnan(css_node->style.dimensions[dimension])) {
css_node->style.dimensions[dimension] = amount;
return YES;
}
return NO;
}
- (void)setIntrinsicContentSize:(CGSize)size
{
if (_cssNode->style.flex == 0) {
BOOL dirty = NO;
dirty |= RCTAssignSuggestedDimension(_cssNode, CSS_HEIGHT, size.height);
dirty |= RCTAssignSuggestedDimension(_cssNode, CSS_WIDTH, size.width);
if (dirty) {
[self dirtyLayout];
}
}
}
- (void)setTopLeft:(CGPoint)topLeft - (void)setTopLeft:(CGPoint)topLeft
{ {
_cssNode->style.position[CSS_LEFT] = topLeft.x; _cssNode->style.position[CSS_LEFT] = topLeft.x;

View File

@ -10,16 +10,9 @@ if [ -z "$1" ]
exit 255 exit 255
fi fi
xctool \
-project IntegrationTests/IntegrationTests.xcodeproj \
-scheme IntegrationTests \
-sdk iphonesimulator8.1 \
-destination "platform=iOS Simulator,OS=${1},name=iPhone 5" \
build test
xctool \ xctool \
-project Examples/UIExplorer/UIExplorer.xcodeproj \ -project Examples/UIExplorer/UIExplorer.xcodeproj \
-scheme UIExplorer \ -scheme UIExplorer \
-sdk iphonesimulator8.1 \ -sdk iphonesimulator${1} \
-destination "platform=iOS Simulator,OS=${1},name=iPhone 5" \ -destination "platform=iOS Simulator,OS=${1},name=iPhone 5" \
build test build test