react-native-morph-card 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +134 -0
- package/android/build.gradle +59 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/com/melivalesca/morphcard/MorphCardModule.kt +120 -0
- package/android/src/main/java/com/melivalesca/morphcard/MorphCardPackage.kt +42 -0
- package/android/src/main/java/com/melivalesca/morphcard/MorphCardSourceManager.kt +40 -0
- package/android/src/main/java/com/melivalesca/morphcard/MorphCardSourceView.kt +755 -0
- package/android/src/main/java/com/melivalesca/morphcard/MorphCardTargetManager.kt +48 -0
- package/android/src/main/java/com/melivalesca/morphcard/MorphCardTargetView.kt +159 -0
- package/android/src/main/java/com/melivalesca/morphcard/MorphCardViewRegistry.kt +24 -0
- package/android/src/main/jni/CMakeLists.txt +62 -0
- package/common/cpp/react/renderer/components/morphcard/RNCMorphCardState.h +30 -0
- package/ios/Fabric/RNCMorphCardSourceComponentView.h +25 -0
- package/ios/Fabric/RNCMorphCardSourceComponentView.mm +582 -0
- package/ios/Fabric/RNCMorphCardTargetComponentView.h +20 -0
- package/ios/Fabric/RNCMorphCardTargetComponentView.mm +99 -0
- package/ios/RNCMorphCardModule.h +14 -0
- package/ios/RNCMorphCardModule.mm +126 -0
- package/ios/RNCMorphCardSource.h +23 -0
- package/ios/RNCMorphCardSource.m +144 -0
- package/ios/RNCMorphCardSourceManager.h +5 -0
- package/ios/RNCMorphCardSourceManager.m +17 -0
- package/ios/RNCMorphCardTarget.h +19 -0
- package/ios/RNCMorphCardTarget.m +27 -0
- package/ios/RNCMorphCardTargetManager.h +5 -0
- package/ios/RNCMorphCardTargetManager.m +16 -0
- package/ios/RNCMorphCardViewRegistry.h +35 -0
- package/ios/RNCMorphCardViewRegistry.m +40 -0
- package/lib/commonjs/MorphCard.types.js +6 -0
- package/lib/commonjs/MorphCard.types.js.map +1 -0
- package/lib/commonjs/MorphCardSource.js +95 -0
- package/lib/commonjs/MorphCardSource.js.map +1 -0
- package/lib/commonjs/MorphCardTarget.js +83 -0
- package/lib/commonjs/MorphCardTarget.js.map +1 -0
- package/lib/commonjs/index.js +45 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/specs/NativeMorphCardModule.js +9 -0
- package/lib/commonjs/specs/NativeMorphCardModule.js.map +1 -0
- package/lib/commonjs/specs/NativeMorphCardSource.js +10 -0
- package/lib/commonjs/specs/NativeMorphCardSource.js.map +1 -0
- package/lib/commonjs/specs/NativeMorphCardTarget.js +10 -0
- package/lib/commonjs/specs/NativeMorphCardTarget.js.map +1 -0
- package/lib/commonjs/useMorphTarget.js +28 -0
- package/lib/commonjs/useMorphTarget.js.map +1 -0
- package/lib/module/MorphCard.types.js +4 -0
- package/lib/module/MorphCard.types.js.map +1 -0
- package/lib/module/MorphCardSource.js +85 -0
- package/lib/module/MorphCardSource.js.map +1 -0
- package/lib/module/MorphCardTarget.js +76 -0
- package/lib/module/MorphCardTarget.js.map +1 -0
- package/lib/module/index.js +6 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/specs/NativeMorphCardModule.js +5 -0
- package/lib/module/specs/NativeMorphCardModule.js.map +1 -0
- package/lib/module/specs/NativeMorphCardSource.js +5 -0
- package/lib/module/specs/NativeMorphCardSource.js.map +1 -0
- package/lib/module/specs/NativeMorphCardTarget.js +5 -0
- package/lib/module/specs/NativeMorphCardTarget.js.map +1 -0
- package/lib/module/useMorphTarget.js +22 -0
- package/lib/module/useMorphTarget.js.map +1 -0
- package/lib/typescript/src/MorphCard.types.d.ts +29 -0
- package/lib/typescript/src/MorphCard.types.d.ts.map +1 -0
- package/lib/typescript/src/MorphCardSource.d.ts +35 -0
- package/lib/typescript/src/MorphCardSource.d.ts.map +1 -0
- package/lib/typescript/src/MorphCardTarget.d.ts +20 -0
- package/lib/typescript/src/MorphCardTarget.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +6 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/specs/NativeMorphCardModule.d.ts +14 -0
- package/lib/typescript/src/specs/NativeMorphCardModule.d.ts.map +1 -0
- package/lib/typescript/src/specs/NativeMorphCardSource.d.ts +13 -0
- package/lib/typescript/src/specs/NativeMorphCardSource.d.ts.map +1 -0
- package/lib/typescript/src/specs/NativeMorphCardTarget.d.ts +25 -0
- package/lib/typescript/src/specs/NativeMorphCardTarget.d.ts.map +1 -0
- package/lib/typescript/src/useMorphTarget.d.ts +16 -0
- package/lib/typescript/src/useMorphTarget.d.ts.map +1 -0
- package/package.json +101 -0
- package/react-native-morph-card.podspec +41 -0
- package/react-native.config.js +13 -0
- package/src/MorphCard.types.ts +29 -0
- package/src/MorphCardSource.tsx +105 -0
- package/src/MorphCardTarget.tsx +127 -0
- package/src/index.tsx +10 -0
- package/src/specs/NativeMorphCardModule.ts +21 -0
- package/src/specs/NativeMorphCardSource.ts +20 -0
- package/src/specs/NativeMorphCardTarget.ts +38 -0
- package/src/useMorphTarget.ts +21 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
2
|
+
#import <morphcard/morphcard.h>
|
|
3
|
+
#else
|
|
4
|
+
#import <React/RCTBridgeModule.h>
|
|
5
|
+
#endif
|
|
6
|
+
|
|
7
|
+
@interface RNCMorphCardModule : NSObject
|
|
8
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
9
|
+
<NativeMorphCardModuleSpec>
|
|
10
|
+
#else
|
|
11
|
+
<RCTBridgeModule>
|
|
12
|
+
#endif
|
|
13
|
+
|
|
14
|
+
@end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#import "RNCMorphCardModule.h"
|
|
2
|
+
#import "RNCMorphCardViewRegistry.h"
|
|
3
|
+
|
|
4
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
5
|
+
#import "RNCMorphCardSourceComponentView.h"
|
|
6
|
+
#endif
|
|
7
|
+
|
|
8
|
+
@implementation RNCMorphCardModule
|
|
9
|
+
|
|
10
|
+
RCT_EXPORT_MODULE()
|
|
11
|
+
|
|
12
|
+
// No-op on iOS — Android uses this to create the overlay before navigation
|
|
13
|
+
RCT_EXPORT_METHOD(prepareExpand : (double)sourceTag) {
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
RCT_EXPORT_METHOD(setTargetConfig
|
|
17
|
+
: (double)sourceTag targetWidth
|
|
18
|
+
: (double)targetWidth targetHeight
|
|
19
|
+
: (double)targetHeight targetBorderRadius
|
|
20
|
+
: (double)targetBorderRadius contentOffsetY
|
|
21
|
+
: (double)contentOffsetY contentCentered
|
|
22
|
+
: (BOOL)contentCentered) {
|
|
23
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
24
|
+
UIView *sourceView =
|
|
25
|
+
[[RNCMorphCardViewRegistry shared] viewForTag:(NSInteger)sourceTag];
|
|
26
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
27
|
+
if ([sourceView
|
|
28
|
+
isKindOfClass:[RNCMorphCardSourceComponentView class]]) {
|
|
29
|
+
RNCMorphCardSourceComponentView *src =
|
|
30
|
+
(RNCMorphCardSourceComponentView *)sourceView;
|
|
31
|
+
src.pendingTargetWidth = targetWidth;
|
|
32
|
+
src.pendingTargetHeight = targetHeight;
|
|
33
|
+
src.pendingTargetBorderRadius = targetBorderRadius;
|
|
34
|
+
src.pendingContentOffsetY = contentOffsetY;
|
|
35
|
+
src.pendingContentCentered = contentCentered;
|
|
36
|
+
}
|
|
37
|
+
#endif
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
RCT_EXPORT_METHOD(expand
|
|
42
|
+
: (double)sourceTag targetTag
|
|
43
|
+
: (double)targetTag resolve
|
|
44
|
+
: (RCTPromiseResolveBlock)resolve reject
|
|
45
|
+
: (RCTPromiseRejectBlock)reject) {
|
|
46
|
+
dispatch_after(
|
|
47
|
+
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(150 * NSEC_PER_MSEC)),
|
|
48
|
+
dispatch_get_main_queue(), ^{
|
|
49
|
+
UIView *sourceView =
|
|
50
|
+
[[RNCMorphCardViewRegistry shared]
|
|
51
|
+
viewForTag:(NSInteger)sourceTag];
|
|
52
|
+
|
|
53
|
+
UIView *targetView =
|
|
54
|
+
[[RNCMorphCardViewRegistry shared]
|
|
55
|
+
viewForTag:(NSInteger)targetTag];
|
|
56
|
+
|
|
57
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
58
|
+
if ([sourceView
|
|
59
|
+
isKindOfClass:[RNCMorphCardSourceComponentView class]]) {
|
|
60
|
+
[(RNCMorphCardSourceComponentView *)sourceView
|
|
61
|
+
expandToTarget:targetView
|
|
62
|
+
resolve:resolve];
|
|
63
|
+
} else {
|
|
64
|
+
NSLog(@"[MorphCard] expand: source %ld is not a "
|
|
65
|
+
@"RNCMorphCardSourceComponentView",
|
|
66
|
+
(long)sourceTag);
|
|
67
|
+
resolve(@(NO));
|
|
68
|
+
}
|
|
69
|
+
#else
|
|
70
|
+
resolve(@(NO));
|
|
71
|
+
#endif
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
RCT_EXPORT_METHOD(getSourceSize
|
|
76
|
+
: (double)sourceTag resolve
|
|
77
|
+
: (RCTPromiseResolveBlock)resolve reject
|
|
78
|
+
: (RCTPromiseRejectBlock)reject) {
|
|
79
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
80
|
+
UIView *sourceView =
|
|
81
|
+
[[RNCMorphCardViewRegistry shared] viewForTag:(NSInteger)sourceTag];
|
|
82
|
+
if (sourceView) {
|
|
83
|
+
resolve(@{
|
|
84
|
+
@"width" : @(sourceView.bounds.size.width),
|
|
85
|
+
@"height" : @(sourceView.bounds.size.height)
|
|
86
|
+
});
|
|
87
|
+
} else {
|
|
88
|
+
resolve(@{@"width" : @(0), @"height" : @(0)});
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
RCT_EXPORT_METHOD(collapse
|
|
94
|
+
: (double)sourceTag resolve
|
|
95
|
+
: (RCTPromiseResolveBlock)resolve reject
|
|
96
|
+
: (RCTPromiseRejectBlock)reject) {
|
|
97
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
98
|
+
UIView *sourceView =
|
|
99
|
+
[[RNCMorphCardViewRegistry shared] viewForTag:(NSInteger)sourceTag];
|
|
100
|
+
|
|
101
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
102
|
+
if ([sourceView
|
|
103
|
+
isKindOfClass:[RNCMorphCardSourceComponentView class]]) {
|
|
104
|
+
[(RNCMorphCardSourceComponentView *)sourceView
|
|
105
|
+
collapseWithResolve:resolve];
|
|
106
|
+
} else {
|
|
107
|
+
NSLog(@"[MorphCard] collapse: source %ld is not a "
|
|
108
|
+
@"RNCMorphCardSourceComponentView",
|
|
109
|
+
(long)sourceTag);
|
|
110
|
+
resolve(@(NO));
|
|
111
|
+
}
|
|
112
|
+
#else
|
|
113
|
+
resolve(@(NO));
|
|
114
|
+
#endif
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
119
|
+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
|
120
|
+
(const facebook::react::ObjCTurboModule::InitParams &)params {
|
|
121
|
+
return std::make_shared<facebook::react::NativeMorphCardModuleSpecJSI>(
|
|
122
|
+
params);
|
|
123
|
+
}
|
|
124
|
+
#endif
|
|
125
|
+
|
|
126
|
+
@end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#import <React/RCTView.h>
|
|
2
|
+
|
|
3
|
+
@interface RNCMorphCardSource : RCTView
|
|
4
|
+
|
|
5
|
+
/// Event fired when the morph animation begins.
|
|
6
|
+
@property (nonatomic, copy) RCTDirectEventBlock onMorphStart;
|
|
7
|
+
|
|
8
|
+
/// Event fired when the morph animation completes (expand finished).
|
|
9
|
+
@property (nonatomic, copy) RCTDirectEventBlock onMorphComplete;
|
|
10
|
+
|
|
11
|
+
/// Event fired when the dismiss animation completes.
|
|
12
|
+
@property (nonatomic, copy) RCTDirectEventBlock onDismissComplete;
|
|
13
|
+
|
|
14
|
+
/// Duration of the morph animation in milliseconds. Defaults to 500.
|
|
15
|
+
@property (nonatomic, assign) CGFloat duration;
|
|
16
|
+
|
|
17
|
+
/// Programmatically trigger the expand.
|
|
18
|
+
- (void)expand;
|
|
19
|
+
|
|
20
|
+
/// Programmatically trigger the dismiss.
|
|
21
|
+
- (void)collapse;
|
|
22
|
+
|
|
23
|
+
@end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#import "RNCMorphCardSource.h"
|
|
2
|
+
#import "RNCMorphCardViewRegistry.h"
|
|
3
|
+
|
|
4
|
+
// ──────────────────────────────────────────────────────
|
|
5
|
+
// HOW THE EXPAND/COLLAPSE WORKS:
|
|
6
|
+
//
|
|
7
|
+
// The card is a normal RCTView in the view hierarchy. When expand
|
|
8
|
+
// is called, we:
|
|
9
|
+
//
|
|
10
|
+
// 1. Save the card's original frame and superview position
|
|
11
|
+
// 2. Move the card to the key window (so it's above everything)
|
|
12
|
+
// 3. Animate the card's frame from its original position to fullscreen
|
|
13
|
+
// 4. The card's children (both the "collapsed" card content and the
|
|
14
|
+
// "expanded" detail content) are inside it — JS controls which
|
|
15
|
+
// is visible via state
|
|
16
|
+
//
|
|
17
|
+
// On collapse, we reverse: animate back to original frame, then
|
|
18
|
+
// reparent back into the original superview.
|
|
19
|
+
//
|
|
20
|
+
// This means the ACTUAL VIEW expands — not a snapshot, not an overlay.
|
|
21
|
+
// The user's content (images, text, etc.) is live throughout.
|
|
22
|
+
// ──────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
@implementation RNCMorphCardSource {
|
|
25
|
+
// Saved state for collapsing back.
|
|
26
|
+
CGRect _originalFrame;
|
|
27
|
+
UIView *_originalSuperview;
|
|
28
|
+
NSInteger _originalIndex;
|
|
29
|
+
CGFloat _originalCornerRadius;
|
|
30
|
+
BOOL _isExpanded;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
- (instancetype)init {
|
|
34
|
+
if (self = [super init]) {
|
|
35
|
+
_duration = 500.0;
|
|
36
|
+
}
|
|
37
|
+
return self;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
- (void)didMoveToWindow {
|
|
41
|
+
[super didMoveToWindow];
|
|
42
|
+
if (self.window) {
|
|
43
|
+
[[RNCMorphCardViewRegistry shared] registerView:self withTag:self.tag];
|
|
44
|
+
} else {
|
|
45
|
+
[[RNCMorphCardViewRegistry shared] unregisterViewWithTag:self.tag];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
- (void)expand {
|
|
50
|
+
if (_isExpanded) return;
|
|
51
|
+
_isExpanded = YES;
|
|
52
|
+
|
|
53
|
+
// ── Save original position ──
|
|
54
|
+
// We need to remember where this view was so we can put it back.
|
|
55
|
+
_originalSuperview = self.superview;
|
|
56
|
+
_originalIndex = [_originalSuperview.subviews indexOfObject:self];
|
|
57
|
+
_originalCornerRadius = self.layer.cornerRadius;
|
|
58
|
+
|
|
59
|
+
// Get current position in window coordinates.
|
|
60
|
+
_originalFrame = [self convertRect:self.bounds toView:nil];
|
|
61
|
+
|
|
62
|
+
// ── Reparent to window ──
|
|
63
|
+
// Move the card to the key window so it sits above everything
|
|
64
|
+
// (navigation bars, tab bars, other views).
|
|
65
|
+
UIWindow *window = self.window;
|
|
66
|
+
if (!window) return;
|
|
67
|
+
|
|
68
|
+
// Set the frame in window coordinates BEFORE reparenting.
|
|
69
|
+
// After removeFromSuperview, the view's frame is relative to the
|
|
70
|
+
// new parent (window), so we use the window-relative frame.
|
|
71
|
+
[self removeFromSuperview];
|
|
72
|
+
self.frame = _originalFrame;
|
|
73
|
+
[window addSubview:self];
|
|
74
|
+
|
|
75
|
+
// Fire event so JS can swap to expanded content.
|
|
76
|
+
if (self.onMorphStart) {
|
|
77
|
+
self.onMorphStart(@{});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Animate to fullscreen ──
|
|
81
|
+
NSTimeInterval durationSeconds = _duration / 1000.0;
|
|
82
|
+
CGRect fullscreen = window.bounds;
|
|
83
|
+
|
|
84
|
+
UIViewPropertyAnimator *animator = [[UIViewPropertyAnimator alloc]
|
|
85
|
+
initWithDuration:durationSeconds
|
|
86
|
+
dampingRatio:0.85
|
|
87
|
+
animations:^{
|
|
88
|
+
self.frame = fullscreen;
|
|
89
|
+
self.layer.cornerRadius = 0;
|
|
90
|
+
}];
|
|
91
|
+
|
|
92
|
+
[animator addCompletion:^(UIViewAnimatingPosition finalPosition) {
|
|
93
|
+
if (self.onMorphComplete) {
|
|
94
|
+
self.onMorphComplete(@{});
|
|
95
|
+
}
|
|
96
|
+
}];
|
|
97
|
+
|
|
98
|
+
[animator startAnimation];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
- (void)collapse {
|
|
102
|
+
if (!_isExpanded) return;
|
|
103
|
+
|
|
104
|
+
NSTimeInterval durationSeconds = _duration / 1000.0;
|
|
105
|
+
|
|
106
|
+
UIViewPropertyAnimator *animator = [[UIViewPropertyAnimator alloc]
|
|
107
|
+
initWithDuration:durationSeconds
|
|
108
|
+
dampingRatio:0.85
|
|
109
|
+
animations:^{
|
|
110
|
+
self.frame = self->_originalFrame;
|
|
111
|
+
self.layer.cornerRadius = self->_originalCornerRadius;
|
|
112
|
+
}];
|
|
113
|
+
|
|
114
|
+
[animator addCompletion:^(UIViewAnimatingPosition finalPosition) {
|
|
115
|
+
self->_isExpanded = NO;
|
|
116
|
+
|
|
117
|
+
// ── Reparent back to original superview ──
|
|
118
|
+
// Put the card back where it was in the view hierarchy.
|
|
119
|
+
[self removeFromSuperview];
|
|
120
|
+
if (self->_originalSuperview) {
|
|
121
|
+
NSInteger insertIndex =
|
|
122
|
+
MIN(self->_originalIndex,
|
|
123
|
+
(NSInteger)self->_originalSuperview.subviews.count);
|
|
124
|
+
[self->_originalSuperview insertSubview:self atIndex:insertIndex];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Restore the original frame relative to the parent.
|
|
128
|
+
// Since we're back in the original superview, we need the
|
|
129
|
+
// local frame, not the window frame. Convert back.
|
|
130
|
+
if (self->_originalSuperview) {
|
|
131
|
+
self.frame = [self->_originalSuperview
|
|
132
|
+
convertRect:self->_originalFrame
|
|
133
|
+
fromView:nil];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (self.onDismissComplete) {
|
|
137
|
+
self.onDismissComplete(@{});
|
|
138
|
+
}
|
|
139
|
+
}];
|
|
140
|
+
|
|
141
|
+
[animator startAnimation];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
@end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#import "RNCMorphCardSourceManager.h"
|
|
2
|
+
#import "RNCMorphCardSource.h"
|
|
3
|
+
|
|
4
|
+
@implementation RNCMorphCardSourceManager
|
|
5
|
+
|
|
6
|
+
RCT_EXPORT_MODULE()
|
|
7
|
+
|
|
8
|
+
- (UIView *)view {
|
|
9
|
+
return [[RNCMorphCardSource alloc] init];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
RCT_EXPORT_VIEW_PROPERTY(onMorphStart, RCTDirectEventBlock)
|
|
13
|
+
RCT_EXPORT_VIEW_PROPERTY(onMorphComplete, RCTDirectEventBlock)
|
|
14
|
+
RCT_EXPORT_VIEW_PROPERTY(onDismissComplete, RCTDirectEventBlock)
|
|
15
|
+
RCT_EXPORT_VIEW_PROPERTY(duration, CGFloat)
|
|
16
|
+
|
|
17
|
+
@end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#import <React/RCTView.h>
|
|
2
|
+
|
|
3
|
+
@interface RNCMorphCardTarget : RCTView
|
|
4
|
+
|
|
5
|
+
/// Duration of the morph animation in milliseconds. Defaults to 350.
|
|
6
|
+
@property (nonatomic, assign) CGFloat duration;
|
|
7
|
+
|
|
8
|
+
/// The react tag of the source card this target morphs from.
|
|
9
|
+
/// Used by the module to look up the source view and snapshot it.
|
|
10
|
+
@property (nonatomic, assign) NSInteger sourceTag;
|
|
11
|
+
|
|
12
|
+
/// Event fired when the morph animation completes.
|
|
13
|
+
@property (nonatomic, copy) RCTDirectEventBlock onMorphComplete;
|
|
14
|
+
|
|
15
|
+
/// Returns this view's frame in window coordinates.
|
|
16
|
+
/// The animation needs this to know where the snapshot should land.
|
|
17
|
+
- (CGRect)frameInWindow;
|
|
18
|
+
|
|
19
|
+
@end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#import "RNCMorphCardTarget.h"
|
|
2
|
+
#import "RNCMorphCardViewRegistry.h"
|
|
3
|
+
|
|
4
|
+
@implementation RNCMorphCardTarget
|
|
5
|
+
|
|
6
|
+
- (instancetype)init {
|
|
7
|
+
if (self = [super init]) {
|
|
8
|
+
_duration = 350.0;
|
|
9
|
+
}
|
|
10
|
+
return self;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
- (void)didMoveToWindow {
|
|
14
|
+
[super didMoveToWindow];
|
|
15
|
+
if (self.window) {
|
|
16
|
+
[[RNCMorphCardViewRegistry shared] registerView:self
|
|
17
|
+
withTag:self.tag];
|
|
18
|
+
} else {
|
|
19
|
+
[[RNCMorphCardViewRegistry shared] unregisterViewWithTag:self.tag];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
- (CGRect)frameInWindow {
|
|
24
|
+
return [self convertRect:self.bounds toView:nil];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#import "RNCMorphCardTargetManager.h"
|
|
2
|
+
#import "RNCMorphCardTarget.h"
|
|
3
|
+
|
|
4
|
+
@implementation RNCMorphCardTargetManager
|
|
5
|
+
|
|
6
|
+
RCT_EXPORT_MODULE()
|
|
7
|
+
|
|
8
|
+
- (UIView *)view {
|
|
9
|
+
return [[RNCMorphCardTarget alloc] init];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
RCT_EXPORT_VIEW_PROPERTY(duration, CGFloat)
|
|
13
|
+
RCT_EXPORT_VIEW_PROPERTY(sourceTag, NSInteger)
|
|
14
|
+
RCT_EXPORT_VIEW_PROPERTY(onMorphComplete, RCTDirectEventBlock)
|
|
15
|
+
|
|
16
|
+
@end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#import <UIKit/UIKit.h>
|
|
2
|
+
|
|
3
|
+
// ──────────────────────────────────────────────────────
|
|
4
|
+
// VIEW REGISTRY
|
|
5
|
+
//
|
|
6
|
+
// On Paper (old arch), we could use self.bridge.uiManager to
|
|
7
|
+
// look up any view by its React tag. On Fabric (new arch),
|
|
8
|
+
// there's no bridge — TurboModules are standalone objects.
|
|
9
|
+
//
|
|
10
|
+
// Our solution: a simple singleton registry. When a MorphCardSource
|
|
11
|
+
// or MorphCardTarget mounts, it registers itself here with its
|
|
12
|
+
// React tag. When the module needs to find a view, it looks it up
|
|
13
|
+
// in this registry instead of going through the bridge.
|
|
14
|
+
//
|
|
15
|
+
// NSMapTable with weak values means: if the view gets deallocated
|
|
16
|
+
// (e.g., the screen is removed), the entry automatically becomes nil.
|
|
17
|
+
// No manual cleanup needed — no risk of retain cycles or stale refs.
|
|
18
|
+
// ──────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
@interface RNCMorphCardViewRegistry : NSObject
|
|
21
|
+
|
|
22
|
+
+ (instancetype)shared;
|
|
23
|
+
|
|
24
|
+
/// Register a view with its React tag. Uses weak references
|
|
25
|
+
/// so views are not retained by the registry.
|
|
26
|
+
- (void)registerView:(UIView *)view withTag:(NSInteger)tag;
|
|
27
|
+
|
|
28
|
+
/// Remove a view from the registry (called on unmount).
|
|
29
|
+
- (void)unregisterViewWithTag:(NSInteger)tag;
|
|
30
|
+
|
|
31
|
+
/// Look up a view by its React tag. Returns nil if the view
|
|
32
|
+
/// has been deallocated or was never registered.
|
|
33
|
+
- (UIView *)viewForTag:(NSInteger)tag;
|
|
34
|
+
|
|
35
|
+
@end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#import "RNCMorphCardViewRegistry.h"
|
|
2
|
+
|
|
3
|
+
@implementation RNCMorphCardViewRegistry {
|
|
4
|
+
// NSMapTable lets us use NSNumber keys with weak object values.
|
|
5
|
+
// "weak" means: if the UIView is deallocated, the entry automatically
|
|
6
|
+
// becomes nil — no manual cleanup needed.
|
|
7
|
+
NSMapTable<NSNumber *, UIView *> *_views;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
+ (instancetype)shared {
|
|
11
|
+
static RNCMorphCardViewRegistry *instance;
|
|
12
|
+
static dispatch_once_t onceToken;
|
|
13
|
+
dispatch_once(&onceToken, ^{
|
|
14
|
+
instance = [[RNCMorphCardViewRegistry alloc] init];
|
|
15
|
+
});
|
|
16
|
+
return instance;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
- (instancetype)init {
|
|
20
|
+
if (self = [super init]) {
|
|
21
|
+
// strongToWeakObjects: keys are strong (NSNumber stays alive),
|
|
22
|
+
// values are weak (UIView can be deallocated freely).
|
|
23
|
+
_views = [NSMapTable strongToWeakObjectsMapTable];
|
|
24
|
+
}
|
|
25
|
+
return self;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
- (void)registerView:(UIView *)view withTag:(NSInteger)tag {
|
|
29
|
+
[_views setObject:view forKey:@(tag)];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
- (void)unregisterViewWithTag:(NSInteger)tag {
|
|
33
|
+
[_views removeObjectForKey:@(tag)];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
- (UIView *)viewForTag:(NSInteger)tag {
|
|
37
|
+
return [_views objectForKey:@(tag)];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":[],"sourceRoot":"../../src","sources":["MorphCard.types.ts"],"mappings":"","ignoreList":[]}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.MorphCardSource = void 0;
|
|
7
|
+
exports.getViewTag = getViewTag;
|
|
8
|
+
exports.morphCollapse = morphCollapse;
|
|
9
|
+
exports.morphExpand = morphExpand;
|
|
10
|
+
var React = _interopRequireWildcard(require("react"));
|
|
11
|
+
var _reactNative = require("react-native");
|
|
12
|
+
var _NativeMorphCardModule = _interopRequireDefault(require("./specs/NativeMorphCardModule"));
|
|
13
|
+
var _NativeMorphCardSource = _interopRequireDefault(require("./specs/NativeMorphCardSource"));
|
|
14
|
+
var _jsxRuntime = require("react/jsx-runtime");
|
|
15
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
16
|
+
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
17
|
+
const NativeSourceView = _NativeMorphCardSource.default ?? _reactNative.View;
|
|
18
|
+
const MorphCardSource = ({
|
|
19
|
+
children,
|
|
20
|
+
duration = 300,
|
|
21
|
+
width,
|
|
22
|
+
height,
|
|
23
|
+
borderRadius,
|
|
24
|
+
backgroundColor,
|
|
25
|
+
scaleMode,
|
|
26
|
+
onPress,
|
|
27
|
+
ref
|
|
28
|
+
}) => {
|
|
29
|
+
const nativeRef = React.useRef(null);
|
|
30
|
+
React.useImperativeHandle(ref, () => nativeRef.current);
|
|
31
|
+
const style = {};
|
|
32
|
+
if (width != null) style.width = width;
|
|
33
|
+
if (height != null) style.height = height;
|
|
34
|
+
if (borderRadius != null) {
|
|
35
|
+
style.borderRadius = borderRadius;
|
|
36
|
+
style.overflow = 'hidden';
|
|
37
|
+
}
|
|
38
|
+
if (backgroundColor != null) style.backgroundColor = backgroundColor;
|
|
39
|
+
const handlePress = React.useCallback(() => {
|
|
40
|
+
if (!onPress) return;
|
|
41
|
+
const tag = (0, _reactNative.findNodeHandle)(nativeRef.current);
|
|
42
|
+
if (tag != null) {
|
|
43
|
+
// Create overlay immediately BEFORE navigation to prevent target screen flash
|
|
44
|
+
_NativeMorphCardModule.default.prepareExpand(tag);
|
|
45
|
+
onPress(tag);
|
|
46
|
+
}
|
|
47
|
+
}, [onPress]);
|
|
48
|
+
const content = /*#__PURE__*/(0, _jsxRuntime.jsx)(NativeSourceView, {
|
|
49
|
+
ref: nativeRef,
|
|
50
|
+
duration: duration,
|
|
51
|
+
scaleMode: scaleMode,
|
|
52
|
+
cardBorderRadius: borderRadius,
|
|
53
|
+
style: style,
|
|
54
|
+
children: children
|
|
55
|
+
});
|
|
56
|
+
if (onPress) {
|
|
57
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
|
|
58
|
+
onPress: handlePress,
|
|
59
|
+
children: content
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return content;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get the native view tag from a ref. Useful for passing sourceTag
|
|
67
|
+
* to the detail screen via navigation params.
|
|
68
|
+
*/
|
|
69
|
+
exports.MorphCardSource = MorphCardSource;
|
|
70
|
+
function getViewTag(viewRef) {
|
|
71
|
+
return (0, _reactNative.findNodeHandle)(viewRef.current);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Expand: background grows from card bounds to fullscreen while
|
|
76
|
+
* card snapshot moves to targetRef's position. Content fades in at the end.
|
|
77
|
+
*
|
|
78
|
+
* Call this AFTER navigating to the detail screen (so the target is mounted).
|
|
79
|
+
*/
|
|
80
|
+
async function morphExpand(sourceRef, targetRef) {
|
|
81
|
+
const sourceTag = (0, _reactNative.findNodeHandle)(sourceRef.current);
|
|
82
|
+
const targetTag = (0, _reactNative.findNodeHandle)(targetRef.current);
|
|
83
|
+
if (!sourceTag || !targetTag) return false;
|
|
84
|
+
return _NativeMorphCardModule.default.expand(sourceTag, targetTag);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Collapse: content fades out, background shrinks from fullscreen back
|
|
89
|
+
* to card bounds while card snapshot moves from target back to card position.
|
|
90
|
+
* Uses the target stored from the last expand call.
|
|
91
|
+
*/
|
|
92
|
+
async function morphCollapse(sourceTag) {
|
|
93
|
+
return _NativeMorphCardModule.default.collapse(sourceTag);
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=MorphCardSource.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["React","_interopRequireWildcard","require","_reactNative","_NativeMorphCardModule","_interopRequireDefault","_NativeMorphCardSource","_jsxRuntime","e","__esModule","default","t","WeakMap","r","n","o","i","f","__proto__","has","get","set","hasOwnProperty","call","Object","defineProperty","getOwnPropertyDescriptor","NativeSourceView","NativeSourceViewSpec","View","MorphCardSource","children","duration","width","height","borderRadius","backgroundColor","scaleMode","onPress","ref","nativeRef","useRef","useImperativeHandle","current","style","overflow","handlePress","useCallback","tag","findNodeHandle","NativeMorphCardModule","prepareExpand","content","jsx","cardBorderRadius","Pressable","exports","getViewTag","viewRef","morphExpand","sourceRef","targetRef","sourceTag","targetTag","expand","morphCollapse","collapse"],"sourceRoot":"../../src","sources":["MorphCardSource.tsx"],"mappings":"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","ignoreList":[]}
|