react-native-morph-card 0.2.0 → 0.2.2

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.
@@ -100,124 +100,19 @@ class MorphCardSourceView(context: Context) : ReactViewGroup(context) {
100
100
 
101
101
  // ── Snapshot ──
102
102
 
103
- /**
104
- * Capture a snapshot of the source card's children WITHOUT border radius clipping.
105
- * Like iOS, we want the raw content (e.g. the full rectangular image), not what's
106
- * visually clipped on screen. The border radius is applied separately during animation.
107
- *
108
- * This disables both Android's clipToOutline AND Fresco's RoundingParams (used by
109
- * React Native's Image component) to capture the full rectangular content.
110
- */
111
103
  private fun captureSnapshot(): Bitmap {
112
104
  val w = width
113
105
  val h = height
114
106
  val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
115
107
  val canvas = Canvas(bitmap)
116
-
117
- // Track Fresco views to restore rounding after capture
118
- data class FrescoState(val view: View, val hierarchy: Any, val roundingParams: Any)
119
- val frescoStates = mutableListOf<FrescoState>()
120
- // Track views whose clipToOutline was disabled
121
- val clippedViews = mutableListOf<View>()
122
-
123
- // Map of Fresco views to their hierarchy (for drawing drawable directly)
124
- val frescoViews = mutableMapOf<View, Any>()
125
-
126
- fun prepareView(view: View) {
127
- // Disable outline clipping
128
- if (view.clipToOutline) {
129
- clippedViews.add(view)
130
- view.clipToOutline = false
131
- }
132
-
133
- // Detect Fresco DraweeView and disable rounding
134
- try {
135
- val getHierarchy = view.javaClass.getMethod("getHierarchy")
136
- val hierarchy = getHierarchy.invoke(view)
137
- if (hierarchy != null) {
138
- val getRounding = hierarchy.javaClass.getMethod("getRoundingParams")
139
- val rounding = getRounding.invoke(hierarchy)
140
- if (rounding != null) {
141
- val roundingClass = Class.forName("com.facebook.drawee.generic.RoundingParams")
142
- val setRounding = hierarchy.javaClass.getMethod("setRoundingParams", roundingClass)
143
- setRounding.invoke(hierarchy, null)
144
- frescoStates.add(FrescoState(view, hierarchy, rounding))
145
- frescoViews[view] = hierarchy
146
- Log.d(TAG, "captureSnapshot: disabled Fresco rounding on ${view.width}x${view.height}")
147
- }
148
- }
149
- } catch (_: Exception) {}
150
-
151
- if (view is ViewGroup) {
152
- for (i in 0 until view.childCount) {
153
- prepareView(view.getChildAt(i))
154
- }
155
- }
156
- }
157
-
158
- for (i in 0 until childCount) {
159
- prepareView(getChildAt(i))
160
- }
161
-
162
- // Draw children. For Fresco views, draw the top-level drawable directly
163
- // (since the internal drawable is rebuilt when roundingParams changes).
164
- fun drawView(view: View, c: Canvas) {
165
- val hierarchy = frescoViews[view]
166
- if (hierarchy != null) {
167
- try {
168
- val getTopDrawable = hierarchy.javaClass.getMethod("getTopLevelDrawable")
169
- val drawable = getTopDrawable.invoke(hierarchy) as? android.graphics.drawable.Drawable
170
- if (drawable != null) {
171
- drawable.setBounds(0, 0, view.width, view.height)
172
- drawable.invalidateSelf()
173
- drawable.draw(c)
174
- Log.d(TAG, "captureSnapshot: drew Fresco drawable directly ${view.width}x${view.height}")
175
- return
176
- }
177
- } catch (_: Exception) {}
178
- }
179
-
180
- if (view is ViewGroup) {
181
- // Draw background
182
- view.background?.let { bg ->
183
- bg.setBounds(0, 0, view.width, view.height)
184
- bg.draw(c)
185
- }
186
- for (i in 0 until view.childCount) {
187
- val child = view.getChildAt(i)
188
- if (child.visibility != VISIBLE) continue
189
- c.save()
190
- c.translate(child.left.toFloat(), child.top.toFloat())
191
- drawView(child, c)
192
- c.restore()
193
- }
194
- } else {
195
- view.draw(c)
196
- }
197
- }
198
-
199
108
  for (i in 0 until childCount) {
200
109
  val child = getChildAt(i)
201
110
  if (child.visibility != VISIBLE) continue
202
111
  canvas.save()
203
112
  canvas.translate(child.left.toFloat(), child.top.toFloat())
204
- drawView(child, canvas)
113
+ child.draw(canvas)
205
114
  canvas.restore()
206
115
  }
207
-
208
- // Restore clipToOutline
209
- for (view in clippedViews) {
210
- view.clipToOutline = true
211
- }
212
- // Restore Fresco rounding params
213
- for (state in frescoStates) {
214
- try {
215
- val roundingClass = Class.forName("com.facebook.drawee.generic.RoundingParams")
216
- val setRounding = state.hierarchy.javaClass.getMethod("setRoundingParams", roundingClass)
217
- setRounding.invoke(state.hierarchy, state.roundingParams)
218
- } catch (_: Exception) {}
219
- }
220
-
221
116
  return bitmap
222
117
  }
223
118
 
@@ -738,7 +633,7 @@ class MorphCardSourceView(context: Context) : ReactViewGroup(context) {
738
633
  this@MorphCardSourceView.alpha = 1f
739
634
 
740
635
  transferSnapshotToTarget(decorView, wrapper, targetView,
741
- targetWidthPx, targetHeightPx, targetCornerRadiusPx)
636
+ targetWidthPx, targetHeightPx, targetCornerRadiusPx, 200L)
742
637
 
743
638
  promise.resolve(true)
744
639
  }
@@ -759,7 +654,8 @@ class MorphCardSourceView(context: Context) : ReactViewGroup(context) {
759
654
  targetView: View?,
760
655
  targetWidthPx: Float,
761
656
  targetHeightPx: Float,
762
- cornerRadius: Float
657
+ cornerRadius: Float,
658
+ fadeDuration: Long = 100
763
659
  ) {
764
660
  val target = targetView as? MorphCardTargetView
765
661
  if (target == null) {
@@ -802,9 +698,19 @@ class MorphCardSourceView(context: Context) : ReactViewGroup(context) {
802
698
  Log.d(TAG, "transferSnapshot: handed snapshot to MorphCardTargetView")
803
699
  }
804
700
 
805
- decorView.removeView(overlay)
806
- overlayContainer = null
807
- Log.d(TAG, "transferSnapshot: overlay removed")
701
+ val fadeOut = ValueAnimator.ofFloat(1f, 0f)
702
+ fadeOut.duration = fadeDuration
703
+ fadeOut.addUpdateListener { anim ->
704
+ overlay.alpha = anim.animatedValue as Float
705
+ }
706
+ fadeOut.addListener(object : android.animation.AnimatorListenerAdapter() {
707
+ override fun onAnimationEnd(animation: android.animation.Animator) {
708
+ decorView.removeView(overlay)
709
+ overlayContainer = null
710
+ Log.d(TAG, "transferSnapshot: overlay fade-out complete")
711
+ }
712
+ })
713
+ fadeOut.start()
808
714
  }
809
715
 
810
716
  // ══════════════════════════════════════════════════════════════
@@ -128,54 +128,26 @@ static CGRect imageFrameForScaleMode(UIViewContentMode mode,
128
128
  #pragma mark - Snapshot helper
129
129
 
130
130
  - (UIImage *)captureSnapshot {
131
- // Temporarily disable clipsToBounds + cornerRadius on all descendants so we
132
- // capture full rectangular content (like iOS does for the source layer itself).
133
- // This mirrors the Android fix that disables Fresco's RoundingParams.
134
- NSMutableArray<NSDictionary *> *clippedViews = [NSMutableArray array];
135
- __block void (^prepareView)(UIView *);
136
- prepareView = ^(UIView *view) {
137
- if (view.clipsToBounds && view.layer.cornerRadius > 0) {
138
- [clippedViews addObject:@{
139
- @"view": view,
140
- @"radius": @(view.layer.cornerRadius)
141
- }];
142
- view.layer.cornerRadius = 0;
143
- view.clipsToBounds = NO;
144
- }
145
- for (UIView *child in view.subviews) {
146
- prepareView(child);
147
- }
148
- };
149
- for (UIView *child in self.subviews) {
150
- prepareView(child);
151
- }
152
-
131
+ // Render children directly into a fresh context so the snapshot
132
+ // captures full rectangular content without the source view's
133
+ // cornerRadius clipping. No on-screen flash since we never
134
+ // modify visible properties.
153
135
  UIGraphicsImageRendererFormat *format =
154
136
  [UIGraphicsImageRendererFormat defaultFormat];
155
137
  format.opaque = NO;
156
138
  CGSize size = self.bounds.size;
157
139
  UIGraphicsImageRenderer *renderer =
158
140
  [[UIGraphicsImageRenderer alloc] initWithSize:size format:format];
159
- UIImage *image =
160
- [renderer imageWithActions:^(UIGraphicsImageRendererContext *ctx) {
161
- for (UIView *child in self.subviews) {
162
- CGContextSaveGState(ctx.CGContext);
163
- CGContextTranslateCTM(ctx.CGContext, child.frame.origin.x,
164
- child.frame.origin.y);
165
- [child drawViewHierarchyInRect:(CGRect){CGPointZero, child.frame.size}
166
- afterScreenUpdates:NO];
167
- CGContextRestoreGState(ctx.CGContext);
168
- }
169
- }];
170
-
171
- // Restore clipping
172
- for (NSDictionary *entry in clippedViews) {
173
- UIView *view = entry[@"view"];
174
- view.layer.cornerRadius = [entry[@"radius"] floatValue];
175
- view.clipsToBounds = YES;
176
- }
177
-
178
- return image;
141
+ return [renderer imageWithActions:^(UIGraphicsImageRendererContext *ctx) {
142
+ for (UIView *child in self.subviews) {
143
+ CGContextSaveGState(ctx.CGContext);
144
+ CGContextTranslateCTM(ctx.CGContext, child.frame.origin.x,
145
+ child.frame.origin.y);
146
+ [child drawViewHierarchyInRect:(CGRect){CGPointZero, child.frame.size}
147
+ afterScreenUpdates:NO];
148
+ CGContextRestoreGState(ctx.CGContext);
149
+ }
150
+ }];
179
151
  }
180
152
 
181
153
  #pragma mark - Expand
@@ -470,22 +442,25 @@ static CGRect imageFrameForScaleMode(UIViewContentMode mode,
470
442
 
471
443
  UIView *content = wrapper.subviews.firstObject;
472
444
 
445
+ UICubicTimingParameters *timing = [[UICubicTimingParameters alloc]
446
+ initWithControlPoint1:CGPointMake(0.25, 1.0)
447
+ controlPoint2:CGPointMake(0.5, 1.0)];
473
448
  UIViewPropertyAnimator *animator = [[UIViewPropertyAnimator alloc]
474
449
  initWithDuration:dur
475
- dampingRatio:0.85
476
- animations:^{
477
- wrapper.frame = self->_cardFrame;
478
- wrapper.layer.cornerRadius = self->_cardCornerRadius;
479
- if (content) {
480
- content.frame = (CGRect){CGPointZero, content.frame.size};
481
- }
482
- }];
450
+ timingParameters:timing];
451
+ [animator addAnimations:^{
452
+ wrapper.frame = self->_cardFrame;
453
+ wrapper.layer.cornerRadius = self->_cardCornerRadius;
454
+ if (content) {
455
+ content.frame = (CGRect){CGPointZero, content.frame.size};
456
+ }
457
+ }];
483
458
 
484
- // Fade out target screen concurrently at 15% (mirrors expand's fade-in)
459
+ // Fade out target screen concurrently at 15%, over 65% of duration
485
460
  dispatch_after(
486
461
  dispatch_time(DISPATCH_TIME_NOW, (int64_t)(dur * 0.15 * NSEC_PER_SEC)),
487
462
  dispatch_get_main_queue(), ^{
488
- [UIView animateWithDuration:dur * 0.5
463
+ [UIView animateWithDuration:dur * 0.65
489
464
  animations:^{
490
465
  if (targetScreen) { targetScreen.alpha = 0; }
491
466
  }
@@ -549,20 +524,23 @@ static CGRect imageFrameForScaleMode(UIViewContentMode mode,
549
524
  sourceScreen.alpha = 1;
550
525
  }
551
526
 
527
+ UICubicTimingParameters *timing2 = [[UICubicTimingParameters alloc]
528
+ initWithControlPoint1:CGPointMake(0.25, 1.0)
529
+ controlPoint2:CGPointMake(0.5, 1.0)];
552
530
  UIViewPropertyAnimator *animator = [[UIViewPropertyAnimator alloc]
553
531
  initWithDuration:dur
554
- dampingRatio:0.85
555
- animations:^{
556
- container.frame = self->_cardFrame;
557
- container.layer.cornerRadius = self->_cardCornerRadius;
558
- snapshot.frame = (CGRect){CGPointZero, self->_cardFrame.size};
559
- }];
532
+ timingParameters:timing2];
533
+ [animator addAnimations:^{
534
+ container.frame = self->_cardFrame;
535
+ container.layer.cornerRadius = self->_cardCornerRadius;
536
+ snapshot.frame = (CGRect){CGPointZero, self->_cardFrame.size};
537
+ }];
560
538
 
561
- // Fade out target screen concurrently at 15% (mirrors expand's fade-in)
539
+ // Fade out target screen concurrently at 15%, over 65% of duration
562
540
  dispatch_after(
563
541
  dispatch_time(DISPATCH_TIME_NOW, (int64_t)(dur * 0.15 * NSEC_PER_SEC)),
564
542
  dispatch_get_main_queue(), ^{
565
- [UIView animateWithDuration:dur * 0.5
543
+ [UIView animateWithDuration:dur * 0.65
566
544
  animations:^{
567
545
  if (targetScreen) { targetScreen.alpha = 0; }
568
546
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-morph-card",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Native card-to-modal morph transition for React Native. iOS App Store-style expand animation.",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",