react-native-morph-card 0.2.0 → 0.2.1

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
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.1",
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",