react-native-morph-card 0.1.14 → 0.2.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.
@@ -114,55 +114,86 @@ class MorphCardSourceView(context: Context) : ReactViewGroup(context) {
114
114
  val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
115
115
  val canvas = Canvas(bitmap)
116
116
 
117
- // Track state to restore: (view, hadClipToOutline, savedRoundingParams)
118
- data class ViewState(val view: View, val hadClip: Boolean, val roundingParams: Any?)
119
- val savedStates = mutableListOf<ViewState>()
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>()
120
122
 
121
- fun disableClipping(view: View) {
122
- val hadClip = view.clipToOutline
123
- var savedRounding: Any? = null
124
-
125
- Log.d(TAG, "captureSnapshot: visiting ${view.javaClass.name} clipToOutline=$hadClip size=${view.width}x${view.height}")
123
+ // Map of Fresco views to their hierarchy (for drawing drawable directly)
124
+ val frescoViews = mutableMapOf<View, Any>()
126
125
 
126
+ fun prepareView(view: View) {
127
127
  // Disable outline clipping
128
- if (hadClip) {
128
+ if (view.clipToOutline) {
129
+ clippedViews.add(view)
129
130
  view.clipToOutline = false
130
131
  }
131
132
 
132
- // Disable Fresco RoundingParams on DraweeView (React Native Image components).
133
- // Uses reflection to avoid a compile-time dependency on Fresco.
133
+ // Detect Fresco DraweeView and disable rounding
134
134
  try {
135
135
  val getHierarchy = view.javaClass.getMethod("getHierarchy")
136
136
  val hierarchy = getHierarchy.invoke(view)
137
137
  if (hierarchy != null) {
138
- Log.d(TAG, "captureSnapshot: found hierarchy ${hierarchy.javaClass.name}")
139
138
  val getRounding = hierarchy.javaClass.getMethod("getRoundingParams")
140
- savedRounding = getRounding.invoke(hierarchy)
141
- Log.d(TAG, "captureSnapshot: roundingParams=$savedRounding")
142
- if (savedRounding != null) {
139
+ val rounding = getRounding.invoke(hierarchy)
140
+ if (rounding != null) {
143
141
  val roundingClass = Class.forName("com.facebook.drawee.generic.RoundingParams")
144
142
  val setRounding = hierarchy.javaClass.getMethod("setRoundingParams", roundingClass)
145
143
  setRounding.invoke(hierarchy, null)
146
- Log.d(TAG, "captureSnapshot: disabled Fresco rounding")
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
147
  }
148
148
  }
149
- } catch (e: Exception) {
150
- Log.d(TAG, "captureSnapshot: reflection skip for ${view.javaClass.simpleName}: ${e.javaClass.simpleName}")
151
- }
152
-
153
- if (hadClip || savedRounding != null) {
154
- savedStates.add(ViewState(view, hadClip, savedRounding))
155
- }
149
+ } catch (_: Exception) {}
156
150
 
157
151
  if (view is ViewGroup) {
158
152
  for (i in 0 until view.childCount) {
159
- disableClipping(view.getChildAt(i))
153
+ prepareView(view.getChildAt(i))
160
154
  }
161
155
  }
162
156
  }
163
157
 
164
158
  for (i in 0 until childCount) {
165
- disableClipping(getChildAt(i))
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
+ }
166
197
  }
167
198
 
168
199
  for (i in 0 until childCount) {
@@ -170,26 +201,21 @@ class MorphCardSourceView(context: Context) : ReactViewGroup(context) {
170
201
  if (child.visibility != VISIBLE) continue
171
202
  canvas.save()
172
203
  canvas.translate(child.left.toFloat(), child.top.toFloat())
173
- child.draw(canvas)
204
+ drawView(child, canvas)
174
205
  canvas.restore()
175
206
  }
176
207
 
177
- // Restore all view states
178
- for (state in savedStates) {
179
- if (state.hadClip) {
180
- state.view.clipToOutline = true
181
- }
182
- if (state.roundingParams != null) {
183
- try {
184
- val getHierarchy = state.view.javaClass.getMethod("getHierarchy")
185
- val hierarchy = getHierarchy.invoke(state.view)
186
- if (hierarchy != null) {
187
- val roundingClass = Class.forName("com.facebook.drawee.generic.RoundingParams")
188
- val setRounding = hierarchy.javaClass.getMethod("setRoundingParams", roundingClass)
189
- setRounding.invoke(hierarchy, state.roundingParams)
190
- }
191
- } catch (_: Exception) {}
192
- }
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) {}
193
219
  }
194
220
 
195
221
  return bitmap
@@ -712,7 +738,7 @@ class MorphCardSourceView(context: Context) : ReactViewGroup(context) {
712
738
  this@MorphCardSourceView.alpha = 1f
713
739
 
714
740
  transferSnapshotToTarget(decorView, wrapper, targetView,
715
- targetWidthPx, targetHeightPx, targetCornerRadiusPx, 200L)
741
+ targetWidthPx, targetHeightPx, targetCornerRadiusPx)
716
742
 
717
743
  promise.resolve(true)
718
744
  }
@@ -733,8 +759,7 @@ class MorphCardSourceView(context: Context) : ReactViewGroup(context) {
733
759
  targetView: View?,
734
760
  targetWidthPx: Float,
735
761
  targetHeightPx: Float,
736
- cornerRadius: Float,
737
- fadeDuration: Long = 100
762
+ cornerRadius: Float
738
763
  ) {
739
764
  val target = targetView as? MorphCardTargetView
740
765
  if (target == null) {
@@ -777,19 +802,9 @@ class MorphCardSourceView(context: Context) : ReactViewGroup(context) {
777
802
  Log.d(TAG, "transferSnapshot: handed snapshot to MorphCardTargetView")
778
803
  }
779
804
 
780
- val fadeOut = ValueAnimator.ofFloat(1f, 0f)
781
- fadeOut.duration = fadeDuration
782
- fadeOut.addUpdateListener { anim ->
783
- overlay.alpha = anim.animatedValue as Float
784
- }
785
- fadeOut.addListener(object : android.animation.AnimatorListenerAdapter() {
786
- override fun onAnimationEnd(animation: android.animation.Animator) {
787
- decorView.removeView(overlay)
788
- overlayContainer = null
789
- Log.d(TAG, "transferSnapshot: overlay fade-out complete")
790
- }
791
- })
792
- fadeOut.start()
805
+ decorView.removeView(overlay)
806
+ overlayContainer = null
807
+ Log.d(TAG, "transferSnapshot: overlay removed")
793
808
  }
794
809
 
795
810
  // ══════════════════════════════════════════════════════════════
@@ -128,25 +128,54 @@ static CGRect imageFrameForScaleMode(UIViewContentMode mode,
128
128
  #pragma mark - Snapshot helper
129
129
 
130
130
  - (UIImage *)captureSnapshot {
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.
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
+
135
153
  UIGraphicsImageRendererFormat *format =
136
154
  [UIGraphicsImageRendererFormat defaultFormat];
137
155
  format.opaque = NO;
138
156
  CGSize size = self.bounds.size;
139
157
  UIGraphicsImageRenderer *renderer =
140
158
  [[UIGraphicsImageRenderer alloc] initWithSize:size format:format];
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, child.frame.origin.y);
145
- [child drawViewHierarchyInRect:(CGRect){CGPointZero, child.frame.size}
146
- afterScreenUpdates:NO];
147
- CGContextRestoreGState(ctx.CGContext);
148
- }
149
- }];
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;
150
179
  }
151
180
 
152
181
  #pragma mark - Expand
@@ -258,9 +287,7 @@ static CGRect imageFrameForScaleMode(UIViewContentMode mode,
258
287
  UIView *ts = self->_targetScreenContainer;
259
288
  if (ts) {
260
289
  [UIView animateWithDuration:dur * 0.5
261
- animations:^{
262
- ts.alpha = 1;
263
- }
290
+ animations:^{ ts.alpha = 1; }
264
291
  completion:nil];
265
292
  }
266
293
  });
@@ -284,14 +311,8 @@ static CGRect imageFrameForScaleMode(UIViewContentMode mode,
284
311
  self.alpha = 1;
285
312
  UIView *ts = self->_targetScreenContainer;
286
313
  if (ts) { ts.alpha = 1; }
287
- [UIView animateWithDuration:0.2
288
- animations:^{
289
- wrapper.alpha = 0;
290
- }
291
- completion:^(BOOL finished) {
292
- [wrapper removeFromSuperview];
293
- self->_wrapperView = nil;
294
- }];
314
+ [wrapper removeFromSuperview];
315
+ self->_wrapperView = nil;
295
316
  resolve(@(YES));
296
317
  }];
297
318
 
@@ -345,9 +366,7 @@ static CGRect imageFrameForScaleMode(UIViewContentMode mode,
345
366
  UIView *ts = self->_targetScreenContainer;
346
367
  if (ts) {
347
368
  [UIView animateWithDuration:dur * 0.5
348
- animations:^{
349
- ts.alpha = 1;
350
- }
369
+ animations:^{ ts.alpha = 1; }
351
370
  completion:nil];
352
371
  }
353
372
  });
@@ -365,15 +384,9 @@ static CGRect imageFrameForScaleMode(UIViewContentMode mode,
365
384
  self.alpha = 1;
366
385
  UIView *ts = self->_targetScreenContainer;
367
386
  if (ts) { ts.alpha = 1; }
368
- [UIView animateWithDuration:0.2
369
- animations:^{
370
- container.alpha = 0;
371
- }
372
- completion:^(BOOL finished) {
373
- [container removeFromSuperview];
374
- self->_wrapperView = nil;
375
- self->_snapshot = nil;
376
- }];
387
+ [container removeFromSuperview];
388
+ self->_wrapperView = nil;
389
+ self->_snapshot = nil;
377
390
  resolve(@(YES));
378
391
  }];
379
392
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-morph-card",
3
- "version": "0.1.14",
3
+ "version": "0.2.0",
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",