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
|
-
|
|
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
|
-
|
|
806
|
-
|
|
807
|
-
|
|
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
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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