diff --git a/CHANGELOG.md b/CHANGELOG.md index 13e6243..fb428bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +# [0.7.0](https://github.com/IjzerenHein/react-native-shared-element/compare/v0.6.1...v0.7.0) (2020-04-19) + +### Features + +* **ios** Add support different border-radii per corner + +### Bug Fixes + +* **ios** Fix transforms applied by parent-navigator (MaterialTopTabNavigator issue) + ## [0.6.1](https://github.com/IjzerenHein/react-native-shared-element/compare/v0.6.0...v0.6.1) (2020-03-30) ### Bug Fixes diff --git a/ios/RNSharedElement.xcodeproj/project.pbxproj b/ios/RNSharedElement.xcodeproj/project.pbxproj index 18b0a50..3e4bf81 100644 --- a/ios/RNSharedElement.xcodeproj/project.pbxproj +++ b/ios/RNSharedElement.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 212F5AFF24335FD400C3F52E /* RNSharedElementCornerRadii.m in Sources */ = {isa = PBXBuildFile; fileRef = 212F5AFD24335FD400C3F52E /* RNSharedElementCornerRadii.m */; }; 214D6B5F22B01BDA00227ED9 /* RNSharedElementNodeManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 214D6B5522B01BD900227ED9 /* RNSharedElementNodeManager.m */; }; 214D6B6022B01BDA00227ED9 /* RNSharedElementNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 214D6B5622B01BD900227ED9 /* RNSharedElementNode.m */; }; 214D6B6122B01BDA00227ED9 /* RNSharedElementTransitionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 214D6B5A22B01BD900227ED9 /* RNSharedElementTransitionManager.m */; }; @@ -30,6 +31,8 @@ /* Begin PBXFileReference section */ 134814201AA4EA6300B7C361 /* libRNSharedElement.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNSharedElement.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 212F5AFD24335FD400C3F52E /* RNSharedElementCornerRadii.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNSharedElementCornerRadii.m; sourceTree = ""; }; + 212F5AFE24335FD400C3F52E /* RNSharedElementCornerRadii.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNSharedElementCornerRadii.h; sourceTree = ""; }; 214D6B5422B01BD900227ED9 /* RNSharedElementNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNSharedElementNode.h; sourceTree = ""; }; 214D6B5522B01BD900227ED9 /* RNSharedElementNodeManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNSharedElementNodeManager.m; sourceTree = ""; }; 214D6B5622B01BD900227ED9 /* RNSharedElementNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNSharedElementNode.m; sourceTree = ""; }; @@ -86,6 +89,8 @@ 214D6B6322B01C5800227ED9 /* RNSharedElementStyle.m */, 2157927D237AFB23003B1102 /* RNSharedElementContent.h */, 2157927C237AFB23003B1102 /* RNSharedElementContent.m */, + 212F5AFE24335FD400C3F52E /* RNSharedElementCornerRadii.h */, + 212F5AFD24335FD400C3F52E /* RNSharedElementCornerRadii.m */, 134814211AA4EA7D00B7C361 /* Products */, ); sourceTree = ""; @@ -149,6 +154,7 @@ files = ( 214D6B6222B01BDA00227ED9 /* RNSharedElementTransition.m in Sources */, 214D6B5F22B01BDA00227ED9 /* RNSharedElementNodeManager.m in Sources */, + 212F5AFF24335FD400C3F52E /* RNSharedElementCornerRadii.m in Sources */, 2157927E237AFB23003B1102 /* RNSharedElementContent.m in Sources */, 214D6B6122B01BDA00227ED9 /* RNSharedElementTransitionManager.m in Sources */, 214D6B6022B01BDA00227ED9 /* RNSharedElementNode.m in Sources */, diff --git a/ios/RNSharedElementCornerRadii.h b/ios/RNSharedElementCornerRadii.h new file mode 100644 index 0000000..29fd884 --- /dev/null +++ b/ios/RNSharedElementCornerRadii.h @@ -0,0 +1,34 @@ +// +// RNSharedElementCornerRadii_h +// react-native-shared-element +// + +#import +#import + +typedef NS_ENUM(NSInteger, RNSharedElementCorner) { + RNSharedElementCornerAll = 0, + RNSharedElementCornerTopLeft = 1, + RNSharedElementCornerTopRight = 2, + RNSharedElementCornerBottomLeft = 3, + RNSharedElementCornerBottomRight = 4, + RNSharedElementCornerTopStart = 5, + RNSharedElementCornerTopEnd = 6, + RNSharedElementCornerBottomStart = 7, + RNSharedElementCornerBottomEnd = 8 +}; + +@interface RNSharedElementCornerRadii : NSObject + +@property (nonatomic, assign) UIUserInterfaceLayoutDirection layoutDirection; + +- (instancetype)init; + +- (CGFloat)radiusForCorner:(RNSharedElementCorner)corner; +- (BOOL)setRadius:(CGFloat)radius corner:(RNSharedElementCorner)corner; + +- (void)updateClipMaskForLayer:(CALayer *)layer bounds:(CGRect)bounds; +- (void)updateShadowPathForLayer:(CALayer *)layer bounds:(CGRect)bounds; +- (RCTCornerRadii)radiiForBounds:(CGRect)bounds; + +@end diff --git a/ios/RNSharedElementCornerRadii.m b/ios/RNSharedElementCornerRadii.m new file mode 100644 index 0000000..5060067 --- /dev/null +++ b/ios/RNSharedElementCornerRadii.m @@ -0,0 +1,162 @@ +// +// RNSharedElementCornerRadii_m +// react-native-shared-element +// + +#import "RNSharedElementCornerRadii.h" +#import +#import + +static CGFloat RNSharedElementDefaultIfNegativeTo(CGFloat defaultValue, CGFloat x) +{ + return x >= 0 ? x : defaultValue; +}; + +#define RADII_COUNT 9 + +@implementation RNSharedElementCornerRadii { + CGFloat _radii[RADII_COUNT]; + BOOL _invalidated; + CGRect _cachedBounds; + RCTCornerRadii _cachedRadii; +} + +- (instancetype)init +{ + if (self = [super init]) { + _invalidated = YES; + _layoutDirection = UIUserInterfaceLayoutDirectionLeftToRight; + for (int i = 0; i < RADII_COUNT; i++) { + _radii[i] = -1; + } + } + return self; +} + + +#pragma mark Properties + +- (void)setLayoutDirection:(UIUserInterfaceLayoutDirection)layoutDirection +{ + if (_layoutDirection != layoutDirection) { + _layoutDirection = layoutDirection; + _invalidated = YES; + } +} + + +#pragma mark Methods + +- (CGFloat)radiusForCorner:(RNSharedElementCorner)corner +{ + return _radii[corner]; +} + +- (BOOL)setRadius:(CGFloat)radius corner:(RNSharedElementCorner)corner +{ + if (_radii[corner] != radius) { + _radii[corner] = radius; + _invalidated = YES; + return YES; + } + return NO; +} + +- (void)updateClipMaskForLayer:(CALayer *)layer bounds:(CGRect)bounds +{ + RCTCornerRadii radii = [self radiiForBounds:bounds]; + + CALayer *mask = nil; + CGFloat cornerRadius = 0; + + if (RCTCornerRadiiAreEqual(radii)) { + cornerRadius = radii.topLeft; + } else { + CAShapeLayer *shapeLayer = [CAShapeLayer layer]; + RCTCornerInsets cornerInsets = RCTGetCornerInsets(radii, UIEdgeInsetsZero); + CGPathRef path = RCTPathCreateWithRoundedRect(bounds, cornerInsets, NULL); + shapeLayer.path = path; + CGPathRelease(path); + mask = shapeLayer; + } + + layer.cornerRadius = cornerRadius; + layer.mask = mask; +} + +- (void)updateShadowPathForLayer:(CALayer *)layer bounds:(CGRect)bounds +{ + RCTCornerRadii radii = [self radiiForBounds:bounds]; + + BOOL hasShadow = layer.shadowOpacity * CGColorGetAlpha(layer.shadowColor) > 0; + if (!hasShadow) { + layer.shadowPath = nil; + return; + } + + RCTCornerInsets cornerInsets = RCTGetCornerInsets(radii, UIEdgeInsetsZero); + CGPathRef path = RCTPathCreateWithRoundedRect(bounds, cornerInsets, NULL); + layer.shadowPath = path; + CGPathRelease(path); +} + +- (RCTCornerRadii)radiiForBounds:(CGRect)bounds; +{ + if (!_invalidated && CGRectEqualToRect(_cachedBounds, bounds)) { + return _cachedRadii; + } + + const BOOL isRTL = _layoutDirection == UIUserInterfaceLayoutDirectionRightToLeft; + const CGFloat radius = MAX(0, _radii[RNSharedElementCornerAll]); + RCTCornerRadii result; + + if ([[RCTI18nUtil sharedInstance] doLeftAndRightSwapInRTL]) { + const CGFloat topStartRadius = RNSharedElementDefaultIfNegativeTo(_radii[RNSharedElementCornerTopLeft], _radii[RNSharedElementCornerTopStart]); + const CGFloat topEndRadius = RNSharedElementDefaultIfNegativeTo(_radii[RNSharedElementCornerTopRight], _radii[RNSharedElementCornerTopEnd]); + const CGFloat bottomStartRadius = RNSharedElementDefaultIfNegativeTo(_radii[RNSharedElementCornerBottomLeft], _radii[RNSharedElementCornerBottomStart]); + const CGFloat bottomEndRadius = RNSharedElementDefaultIfNegativeTo(_radii[RNSharedElementCornerBottomRight], _radii[RNSharedElementCornerBottomEnd]); + + const CGFloat directionAwareTopLeftRadius = isRTL ? topEndRadius : topStartRadius; + const CGFloat directionAwareTopRightRadius = isRTL ? topStartRadius : topEndRadius; + const CGFloat directionAwareBottomLeftRadius = isRTL ? bottomEndRadius : bottomStartRadius; + const CGFloat directionAwareBottomRightRadius = isRTL ? bottomStartRadius : bottomEndRadius; + + result.topLeft = RNSharedElementDefaultIfNegativeTo(radius, directionAwareTopLeftRadius); + result.topRight = RNSharedElementDefaultIfNegativeTo(radius, directionAwareTopRightRadius); + result.bottomLeft = RNSharedElementDefaultIfNegativeTo(radius, directionAwareBottomLeftRadius); + result.bottomRight = RNSharedElementDefaultIfNegativeTo(radius, directionAwareBottomRightRadius); + } else { + const CGFloat directionAwareTopLeftRadius = isRTL ? _radii[RNSharedElementCornerTopEnd] : _radii[RNSharedElementCornerTopStart]; + const CGFloat directionAwareTopRightRadius = isRTL ? _radii[RNSharedElementCornerTopStart] : _radii[RNSharedElementCornerTopEnd]; + const CGFloat directionAwareBottomLeftRadius = isRTL ? _radii[RNSharedElementCornerBottomEnd] : _radii[RNSharedElementCornerBottomStart]; + const CGFloat directionAwareBottomRightRadius = isRTL ? _radii[RNSharedElementCornerBottomStart] : _radii[RNSharedElementCornerBottomEnd]; + + result.topLeft = + RNSharedElementDefaultIfNegativeTo(radius, RNSharedElementDefaultIfNegativeTo(_radii[RNSharedElementCornerTopLeft], directionAwareTopLeftRadius)); + result.topRight = + RNSharedElementDefaultIfNegativeTo(radius, RNSharedElementDefaultIfNegativeTo(_radii[RNSharedElementCornerTopRight], directionAwareTopRightRadius)); + result.bottomLeft = + RNSharedElementDefaultIfNegativeTo(radius, RNSharedElementDefaultIfNegativeTo(_radii[RNSharedElementCornerBottomLeft], directionAwareBottomLeftRadius)); + result.bottomRight = RNSharedElementDefaultIfNegativeTo( + radius, RNSharedElementDefaultIfNegativeTo(_radii[RNSharedElementCornerBottomRight], directionAwareBottomRightRadius)); + } + + // Get scale factors required to prevent radii from overlapping + const CGFloat topScaleFactor = RCTZeroIfNaN(MIN(1, bounds.size.width / (result.topLeft + result.topRight))); + const CGFloat bottomScaleFactor = RCTZeroIfNaN(MIN(1, bounds.size.width / (result.bottomLeft + result.bottomRight))); + const CGFloat rightScaleFactor = RCTZeroIfNaN(MIN(1, bounds.size.height / (result.topRight + result.bottomRight))); + const CGFloat leftScaleFactor = RCTZeroIfNaN(MIN(1, bounds.size.height / (result.topLeft + result.bottomLeft))); + + result.topLeft *= MIN(topScaleFactor, leftScaleFactor); + result.topRight *= MIN(topScaleFactor, rightScaleFactor); + result.bottomLeft *= MIN(bottomScaleFactor, leftScaleFactor); + result.bottomRight *= MIN(bottomScaleFactor, rightScaleFactor); + + _cachedBounds = bounds; + _cachedRadii = result; + _invalidated = NO; + + return result; +} + +@end diff --git a/ios/RNSharedElementStyle.h b/ios/RNSharedElementStyle.h index cbc86e6..ce8c44e 100644 --- a/ios/RNSharedElementStyle.h +++ b/ios/RNSharedElementStyle.h @@ -7,22 +7,23 @@ #define RNSharedElementStyle_h #import +#import "RNSharedElementCornerRadii.h" @interface RNSharedElementStyle : NSObject -@property (nonatomic, assign) UIView* view; +@property (nonatomic, weak) UIView *view; @property (nonatomic, assign) CGRect layout; @property (nonatomic, assign) CGSize size; @property (nonatomic, assign) CATransform3D transform; @property (nonatomic, assign) UIViewContentMode contentMode; @property (nonatomic, assign) CGFloat opacity; -@property (nonatomic, assign) UIColor* backgroundColor; -@property (nonatomic, assign) CGFloat cornerRadius; +@property (nonatomic, strong) UIColor *backgroundColor; +@property (nonatomic, readonly) RNSharedElementCornerRadii *cornerRadii; @property (nonatomic, assign) CGFloat borderWidth; -@property (nonatomic, assign) UIColor* borderColor; +@property (nonatomic, strong) UIColor *borderColor; @property (nonatomic, assign) CGFloat shadowOpacity; @property (nonatomic, assign) CGFloat shadowRadius; @property (nonatomic, assign) CGSize shadowOffset; -@property (nonatomic, assign) UIColor* shadowColor; +@property (nonatomic, strong) UIColor *shadowColor; - (instancetype)init; - (instancetype)initWithView:(UIView*) view; diff --git a/ios/RNSharedElementStyle.m b/ios/RNSharedElementStyle.m index f19ebeb..bfdf9f3 100644 --- a/ios/RNSharedElementStyle.m +++ b/ios/RNSharedElementStyle.m @@ -7,77 +7,61 @@ #import @implementation RNSharedElementStyle -{ - UIColor* _backgroundColor; - UIColor* _borderColor; - UIColor* _shadowColor; -} - (instancetype)init { + if ((self = [super init])) { + _cornerRadii = [RNSharedElementCornerRadii new]; + } return self; } - (instancetype)initWithView:(UIView*) view { - _view = view; - _size = view.bounds.size; - _transform = [RNSharedElementStyle getAbsoluteViewTransform:view]; - - // Set base props from style - CALayer* layer = view.layer; - _opacity = layer.opacity; - _cornerRadius = layer.cornerRadius; - _borderWidth = layer.borderWidth; - _borderColor = layer.borderColor ? [UIColor colorWithCGColor:layer.borderColor] : [UIColor clearColor]; - _backgroundColor = layer.backgroundColor ? [UIColor colorWithCGColor:layer.backgroundColor] : [UIColor clearColor]; - _shadowColor = layer.shadowColor ? [UIColor colorWithCGColor:layer.shadowColor] : [UIColor clearColor]; - _shadowOffset = layer.shadowOffset; - _shadowRadius = layer.shadowRadius; - _shadowOpacity = layer.shadowOpacity; - - // On RN60 and beyond, certain styles are not immediately applied to the view/layer - // when a borderWidth is set on the view. Therefore, as a fail-safe we also try to - // get the props from the RCTView directly, when possible. - if ([view isKindOfClass:[RCTView class]]) { - RCTView* rctView = (RCTView*) view; - _cornerRadius = rctView.borderRadius; - _borderColor = rctView.borderColor ? [UIColor colorWithCGColor:rctView.borderColor] : [UIColor clearColor]; - _borderWidth = rctView.borderWidth >= 0.0f ? rctView.borderWidth : 0.0f; - _backgroundColor = rctView.backgroundColor ? rctView.backgroundColor : [UIColor clearColor]; + if ((self = [super init])) { + _view = view; + _size = view.bounds.size; + _transform = [RNSharedElementStyle getAbsoluteViewTransform:view]; + + // Set base props from style + CALayer* layer = view.layer; + _opacity = layer.opacity; + _borderWidth = layer.borderWidth; + _borderColor = layer.borderColor ? [UIColor colorWithCGColor:layer.borderColor] : [UIColor clearColor]; + _backgroundColor = layer.backgroundColor ? [UIColor colorWithCGColor:layer.backgroundColor] : [UIColor clearColor]; + _shadowColor = layer.shadowColor ? [UIColor colorWithCGColor:layer.shadowColor] : [UIColor clearColor]; + _shadowOffset = layer.shadowOffset; + _shadowRadius = layer.shadowRadius; + _shadowOpacity = layer.shadowOpacity; + + // On RN60 and beyond, certain styles are not immediately applied to the view/layer + // when a borderWidth is set on the view. Therefore, as a fail-safe we also try to + // get the props from the RCTView directly, when possible. + if ([view isKindOfClass:[RCTView class]]) { + RCTView* rctView = (RCTView*) view; + _cornerRadii = [RNSharedElementStyle cornerRadiiFromRCTView: rctView]; + _borderColor = rctView.borderColor ? [UIColor colorWithCGColor:rctView.borderColor] : [UIColor clearColor]; + _borderWidth = rctView.borderWidth >= 0.0f ? rctView.borderWidth : 0.0f; + _backgroundColor = rctView.backgroundColor ? rctView.backgroundColor : [UIColor clearColor]; + } else { + _cornerRadii = [RNSharedElementCornerRadii new]; + [_cornerRadii setRadius:layer.cornerRadius corner:RNSharedElementCornerAll]; + } } return self; } -- (void) setBackgroundColor:(UIColor*)backgroundColor { - _backgroundColor = backgroundColor; -} -- (UIColor*) backgroundColor -{ - return _backgroundColor; -} - -- (void) setBorderColor:(UIColor*)borderColor { - _borderColor = borderColor; -} -- (UIColor*) borderColor -{ - return _borderColor; -} - -- (void) setShadowColor:(UIColor*)shadowColor { - _shadowColor = shadowColor; -} - -- (UIColor*)shadowColor -{ - return _shadowColor; -} - + (NSString*) stringFromTransform:(CATransform3D) transform { - return [NSString stringWithFormat:@"x=%f, y=%f, z=%f", - transform.m41, transform.m42, transform.m43]; + BOOL isAffine = CATransform3DIsAffine(transform); + if (isAffine) { + CGAffineTransform affine = CATransform3DGetAffineTransform(transform); + return [NSString stringWithFormat:@"tx=%f, ty=%f, sx=%f, sy=%f, ro=%f", + affine.tx, affine.ty, affine.a, affine.d, atan2f(affine.b, affine.a) * (180 / M_PI)]; + } else { + return [NSString stringWithFormat:@"x=%f, y=%f, z=%f", + transform.m41, transform.m42, transform.m43]; + } } + (CATransform3D) getAbsoluteViewTransform:(UIView*) view @@ -85,24 +69,7 @@ + (CATransform3D) getAbsoluteViewTransform:(UIView*) view CATransform3D transform = view.layer.transform; view = view.superview; while (view != nil) { - CATransform3D t2 = view.layer.transform; - // Other transform props are not needed for now, maybe support them later - /*transform.m11 *= t2.m11; - transform.m12 *= t2.m12; - transform.m13 *= t2.m13; - transform.m14 *= t2.m14; - transform.m21 *= t2.m21; - transform.m22 *= t2.m22; - transform.m23 *= t2.m23; - transform.m24 *= t2.m24; - transform.m31 *= t2.m31; - transform.m32 *= t2.m32; - transform.m33 *= t2.m33; - transform.m34 *= t2.m34;*/ - transform.m41 += t2.m41; // translateX - transform.m42 += t2.m42; // translateY - transform.m43 += t2.m43; // translateZ - //transform.m44 *= t2.m44; + transform = CATransform3DConcat(transform, view.layer.transform); view = view.superview; } return transform; @@ -134,7 +101,15 @@ + (RNSharedElementStyle*) getInterpolatedStyle:(RNSharedElementStyle*)style1 sty { RNSharedElementStyle* style = [[RNSharedElementStyle alloc]init]; style.opacity = style1.opacity + ((style2.opacity - style1.opacity) * position); - style.cornerRadius = style1.cornerRadius + ((style2.cornerRadius - style1.cornerRadius) * position); + + CGRect radiiRect = CGRectMake(0, 0, 1000000, 1000000); + RCTCornerRadii radii1 = [style1.cornerRadii radiiForBounds:radiiRect]; + RCTCornerRadii radii2 = [style2.cornerRadii radiiForBounds:radiiRect]; + [style.cornerRadii setRadius:radii1.topLeft + ((radii2.topLeft - radii1.topLeft) * position) corner:RNSharedElementCornerTopLeft]; + [style.cornerRadii setRadius:radii1.topRight + ((radii2.topRight - radii1.topRight) * position) corner:RNSharedElementCornerTopRight]; + [style.cornerRadii setRadius:radii1.bottomLeft + ((radii2.bottomLeft - radii1.bottomLeft) * position) corner:RNSharedElementCornerBottomLeft]; + [style.cornerRadii setRadius:radii1.bottomRight + ((radii2.bottomRight - radii1.bottomRight) * position) corner:RNSharedElementCornerBottomRight]; + style.borderWidth = style1.borderWidth + ((style2.borderWidth - style1.borderWidth) * position); style.borderColor = [RNSharedElementStyle getInterpolatedColor:style1.borderColor color2:style2.borderColor position:position]; style.backgroundColor = [RNSharedElementStyle getInterpolatedColor:style1.backgroundColor color2:style2.backgroundColor position:position]; @@ -148,4 +123,20 @@ + (RNSharedElementStyle*) getInterpolatedStyle:(RNSharedElementStyle*)style1 sty return style; } ++ (RNSharedElementCornerRadii *)cornerRadiiFromRCTView:(RCTView *)rctView +{ + RNSharedElementCornerRadii *cornerRadii = [RNSharedElementCornerRadii new]; + [cornerRadii setRadius:[rctView borderRadius] corner:RNSharedElementCornerAll]; + [cornerRadii setRadius:[rctView borderTopLeftRadius] corner:RNSharedElementCornerTopLeft]; + [cornerRadii setRadius:[rctView borderTopRightRadius] corner:RNSharedElementCornerTopRight]; + [cornerRadii setRadius:[rctView borderTopStartRadius] corner:RNSharedElementCornerTopStart]; + [cornerRadii setRadius:[rctView borderTopEndRadius] corner:RNSharedElementCornerTopEnd]; + [cornerRadii setRadius:[rctView borderBottomLeftRadius] corner:RNSharedElementCornerBottomLeft]; + [cornerRadii setRadius:[rctView borderBottomRightRadius] corner:RNSharedElementCornerBottomRight]; + [cornerRadii setRadius:[rctView borderBottomStartRadius] corner:RNSharedElementCornerBottomStart]; + [cornerRadii setRadius:[rctView borderBottomEndRadius] corner:RNSharedElementCornerBottomEnd]; + [cornerRadii setLayoutDirection:[rctView reactLayoutDirection]]; + return cornerRadii; +} + @end diff --git a/ios/RNSharedElementTransition.m b/ios/RNSharedElementTransition.m index 6899387..5c014c3 100644 --- a/ios/RNSharedElementTransition.m +++ b/ios/RNSharedElementTransition.m @@ -31,6 +31,7 @@ @implementation RNSharedElementTransition UIImageView* _secondaryImageView; BOOL _reactFrameSet; BOOL _initialLayoutPassCompleted; + int _initialVisibleAncestorIndex; } - (instancetype)initWithNodeManager:(RNSharedElementNodeManager*)nodeManager @@ -48,6 +49,7 @@ - (instancetype)initWithNodeManager:(RNSharedElementNodeManager*)nodeManager _align = RNSharedElementAlignCenterCenter; _reactFrameSet = NO; _initialLayoutPassCompleted = NO; + _initialVisibleAncestorIndex = -1; self.userInteractionEnabled = NO; _outerStyleView = [[UIImageView alloc]init]; @@ -228,32 +230,31 @@ - (void) didLoadStyle:(RNSharedElementStyle *)style node:(RNSharedElementNode*)n } - (CGRect)normalizeLayout:(CGRect)layout + compensateForTransforms:(BOOL)compensateForTransforms ancestor:(RNSharedElementTransitionItem*)ancestor otherAncestor:(RNSharedElementTransitionItem*)otherAncestor + { - RNSharedElementStyle* style = ancestor.style; - if (style == nil) return [self.superview convertRect:layout fromView:nil]; - RNSharedElementStyle* otherStyle = otherAncestor ? otherAncestor.style : nil; - - // Exclude translations that have been applied to the scene by the navigator. - // E.g. a navigator might translate the scene to the right of the screen, - // in order to create a "slide in" animation to show it. In this case the absolute - // measured position of the element is incorrect, and needs to be corrected. - CGFloat sceneTranslateX = ((otherStyle == nil) || (otherStyle.transform.m41 != style.transform.m41)) ? style.transform.m41 : 0; - CGFloat sceneTranslateY = ((otherStyle == nil) || (otherStyle.transform.m42 != style.transform.m42)) ? style.transform.m42 : 0; - layout.origin.x -= sceneTranslateX; - layout.origin.y -= sceneTranslateY; - - // Undo any scaling in case the screen is scaled - if (!CGSizeEqualToSize(style.layout.size, style.size)) { - CGFloat scaleX = style.size.width / style.layout.size.width; - CGFloat scaleY = style.size.height / style.layout.size.height; - layout.origin.x *= scaleX; - layout.origin.y *= scaleY; - layout.size.width *= scaleX; - layout.size.height *= scaleY; + // Compensate for any transforms that have been applied to the scene by the + // navigator. For instance, a navigator may translate the scene to the right, + // outside of the screen, in order to show it using a slide animation. + // In such a case, remove that transform in order to obtain the "real" + // size and position on the screen. + if (compensateForTransforms && (ancestor.style != nil)) { + RNSharedElementStyle* style = ancestor.style; + RNSharedElementStyle* otherStyle = otherAncestor ? otherAncestor.style : nil; + CATransform3D transform = otherStyle ? CATransform3DConcat(style.transform, CATransform3DInvert(otherStyle.transform)) : style.transform; + if (CATransform3DIsAffine(transform)) { + CGAffineTransform affineTransform = CATransform3DGetAffineTransform(CATransform3DInvert(transform)); + layout = CGRectApplyAffineTransform(layout, affineTransform); + } else { + // Fallback + layout.origin.x -= transform.m41; + layout.origin.y -= transform.m42; + } } + // Convert to render overlay coordinates return [self.superview convertRect:layout fromView:nil]; } @@ -320,22 +321,26 @@ - (UIEdgeInsets) getInterpolatedClipInsets:(CGRect)interpolatedLayout startClipI return clipInsets; } -- (void) applyStyle:(RNSharedElementStyle*)style layer:(CALayer*)layer +- (void) applyStyle:(RNSharedElementStyle*)style view:(UIView*)view { + CALayer *layer = view.layer; + layer.opacity = style.opacity; layer.backgroundColor = style.backgroundColor.CGColor; - layer.cornerRadius = style.cornerRadius; layer.borderWidth = style.borderWidth; layer.borderColor = style.borderColor.CGColor; layer.shadowOpacity = style.shadowOpacity; layer.shadowRadius = style.shadowRadius; layer.shadowOffset = style.shadowOffset; layer.shadowColor = style.shadowColor.CGColor; + [style.cornerRadii updateShadowPathForLayer:layer bounds:view.bounds]; + [style.cornerRadii updateClipMaskForLayer:layer bounds:view.bounds]; } - (void) fireMeasureEvent:(RNSharedElementTransitionItem*) item layout:(CGRect)layout visibleLayout:(CGRect)visibleLayout contentLayout:(CGRect)contentLayout { if (!self.onMeasureNode) return; + RCTCornerRadii cornerRadii = [item.style.cornerRadii radiiForBounds:_outerStyleView.bounds]; NSDictionary* eventData = @{ @"node": item.name, @"layout": @{ @@ -354,7 +359,10 @@ - (void) fireMeasureEvent:(RNSharedElementTransitionItem*) item layout:(CGRect)l }, @"contentType": item.content ? item.content.typeName : @"none", @"style": @{ - @"borderRadius": @(item.style.cornerRadius) + @"borderTopLeftRadius": @(cornerRadii.topLeft), + @"borderTopRightRadius": @(cornerRadii.topRight), + @"borderBottomLeftRadius": @(cornerRadii.bottomLeft), + @"borderBotomRightRadius": @(cornerRadii.bottomRight) } }; self.onMeasureNode(eventData); @@ -369,19 +377,36 @@ - (void) updateStyle RNSharedElementTransitionItem* startAncestor = [_items objectAtIndex:ITEM_START_ANCESTOR]; RNSharedElementTransitionItem* endItem = [_items objectAtIndex:ITEM_END]; RNSharedElementTransitionItem* endAncestor = [_items objectAtIndex:ITEM_END_ANCESTOR]; + RNSharedElementStyle* startStyle = startItem.style; + RNSharedElementStyle* endStyle = endItem.style; + + // Determine starting scene that is currently visible to the user + if (_initialVisibleAncestorIndex < 0) { + RNSharedElementStyle* startAncenstorLayout = startAncestor.style; + RNSharedElementStyle* endAncestorStyle = endAncestor.style; + if (startAncenstorLayout && !endAncestorStyle) { + _initialVisibleAncestorIndex = 0; + } else if (!startAncenstorLayout && endAncestorStyle) { + _initialVisibleAncestorIndex = 1; + } else if (startAncenstorLayout && endAncestorStyle){ + CGRect startAncestorVisible = CGRectIntersection(self.superview.bounds, [self.superview convertRect:startAncenstorLayout.layout fromView:nil]); + CGRect endAncestorVisible = CGRectIntersection(self.superview.bounds, [self.superview convertRect:endAncestorStyle.layout fromView:nil]); + _initialVisibleAncestorIndex = ((endAncestorVisible.size.width * endAncestorVisible.size.height) > (startAncestorVisible.size.width * startAncestorVisible.size.height)) ? 1 : 0; + } + } // Get start layout - RNSharedElementStyle* startStyle = startItem.style; - CGRect startLayout = startStyle ? [self normalizeLayout:startStyle.layout ancestor:startAncestor otherAncestor:endAncestor] : CGRectZero; - CGRect startVisibleLayout = startStyle ? [self normalizeLayout:[startItem visibleLayoutForAncestor:startAncestor] ancestor:startAncestor otherAncestor:endAncestor] : CGRectZero; - CGRect startContentLayout = startStyle ? [self normalizeLayout:[startItem contentLayoutForContent:startItem.content] ancestor:startAncestor otherAncestor:endAncestor] : CGRectZero; + BOOL startCompensate = _initialVisibleAncestorIndex == 1; + CGRect startLayout = startStyle ? [self normalizeLayout:startStyle.layout compensateForTransforms:startCompensate ancestor:startAncestor otherAncestor:endAncestor] : CGRectZero; + CGRect startVisibleLayout = startStyle ? [self normalizeLayout:[startItem visibleLayoutForAncestor:startAncestor] compensateForTransforms:startCompensate ancestor:startAncestor otherAncestor:endAncestor] : CGRectZero; + CGRect startContentLayout = startStyle ? [self normalizeLayout:[startItem contentLayoutForContent:startItem.content] compensateForTransforms:startCompensate ancestor:startAncestor otherAncestor:endAncestor] : CGRectZero; UIEdgeInsets startClipInsets = [self getClipInsets:startLayout visibleLayout:startVisibleLayout]; // Get end layout - RNSharedElementStyle* endStyle = endItem.style; - CGRect endLayout = endStyle ? [self normalizeLayout:endStyle.layout ancestor:endAncestor otherAncestor:startAncestor] : CGRectZero; - CGRect endVisibleLayout = endStyle ? [self normalizeLayout:[endItem visibleLayoutForAncestor:endAncestor] ancestor:endAncestor otherAncestor:startAncestor] : CGRectZero; - CGRect endContentLayout = endStyle ? [self normalizeLayout:[endItem contentLayoutForContent:(endItem.content ? endItem.content : startItem.content)] ancestor:endAncestor otherAncestor:startAncestor] : CGRectZero; + BOOL endCompensate = _initialVisibleAncestorIndex == 0; + CGRect endLayout = endStyle ? [self normalizeLayout:endStyle.layout compensateForTransforms:endCompensate ancestor:endAncestor otherAncestor:startAncestor] : CGRectZero; + CGRect endVisibleLayout = endStyle ? [self normalizeLayout:[endItem visibleLayoutForAncestor:endAncestor] compensateForTransforms:endCompensate ancestor:endAncestor otherAncestor:startAncestor] : CGRectZero; + CGRect endContentLayout = endStyle ? [self normalizeLayout:[endItem contentLayoutForContent:(endItem.content ? endItem.content : startItem.content)] compensateForTransforms:endCompensate ancestor:endAncestor otherAncestor:startAncestor] : CGRectZero; UIEdgeInsets endClipInsets = [self getClipInsets:endLayout visibleLayout:endVisibleLayout]; // Get interpolated style & layout @@ -431,15 +456,15 @@ - (void) updateStyle // background color, and shadow. Because of the shadow, the view itsself // does not mask its bounds, otherwise the shadow isn't visible. _outerStyleView.frame = interpolatedLayout; - [self applyStyle:interpolatedStyle layer:_outerStyleView.layer]; + [self applyStyle:interpolatedStyle view:_outerStyleView]; // Update inner clip view. This view holds the image/content views // inside and clips their content. CGRect innerClipFrame = interpolatedLayout; innerClipFrame.origin.x = 0; innerClipFrame.origin.y = 0; - _innerClipView.layer.cornerRadius = interpolatedStyle.cornerRadius; _innerClipView.frame = innerClipFrame; + [interpolatedStyle.cornerRadii updateClipMaskForLayer:_innerClipView.layer bounds:_innerClipView.bounds]; _innerClipView.layer.masksToBounds = _resize != RNSharedElementResizeNone; // Update content diff --git a/jest.setup.js b/jest.setup.js deleted file mode 100644 index 9826692..0000000 --- a/jest.setup.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @format - * @flow - */ -/* eslint-env jest */ - -import { NativeModules } from 'react-native'; - -// Mock the RNCGeolocation native module to allow us to unit test the JavaScript code -NativeModules.RNCGeolocation = { - addListener: jest.fn(), - getCurrentPosition: jest.fn(), - removeListeners: jest.fn(), - requestAuthorization: jest.fn(), - setConfiguration: jest.fn(), - startObserving: jest.fn(), - stopObserving: jest.fn(), -}; - -// Reset the mocks before each test -global.beforeEach(() => { - jest.resetAllMocks(); -}); diff --git a/package.json b/package.json index 4ef82a7..20dd933 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-shared-element", - "version": "0.6.1", + "version": "0.7.0", "description": "Native shared element transition primitives for react-native 💫", "main": "build/index.js", "types": "build/index.d.ts", diff --git a/test-app/ios/Podfile.lock b/test-app/ios/Podfile.lock index b3dd569..2cc6dd1 100644 --- a/test-app/ios/Podfile.lock +++ b/test-app/ios/Podfile.lock @@ -272,7 +272,7 @@ PODS: - React - RNScreens (2.0.0-alpha.12): - React - - RNSharedElement (0.6.0): + - RNSharedElement (0.6.1): - React - RNVectorIcons (6.6.0): - React @@ -556,7 +556,7 @@ SPEC CHECKSUMS: RNGestureHandler: 911d3b110a7a233a34c4f800e7188a84b75319c6 RNReanimated: b2ab0b693dddd2339bd2f300e770f6302d2e960c RNScreens: 254da4b84f25971cbb30ed3ddc84131f23cac812 - RNSharedElement: 7c98e474c8f72a67bbabb820b4b86460aa6502d8 + RNSharedElement: 7bc62aefd600b4c13d6e5b60e840ef20f7749a72 RNVectorIcons: 0bb4def82230be1333ddaeee9fcba45f0b288ed4 SDWebImage: 21b19f56b4226cdfe3aefe4e6848dc43ed129a86 SDWebImageWebPCoder: 947093edd1349d820c40afbd9f42acb6cdecd987 diff --git a/test-app/metro.config.js b/test-app/metro.config.js index 116ac41..19b66bf 100644 --- a/test-app/metro.config.js +++ b/test-app/metro.config.js @@ -12,9 +12,9 @@ module.exports = { getTransformOptions: async () => ({ transform: { experimentalImportSupport: false, - inlineRequires: false - } - }) + inlineRequires: false, + }, + }), }, // Add custom resolver and watch-folders because @@ -22,8 +22,14 @@ module.exports = { resolver: { extraNodeModules: new Proxy( {}, - { get: (_, name) => path.resolve("./node_modules", name) } - ) + { + get: (_, name) => + path.resolve( + name === "react-native-shared-element" ? ".." : "./node_modules", + name + ), + } + ), }, - watchFolders: [path.resolve("./node_modules"), path.resolve("..")] + watchFolders: [path.resolve("./node_modules"), path.resolve("..")], }; diff --git a/test-app/src/tests/image/ImageTests.tsx b/test-app/src/tests/image/ImageTests.tsx index 76661a7..85a4554 100644 --- a/test-app/src/tests/image/ImageTests.tsx +++ b/test-app/src/tests/image/ImageTests.tsx @@ -145,6 +145,19 @@ export function createImageTests(config: { start: , end: }, + { + name: "Border-radius some corners", + description: + "The border-radius should animate correct when only set for some corners.", + start: ( + + ), + end: + }, { name: "Border-radius & contain", description: diff --git a/test-app/src/tests/view/ViewTests.tsx b/test-app/src/tests/view/ViewTests.tsx index e999e01..f61aa55 100644 --- a/test-app/src/tests/view/ViewTests.tsx +++ b/test-app/src/tests/view/ViewTests.tsx @@ -49,6 +49,23 @@ export const ViewTests: TestGroup = { start: , end: }, + { + name: "View Border-radii", + description: + "The border-radii should animate correct when only set for some corners.", + start: ( + + ), + end: + }, { name: "View Border ➔ No-border", description: @@ -94,6 +111,25 @@ export const ViewTests: TestGroup = { /> ), end: + }, + { + name: "View Border-radii & Shadow", + description: + "The border-radii should animate correct when only set for some corners.", + start: ( + + ), + end: } ] },