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,755 @@
|
|
|
1
|
+
package com.melivalesca.morphcard
|
|
2
|
+
|
|
3
|
+
import android.animation.ValueAnimator
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.graphics.Bitmap
|
|
6
|
+
import android.graphics.Canvas
|
|
7
|
+
import android.graphics.Color
|
|
8
|
+
import android.graphics.Outline
|
|
9
|
+
import android.graphics.RectF
|
|
10
|
+
import android.graphics.drawable.ColorDrawable
|
|
11
|
+
import android.os.Handler
|
|
12
|
+
import android.os.Looper
|
|
13
|
+
import android.util.Log
|
|
14
|
+
import android.view.View
|
|
15
|
+
import android.view.ViewGroup
|
|
16
|
+
import android.view.ViewOutlineProvider
|
|
17
|
+
import android.view.animation.PathInterpolator
|
|
18
|
+
import android.widget.FrameLayout
|
|
19
|
+
import android.widget.ImageView
|
|
20
|
+
import com.facebook.react.bridge.Promise
|
|
21
|
+
import com.facebook.react.views.view.ReactViewGroup
|
|
22
|
+
import java.lang.ref.WeakReference
|
|
23
|
+
import kotlin.math.max
|
|
24
|
+
import kotlin.math.min
|
|
25
|
+
|
|
26
|
+
class MorphCardSourceView(context: Context) : ReactViewGroup(context) {
|
|
27
|
+
|
|
28
|
+
// ── Props (all in dp) ──
|
|
29
|
+
var duration: Double = 500.0
|
|
30
|
+
var scaleMode: String = "aspectFill"
|
|
31
|
+
var borderRadiusDp: Float = 0f
|
|
32
|
+
|
|
33
|
+
// ── Target config (set by module, in dp from JS) ──
|
|
34
|
+
var pendingTargetWidth: Float = 0f
|
|
35
|
+
var pendingTargetHeight: Float = 0f
|
|
36
|
+
var pendingTargetBorderRadius: Float = -1f
|
|
37
|
+
var pendingContentOffsetY: Float = 0f
|
|
38
|
+
var pendingContentCentered: Boolean = false
|
|
39
|
+
|
|
40
|
+
// ── Internal state (all in px) ──
|
|
41
|
+
var isExpanded = false
|
|
42
|
+
private set
|
|
43
|
+
private var hasWrapper = false
|
|
44
|
+
private var cardLeft = 0f
|
|
45
|
+
private var cardTop = 0f
|
|
46
|
+
private var cardWidth = 0f
|
|
47
|
+
private var cardHeight = 0f
|
|
48
|
+
private var cardCornerRadiusPx = 0f
|
|
49
|
+
private var cardBgColor: Int? = null
|
|
50
|
+
private var targetViewRef: View? = null
|
|
51
|
+
private var overlayContainer: FrameLayout? = null
|
|
52
|
+
val hasOverlay: Boolean get() = overlayContainer != null
|
|
53
|
+
private var sourceScreenContainerRef: WeakReference<View>? = null
|
|
54
|
+
private var targetScreenContainerRef: WeakReference<View>? = null
|
|
55
|
+
private var screenStackRef: WeakReference<ViewGroup>? = null
|
|
56
|
+
private var hierarchyListener: ViewGroup.OnHierarchyChangeListener? = null
|
|
57
|
+
|
|
58
|
+
// Spring-like interpolator (approximates iOS dampingRatio:0.85)
|
|
59
|
+
private val springInterpolator = PathInterpolator(0.25f, 1.0f, 0.5f, 1.0f)
|
|
60
|
+
|
|
61
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
62
|
+
|
|
63
|
+
private val density: Float
|
|
64
|
+
get() = resources.displayMetrics.density
|
|
65
|
+
|
|
66
|
+
override fun onAttachedToWindow() {
|
|
67
|
+
super.onAttachedToWindow()
|
|
68
|
+
MorphCardViewRegistry.register(this, id)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
override fun onDetachedFromWindow() {
|
|
72
|
+
super.onDetachedFromWindow()
|
|
73
|
+
MorphCardViewRegistry.unregister(id)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
77
|
+
super.onLayout(changed, left, top, right, bottom)
|
|
78
|
+
applyBorderRadiusClipping()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private fun applyBorderRadiusClipping() {
|
|
82
|
+
val radiusPx = if (borderRadiusDp > 0f) borderRadiusDp * density else 0f
|
|
83
|
+
if (radiusPx > 0f) {
|
|
84
|
+
clipToOutline = true
|
|
85
|
+
outlineProvider = object : ViewOutlineProvider() {
|
|
86
|
+
override fun getOutline(v: View, outline: Outline) {
|
|
87
|
+
outline.setRoundRect(0, 0, v.width, v.height, radiusPx)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
clipToOutline = false
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Snapshot ──
|
|
96
|
+
|
|
97
|
+
private fun captureSnapshot(): Bitmap {
|
|
98
|
+
val w = width
|
|
99
|
+
val h = height
|
|
100
|
+
val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
|
|
101
|
+
val canvas = Canvas(bitmap)
|
|
102
|
+
for (i in 0 until childCount) {
|
|
103
|
+
val child = getChildAt(i)
|
|
104
|
+
if (child.visibility != VISIBLE) continue
|
|
105
|
+
canvas.save()
|
|
106
|
+
canvas.translate(child.left.toFloat(), child.top.toFloat())
|
|
107
|
+
child.draw(canvas)
|
|
108
|
+
canvas.restore()
|
|
109
|
+
}
|
|
110
|
+
return bitmap
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── Helpers ──
|
|
114
|
+
|
|
115
|
+
private fun getLocationInWindow(view: View): IntArray {
|
|
116
|
+
val loc = IntArray(2)
|
|
117
|
+
view.getLocationInWindow(loc)
|
|
118
|
+
return loc
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Find the screen container for a view (ScreensCoordinatorLayout).
|
|
123
|
+
* Walks up until parent is ScreenStack/ScreenContainer.
|
|
124
|
+
*/
|
|
125
|
+
private fun findScreenContainer(view: View?): View? {
|
|
126
|
+
if (view == null) return null
|
|
127
|
+
var current: View? = view
|
|
128
|
+
while (current != null) {
|
|
129
|
+
val parent = current.parent
|
|
130
|
+
if (parent is ViewGroup) {
|
|
131
|
+
val parentName = parent.javaClass.name
|
|
132
|
+
if (parentName.contains("ScreenStack") || parentName.contains("ScreenContainer")) {
|
|
133
|
+
return current
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
current = if (current.parent is View) current.parent as View else null
|
|
137
|
+
}
|
|
138
|
+
return null
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
private fun extractBackgroundColor(): Int? {
|
|
143
|
+
val bg = background ?: return null
|
|
144
|
+
if (bg is ColorDrawable) return bg.color
|
|
145
|
+
try {
|
|
146
|
+
val clazz = bg.javaClass
|
|
147
|
+
try {
|
|
148
|
+
val bgField = clazz.getDeclaredField("background")
|
|
149
|
+
bgField.isAccessible = true
|
|
150
|
+
val bgDrawable = bgField.get(bg)
|
|
151
|
+
if (bgDrawable != null) {
|
|
152
|
+
val colorField = bgDrawable.javaClass.getDeclaredField("backgroundColor")
|
|
153
|
+
colorField.isAccessible = true
|
|
154
|
+
val color = colorField.getInt(bgDrawable)
|
|
155
|
+
if (Color.alpha(color) > 3) return color
|
|
156
|
+
}
|
|
157
|
+
} catch (_: Exception) {}
|
|
158
|
+
try {
|
|
159
|
+
val cssField = clazz.getDeclaredField("cssBackground")
|
|
160
|
+
cssField.isAccessible = true
|
|
161
|
+
val cssBg = cssField.get(bg)
|
|
162
|
+
if (cssBg != null) {
|
|
163
|
+
val colorField = cssBg.javaClass.getDeclaredField("mColor")
|
|
164
|
+
colorField.isAccessible = true
|
|
165
|
+
val color = colorField.getInt(cssBg)
|
|
166
|
+
if (Color.alpha(color) > 3) return color
|
|
167
|
+
}
|
|
168
|
+
} catch (_: Exception) {}
|
|
169
|
+
} catch (_: Exception) {}
|
|
170
|
+
return null
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private fun getDecorView(): ViewGroup? {
|
|
174
|
+
var v: View = this
|
|
175
|
+
while (v.parent is View) {
|
|
176
|
+
v = v.parent as View
|
|
177
|
+
}
|
|
178
|
+
return v as? ViewGroup
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private fun getCornerRadiusPx(): Float {
|
|
182
|
+
if (borderRadiusDp > 0f) return borderRadiusDp * density
|
|
183
|
+
return 0f
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private fun imageFrameForScaleMode(
|
|
187
|
+
mode: String,
|
|
188
|
+
imageWidth: Float,
|
|
189
|
+
imageHeight: Float,
|
|
190
|
+
containerWidth: Float,
|
|
191
|
+
containerHeight: Float
|
|
192
|
+
): RectF {
|
|
193
|
+
return when (mode) {
|
|
194
|
+
"aspectFit" -> {
|
|
195
|
+
val scale = min(containerWidth / imageWidth, containerHeight / imageHeight)
|
|
196
|
+
val w = imageWidth * scale
|
|
197
|
+
val h = imageHeight * scale
|
|
198
|
+
RectF((containerWidth - w) / 2f, (containerHeight - h) / 2f,
|
|
199
|
+
(containerWidth + w) / 2f, (containerHeight + h) / 2f)
|
|
200
|
+
}
|
|
201
|
+
"stretch" -> RectF(0f, 0f, containerWidth, containerHeight)
|
|
202
|
+
else -> {
|
|
203
|
+
val scale = max(containerWidth / imageWidth, containerHeight / imageHeight)
|
|
204
|
+
val w = imageWidth * scale
|
|
205
|
+
val h = imageHeight * scale
|
|
206
|
+
RectF((containerWidth - w) / 2f, (containerHeight - h) / 2f,
|
|
207
|
+
(containerWidth + w) / 2f, (containerHeight + h) / 2f)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private fun setRoundedCorners(view: View, radiusPx: Float) {
|
|
213
|
+
if (radiusPx <= 0f) {
|
|
214
|
+
view.clipToOutline = false
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
view.clipToOutline = true
|
|
218
|
+
view.outlineProvider = object : ViewOutlineProvider() {
|
|
219
|
+
override fun getOutline(v: View, outline: Outline) {
|
|
220
|
+
outline.setRoundRect(0, 0, v.width, v.height, radiusPx)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private fun removeHierarchyListener() {
|
|
226
|
+
screenStackRef?.get()?.setOnHierarchyChangeListener(null)
|
|
227
|
+
screenStackRef = null
|
|
228
|
+
hierarchyListener = null
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ══════════════════════════════════════════════════════════════
|
|
232
|
+
// PHASE 1: prepareExpand — called IMMEDIATELY, before delay
|
|
233
|
+
// Creates overlay at source position + hides target screen.
|
|
234
|
+
// ══════════════════════════════════════════════════════════════
|
|
235
|
+
|
|
236
|
+
fun prepareExpand(targetView: View?) {
|
|
237
|
+
Log.d(TAG, "=== prepareExpand START === isExpanded=$isExpanded targetView=$targetView targetView.id=${targetView?.id}")
|
|
238
|
+
if (isExpanded) {
|
|
239
|
+
Log.d(TAG, "prepareExpand: SKIPPED — already expanded")
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
val decorView = getDecorView() ?: return
|
|
244
|
+
|
|
245
|
+
// Clean up any stale overlay from a previous cycle
|
|
246
|
+
overlayContainer?.let { stale ->
|
|
247
|
+
Log.d(TAG, "prepareExpand: removing stale overlay")
|
|
248
|
+
decorView.removeView(stale)
|
|
249
|
+
overlayContainer = null
|
|
250
|
+
}
|
|
251
|
+
// Clear snapshot from previous target view if any
|
|
252
|
+
(targetViewRef as? MorphCardTargetView)?.clearSnapshot()
|
|
253
|
+
|
|
254
|
+
targetViewRef = targetView
|
|
255
|
+
cardBgColor = extractBackgroundColor()
|
|
256
|
+
hasWrapper = cardBgColor != null
|
|
257
|
+
|
|
258
|
+
// Save card geometry
|
|
259
|
+
val loc = getLocationInWindow(this)
|
|
260
|
+
cardLeft = loc[0].toFloat()
|
|
261
|
+
cardTop = loc[1].toFloat()
|
|
262
|
+
cardWidth = width.toFloat()
|
|
263
|
+
cardHeight = height.toFloat()
|
|
264
|
+
cardCornerRadiusPx = getCornerRadiusPx()
|
|
265
|
+
Log.d(TAG, "prepareExpand: source card=[${cardLeft},${cardTop},${cardWidth}x${cardHeight}] cornerR=$cardCornerRadiusPx hasWrapper=$hasWrapper")
|
|
266
|
+
|
|
267
|
+
// Find screen containers
|
|
268
|
+
val sourceScreen = findScreenContainer(this)
|
|
269
|
+
val targetScreen = findScreenContainer(targetView)
|
|
270
|
+
sourceScreenContainerRef = if (sourceScreen != null) WeakReference(sourceScreen) else null
|
|
271
|
+
targetScreenContainerRef = if (targetScreen != null) WeakReference(targetScreen) else null
|
|
272
|
+
|
|
273
|
+
// Hide target screen with INVISIBLE (can't be overridden by alpha resets)
|
|
274
|
+
if (targetScreen != null && targetScreen !== sourceScreen) {
|
|
275
|
+
targetScreen.visibility = View.INVISIBLE
|
|
276
|
+
Log.d(TAG, "prepareExpand: set target screen INVISIBLE")
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Watch the ScreenStack for new screens being added and hide them
|
|
280
|
+
// immediately. This catches the target screen BEFORE it renders,
|
|
281
|
+
// even before MorphCardTargetView.onAttachedToWindow fires.
|
|
282
|
+
removeHierarchyListener()
|
|
283
|
+
val screenStack = sourceScreen?.parent as? ViewGroup
|
|
284
|
+
if (screenStack != null) {
|
|
285
|
+
val listener = object : ViewGroup.OnHierarchyChangeListener {
|
|
286
|
+
override fun onChildViewAdded(parent: View?, child: View?) {
|
|
287
|
+
if (child != null && child !== sourceScreen) {
|
|
288
|
+
child.visibility = View.INVISIBLE
|
|
289
|
+
Log.d(TAG, "prepareExpand: intercepted new screen, set INVISIBLE")
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
override fun onChildViewRemoved(parent: View?, child: View?) {}
|
|
293
|
+
}
|
|
294
|
+
screenStack.setOnHierarchyChangeListener(listener)
|
|
295
|
+
screenStackRef = WeakReference(screenStack)
|
|
296
|
+
hierarchyListener = listener
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Capture snapshot
|
|
300
|
+
val cardImage = captureSnapshot()
|
|
301
|
+
|
|
302
|
+
// Create overlay at source position
|
|
303
|
+
val bgColor = cardBgColor
|
|
304
|
+
val wrapper = FrameLayout(context)
|
|
305
|
+
wrapper.layoutParams = FrameLayout.LayoutParams(cardWidth.toInt(), cardHeight.toInt())
|
|
306
|
+
wrapper.x = cardLeft
|
|
307
|
+
wrapper.y = cardTop
|
|
308
|
+
wrapper.clipChildren = true
|
|
309
|
+
wrapper.clipToPadding = true
|
|
310
|
+
setRoundedCorners(wrapper, cardCornerRadiusPx)
|
|
311
|
+
if (bgColor != null) {
|
|
312
|
+
wrapper.setBackgroundColor(bgColor)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
val content = ImageView(context)
|
|
316
|
+
content.setImageBitmap(cardImage)
|
|
317
|
+
content.scaleType = ImageView.ScaleType.FIT_XY
|
|
318
|
+
content.layoutParams = FrameLayout.LayoutParams(cardWidth.toInt(), cardHeight.toInt())
|
|
319
|
+
wrapper.addView(content)
|
|
320
|
+
|
|
321
|
+
decorView.addView(wrapper)
|
|
322
|
+
overlayContainer = wrapper
|
|
323
|
+
|
|
324
|
+
// Hide source card — overlay covers it
|
|
325
|
+
alpha = 0f
|
|
326
|
+
|
|
327
|
+
Log.d(TAG, "=== prepareExpand DONE === overlay at [${cardLeft},${cardTop}]")
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Check if the target view's screen container is set up and positioned.
|
|
332
|
+
* Returns false if the screen container can't be found yet.
|
|
333
|
+
*/
|
|
334
|
+
fun isTargetScreenReady(targetView: View?): Boolean {
|
|
335
|
+
if (targetView == null) return true
|
|
336
|
+
val screenContainer = findScreenContainer(targetView) ?: return false
|
|
337
|
+
// Also check position isn't the same as source (stale layout)
|
|
338
|
+
val loc = IntArray(2)
|
|
339
|
+
targetView.getLocationInWindow(loc)
|
|
340
|
+
val targetAtSource = loc[0].toFloat() == cardLeft && loc[1].toFloat() == cardTop
|
|
341
|
+
if (targetAtSource && targetView.width > 0) {
|
|
342
|
+
Log.d(TAG, "isTargetScreenReady: target still at source position [${loc[0]},${loc[1]}], waiting...")
|
|
343
|
+
return false
|
|
344
|
+
}
|
|
345
|
+
return true
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ══════════════════════════════════════════════════════════════
|
|
349
|
+
// PHASE 2: animateExpand — called after delay, positions stable
|
|
350
|
+
// Animates overlay from source to target position.
|
|
351
|
+
// ══════════════════════════════════════════════════════════════
|
|
352
|
+
|
|
353
|
+
fun animateExpand(targetView: View?, promise: Promise) {
|
|
354
|
+
Log.d(TAG, "=== animateExpand START ===")
|
|
355
|
+
val wrapper = overlayContainer
|
|
356
|
+
if (wrapper == null) {
|
|
357
|
+
Log.d(TAG, "animateExpand: NO OVERLAY — falling back to expandToTarget")
|
|
358
|
+
expandToTarget(targetView, promise)
|
|
359
|
+
return
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
isExpanded = true
|
|
363
|
+
// Save target view reference for collapse (prepareExpand may have been called with null)
|
|
364
|
+
if (targetView != null) {
|
|
365
|
+
targetViewRef = targetView
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
val decorView = getDecorView()
|
|
369
|
+
if (decorView == null) {
|
|
370
|
+
promise.resolve(false)
|
|
371
|
+
return
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Re-hide target screen right before animation starts (belt-and-suspenders)
|
|
375
|
+
val preTargetScreen = targetScreenContainerRef?.get()
|
|
376
|
+
val preSourceScreen = sourceScreenContainerRef?.get()
|
|
377
|
+
// Ensure target screen stays INVISIBLE (belt-and-suspenders)
|
|
378
|
+
if (preTargetScreen != null && preTargetScreen !== preSourceScreen) {
|
|
379
|
+
preTargetScreen.visibility = View.INVISIBLE
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Stop intercepting new screens — animation is taking over
|
|
383
|
+
removeHierarchyListener()
|
|
384
|
+
|
|
385
|
+
// Read target position (now settled after delay)
|
|
386
|
+
val d = density
|
|
387
|
+
val targetLoc = if (targetView != null) getLocationInWindow(targetView) else intArrayOf(cardLeft.toInt(), cardTop.toInt())
|
|
388
|
+
val twPx = if (pendingTargetWidth > 0) pendingTargetWidth * d else cardWidth
|
|
389
|
+
val thPx = if (pendingTargetHeight > 0) pendingTargetHeight * d else cardHeight
|
|
390
|
+
val tbrPx = if (pendingTargetBorderRadius >= 0) pendingTargetBorderRadius * d else cardCornerRadiusPx
|
|
391
|
+
|
|
392
|
+
val targetLeft = targetLoc[0].toFloat()
|
|
393
|
+
val targetTop = targetLoc[1].toFloat()
|
|
394
|
+
val targetWidthPx = twPx
|
|
395
|
+
val targetHeightPx = thPx
|
|
396
|
+
val targetCornerRadiusPx = tbrPx
|
|
397
|
+
|
|
398
|
+
// Log target view details
|
|
399
|
+
if (targetView != null) {
|
|
400
|
+
Log.d(TAG, "animateExpand: targetView.id=${targetView.id} isAttached=${targetView.isAttachedToWindow} isLaidOut=${targetView.isLaidOut}")
|
|
401
|
+
Log.d(TAG, "animateExpand: targetView size=${targetView.width}x${targetView.height}")
|
|
402
|
+
}
|
|
403
|
+
Log.d(TAG, "animateExpand: source=[${cardLeft},${cardTop},${cardWidth}x${cardHeight}]")
|
|
404
|
+
Log.d(TAG, "animateExpand: target=[${targetLeft},${targetTop},${targetWidthPx}x${targetHeightPx}] cornerR=$targetCornerRadiusPx")
|
|
405
|
+
Log.d(TAG, "animateExpand: pendingTarget w=${pendingTargetWidth} h=${pendingTargetHeight} br=${pendingTargetBorderRadius}")
|
|
406
|
+
|
|
407
|
+
val dur = duration.toLong()
|
|
408
|
+
val content = if (wrapper.childCount > 0) wrapper.getChildAt(0) else null
|
|
409
|
+
|
|
410
|
+
// Compute content offset for wrapper mode
|
|
411
|
+
val contentCx = if (hasWrapper && pendingContentCentered) (targetWidthPx - cardWidth) / 2f else 0f
|
|
412
|
+
val contentCy = if (hasWrapper && pendingContentCentered) (targetHeightPx - cardHeight) / 2f
|
|
413
|
+
else if (hasWrapper) pendingContentOffsetY * d else 0f
|
|
414
|
+
|
|
415
|
+
// For no-wrapper mode, compute image frame
|
|
416
|
+
val targetImageFrame = if (!hasWrapper && content != null) {
|
|
417
|
+
imageFrameForScaleMode(scaleMode, cardWidth, cardHeight, targetWidthPx, targetHeightPx)
|
|
418
|
+
} else null
|
|
419
|
+
|
|
420
|
+
val animator = ValueAnimator.ofFloat(0f, 1f)
|
|
421
|
+
animator.duration = dur
|
|
422
|
+
animator.interpolator = springInterpolator
|
|
423
|
+
|
|
424
|
+
animator.addUpdateListener { anim ->
|
|
425
|
+
val t = anim.animatedValue as Float
|
|
426
|
+
wrapper.x = lerp(cardLeft, targetLeft, t)
|
|
427
|
+
wrapper.y = lerp(cardTop, targetTop, t)
|
|
428
|
+
val lp = wrapper.layoutParams
|
|
429
|
+
lp.width = lerp(cardWidth, targetWidthPx, t).toInt()
|
|
430
|
+
lp.height = lerp(cardHeight, targetHeightPx, t).toInt()
|
|
431
|
+
wrapper.layoutParams = lp
|
|
432
|
+
setRoundedCorners(wrapper, lerp(cardCornerRadiusPx, targetCornerRadiusPx, t))
|
|
433
|
+
|
|
434
|
+
if (content != null) {
|
|
435
|
+
if (hasWrapper) {
|
|
436
|
+
content.x = lerp(0f, contentCx, t)
|
|
437
|
+
content.y = lerp(0f, contentCy, t)
|
|
438
|
+
} else if (targetImageFrame != null) {
|
|
439
|
+
val slp = content.layoutParams as FrameLayout.LayoutParams
|
|
440
|
+
slp.width = lerp(cardWidth, targetImageFrame.width(), t).toInt()
|
|
441
|
+
slp.height = lerp(cardHeight, targetImageFrame.height(), t).toInt()
|
|
442
|
+
content.layoutParams = slp
|
|
443
|
+
content.x = lerp(0f, targetImageFrame.left, t)
|
|
444
|
+
content.y = lerp(0f, targetImageFrame.top, t)
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Crossfade: at 15% of animation, make target screen VISIBLE with alpha=0
|
|
450
|
+
// then fade alpha to 1 over 50% of duration
|
|
451
|
+
val targetScreen = targetScreenContainerRef?.get()
|
|
452
|
+
val sourceScreen = sourceScreenContainerRef?.get()
|
|
453
|
+
if (targetScreen != null && targetScreen !== sourceScreen) {
|
|
454
|
+
mainHandler.postDelayed({
|
|
455
|
+
// Switch from INVISIBLE to VISIBLE but with alpha=0
|
|
456
|
+
targetScreen.alpha = 0f
|
|
457
|
+
targetScreen.visibility = View.VISIBLE
|
|
458
|
+
val fadeAnimator = ValueAnimator.ofFloat(0f, 1f)
|
|
459
|
+
fadeAnimator.duration = (dur * 0.5f).toLong()
|
|
460
|
+
fadeAnimator.addUpdateListener { a ->
|
|
461
|
+
targetScreen.alpha = a.animatedValue as Float
|
|
462
|
+
}
|
|
463
|
+
fadeAnimator.start()
|
|
464
|
+
}, (dur * 0.15f).toLong())
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
animator.addListener(object : android.animation.AnimatorListenerAdapter() {
|
|
468
|
+
override fun onAnimationEnd(animation: android.animation.Animator) {
|
|
469
|
+
Log.d(TAG, "=== animateExpand COMPLETE ===")
|
|
470
|
+
targetScreenContainerRef?.get()?.let {
|
|
471
|
+
it.visibility = View.VISIBLE
|
|
472
|
+
it.alpha = 1f
|
|
473
|
+
}
|
|
474
|
+
this@MorphCardSourceView.alpha = 1f
|
|
475
|
+
|
|
476
|
+
transferSnapshotToTarget(decorView, wrapper, targetView,
|
|
477
|
+
targetWidthPx, targetHeightPx, targetCornerRadiusPx, 200L)
|
|
478
|
+
|
|
479
|
+
promise.resolve(true)
|
|
480
|
+
}
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
animator.start()
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Transfer the snapshot INTO the MorphCardTargetView via showSnapshot(),
|
|
488
|
+
* then make the target screen VISIBLE and fade out the DecorView overlay.
|
|
489
|
+
* This allows absolutely positioned elements (X button, etc.) to render
|
|
490
|
+
* on top of the snapshot, just like iOS.
|
|
491
|
+
*/
|
|
492
|
+
private fun transferSnapshotToTarget(
|
|
493
|
+
decorView: ViewGroup,
|
|
494
|
+
overlay: FrameLayout,
|
|
495
|
+
targetView: View?,
|
|
496
|
+
targetWidthPx: Float,
|
|
497
|
+
targetHeightPx: Float,
|
|
498
|
+
cornerRadius: Float,
|
|
499
|
+
fadeDuration: Long = 100
|
|
500
|
+
) {
|
|
501
|
+
val target = targetView as? MorphCardTargetView
|
|
502
|
+
if (target == null) {
|
|
503
|
+
Log.d(TAG, "transferSnapshot: targetView is not MorphCardTargetView, removing overlay")
|
|
504
|
+
decorView.removeView(overlay)
|
|
505
|
+
overlayContainer = null
|
|
506
|
+
return
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Get the bitmap from the overlay
|
|
510
|
+
val origImg = if (overlay.childCount > 0) overlay.getChildAt(0) as? ImageView else null
|
|
511
|
+
val bitmap = if (origImg != null) {
|
|
512
|
+
// Extract the bitmap from the drawable
|
|
513
|
+
val drawable = origImg.drawable
|
|
514
|
+
if (drawable is android.graphics.drawable.BitmapDrawable) {
|
|
515
|
+
drawable.bitmap
|
|
516
|
+
} else {
|
|
517
|
+
// Fallback: render the overlay content to a bitmap
|
|
518
|
+
val bmp = Bitmap.createBitmap(overlay.width, overlay.height, Bitmap.Config.ARGB_8888)
|
|
519
|
+
val canvas = Canvas(bmp)
|
|
520
|
+
overlay.draw(canvas)
|
|
521
|
+
bmp
|
|
522
|
+
}
|
|
523
|
+
} else null
|
|
524
|
+
|
|
525
|
+
if (bitmap != null) {
|
|
526
|
+
// Compute the image frame within the target view
|
|
527
|
+
val frame = if (hasWrapper) {
|
|
528
|
+
val cx = if (pendingContentCentered) (targetWidthPx - cardWidth) / 2f else 0f
|
|
529
|
+
val cy = if (pendingContentCentered) (targetHeightPx - cardHeight) / 2f
|
|
530
|
+
else pendingContentOffsetY * density
|
|
531
|
+
RectF(cx, cy, cx + cardWidth, cy + cardHeight)
|
|
532
|
+
} else {
|
|
533
|
+
imageFrameForScaleMode(scaleMode, cardWidth, cardHeight,
|
|
534
|
+
target.width.toFloat(), target.height.toFloat())
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
target.showSnapshot(bitmap, ImageView.ScaleType.FIT_XY, frame, cornerRadius, cardBgColor)
|
|
538
|
+
Log.d(TAG, "transferSnapshot: handed snapshot to MorphCardTargetView")
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
val fadeOut = ValueAnimator.ofFloat(1f, 0f)
|
|
542
|
+
fadeOut.duration = fadeDuration
|
|
543
|
+
fadeOut.addUpdateListener { anim ->
|
|
544
|
+
overlay.alpha = anim.animatedValue as Float
|
|
545
|
+
}
|
|
546
|
+
fadeOut.addListener(object : android.animation.AnimatorListenerAdapter() {
|
|
547
|
+
override fun onAnimationEnd(animation: android.animation.Animator) {
|
|
548
|
+
decorView.removeView(overlay)
|
|
549
|
+
overlayContainer = null
|
|
550
|
+
Log.d(TAG, "transferSnapshot: overlay fade-out complete")
|
|
551
|
+
}
|
|
552
|
+
})
|
|
553
|
+
fadeOut.start()
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// ══════════════════════════════════════════════════════════════
|
|
557
|
+
// Fallback: expandToTarget (direct, used if prepareExpand wasn't called)
|
|
558
|
+
// ══════════════════════════════════════════════════════════════
|
|
559
|
+
|
|
560
|
+
fun expandToTarget(targetView: View?, promise: Promise) {
|
|
561
|
+
Log.d(TAG, "expandToTarget: fallback path")
|
|
562
|
+
if (isExpanded) {
|
|
563
|
+
promise.resolve(false)
|
|
564
|
+
return
|
|
565
|
+
}
|
|
566
|
+
prepareExpand(targetView)
|
|
567
|
+
animateExpand(targetView, promise)
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// ══════════════════════════════════════════════════════════════
|
|
571
|
+
// COLLAPSE
|
|
572
|
+
// ══════════════════════════════════════════════════════════════
|
|
573
|
+
|
|
574
|
+
fun collapseWithResolve(promise: Promise) {
|
|
575
|
+
collapseFromTarget(targetViewRef, promise)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
private fun collapseFromTarget(targetView: View?, promise: Promise) {
|
|
579
|
+
Log.d(TAG, "=== collapseFromTarget START === isExpanded=$isExpanded hasWrapper=$hasWrapper overlayContainer=${overlayContainer != null}")
|
|
580
|
+
if (!isExpanded) {
|
|
581
|
+
promise.resolve(false)
|
|
582
|
+
return
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
val decorView = getDecorView()
|
|
586
|
+
if (decorView == null) {
|
|
587
|
+
promise.resolve(false)
|
|
588
|
+
return
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
val d = density
|
|
592
|
+
val dur = duration.toLong()
|
|
593
|
+
|
|
594
|
+
// Create DecorView overlay for collapse animation.
|
|
595
|
+
// Get snapshot from MorphCardTargetView if available, otherwise recapture.
|
|
596
|
+
var wrapper = overlayContainer
|
|
597
|
+
if (wrapper == null) {
|
|
598
|
+
val target = targetView as? MorphCardTargetView
|
|
599
|
+
val targetLoc = if (targetView != null) getLocationInWindow(targetView) else intArrayOf(cardLeft.toInt(), cardTop.toInt())
|
|
600
|
+
val twPx = if (pendingTargetWidth > 0) pendingTargetWidth * d else cardWidth
|
|
601
|
+
val thPx = if (pendingTargetHeight > 0) pendingTargetHeight * d else cardHeight
|
|
602
|
+
val tbrPx = if (pendingTargetBorderRadius >= 0) pendingTargetBorderRadius * d else cardCornerRadiusPx
|
|
603
|
+
|
|
604
|
+
// Recapture snapshot from source (the image hasn't changed)
|
|
605
|
+
alpha = 1f
|
|
606
|
+
val cardImage = captureSnapshot()
|
|
607
|
+
alpha = 0f
|
|
608
|
+
|
|
609
|
+
// Clear the snapshot from the target view
|
|
610
|
+
target?.clearSnapshot()
|
|
611
|
+
|
|
612
|
+
wrapper = FrameLayout(context)
|
|
613
|
+
wrapper.layoutParams = FrameLayout.LayoutParams(twPx.toInt(), thPx.toInt())
|
|
614
|
+
wrapper.x = targetLoc[0].toFloat()
|
|
615
|
+
wrapper.y = targetLoc[1].toFloat()
|
|
616
|
+
wrapper.clipChildren = true
|
|
617
|
+
wrapper.clipToPadding = true
|
|
618
|
+
setRoundedCorners(wrapper, tbrPx)
|
|
619
|
+
|
|
620
|
+
val bgColor = cardBgColor
|
|
621
|
+
if (bgColor != null) {
|
|
622
|
+
wrapper.setBackgroundColor(bgColor)
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
val content = ImageView(context)
|
|
626
|
+
content.setImageBitmap(cardImage)
|
|
627
|
+
content.scaleType = ImageView.ScaleType.FIT_XY
|
|
628
|
+
|
|
629
|
+
if (hasWrapper) {
|
|
630
|
+
val cx = if (pendingContentCentered) (twPx - cardWidth) / 2f else 0f
|
|
631
|
+
val cy = if (pendingContentCentered) (thPx - cardHeight) / 2f else pendingContentOffsetY * d
|
|
632
|
+
content.layoutParams = FrameLayout.LayoutParams(cardWidth.toInt(), cardHeight.toInt())
|
|
633
|
+
content.x = cx
|
|
634
|
+
content.y = cy
|
|
635
|
+
} else {
|
|
636
|
+
val imageFrame = imageFrameForScaleMode(scaleMode, cardWidth, cardHeight, twPx, thPx)
|
|
637
|
+
content.layoutParams = FrameLayout.LayoutParams(imageFrame.width().toInt(), imageFrame.height().toInt())
|
|
638
|
+
content.x = imageFrame.left
|
|
639
|
+
content.y = imageFrame.top
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
wrapper.addView(content)
|
|
643
|
+
decorView.addView(wrapper)
|
|
644
|
+
overlayContainer = wrapper
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Ensure wrapper is valid
|
|
648
|
+
if (wrapper == null) {
|
|
649
|
+
isExpanded = false
|
|
650
|
+
promise.resolve(false)
|
|
651
|
+
return
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Show source screen underneath
|
|
655
|
+
val sourceScreen = sourceScreenContainerRef?.get()
|
|
656
|
+
val targetScreen = targetScreenContainerRef?.get()
|
|
657
|
+
sourceScreen?.alpha = 1f
|
|
658
|
+
|
|
659
|
+
val content = if (wrapper.childCount > 0) wrapper.getChildAt(0) else null
|
|
660
|
+
|
|
661
|
+
val startLeft = wrapper.x
|
|
662
|
+
val startTop = wrapper.y
|
|
663
|
+
val startWidth = wrapper.layoutParams.width.toFloat()
|
|
664
|
+
val startHeight = wrapper.layoutParams.height.toFloat()
|
|
665
|
+
val startCx = content?.x ?: 0f
|
|
666
|
+
val startCy = content?.y ?: 0f
|
|
667
|
+
val startCr = if (pendingTargetBorderRadius >= 0) pendingTargetBorderRadius * d else cardCornerRadiusPx
|
|
668
|
+
|
|
669
|
+
val startImgW = content?.layoutParams?.width?.toFloat() ?: cardWidth
|
|
670
|
+
val startImgH = content?.layoutParams?.height?.toFloat() ?: cardHeight
|
|
671
|
+
|
|
672
|
+
val animator = ValueAnimator.ofFloat(0f, 1f)
|
|
673
|
+
animator.duration = dur
|
|
674
|
+
animator.interpolator = springInterpolator
|
|
675
|
+
|
|
676
|
+
animator.addUpdateListener { anim ->
|
|
677
|
+
val t = anim.animatedValue as Float
|
|
678
|
+
wrapper.x = lerp(startLeft, cardLeft, t)
|
|
679
|
+
wrapper.y = lerp(startTop, cardTop, t)
|
|
680
|
+
val lp = wrapper.layoutParams
|
|
681
|
+
lp.width = lerp(startWidth, cardWidth, t).toInt()
|
|
682
|
+
lp.height = lerp(startHeight, cardHeight, t).toInt()
|
|
683
|
+
wrapper.layoutParams = lp
|
|
684
|
+
setRoundedCorners(wrapper, lerp(startCr, cardCornerRadiusPx, t))
|
|
685
|
+
|
|
686
|
+
if (content != null) {
|
|
687
|
+
if (hasWrapper) {
|
|
688
|
+
content.x = lerp(startCx, 0f, t)
|
|
689
|
+
content.y = lerp(startCy, 0f, t)
|
|
690
|
+
} else {
|
|
691
|
+
content.x = lerp(startCx, 0f, t)
|
|
692
|
+
content.y = lerp(startCy, 0f, t)
|
|
693
|
+
val slp = content.layoutParams
|
|
694
|
+
slp.width = lerp(startImgW, cardWidth, t).toInt()
|
|
695
|
+
slp.height = lerp(startImgH, cardHeight, t).toInt()
|
|
696
|
+
content.layoutParams = slp
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Crossfade: fade out target screen starting at 10%, over 65% of duration
|
|
702
|
+
if (targetScreen != null && targetScreen !== sourceScreen) {
|
|
703
|
+
mainHandler.postDelayed({
|
|
704
|
+
val fadeAnimator = ValueAnimator.ofFloat(1f, 0f)
|
|
705
|
+
fadeAnimator.duration = (dur * 0.65f).toLong()
|
|
706
|
+
fadeAnimator.addUpdateListener { a ->
|
|
707
|
+
targetScreen.alpha = a.animatedValue as Float
|
|
708
|
+
}
|
|
709
|
+
fadeAnimator.addListener(object : android.animation.AnimatorListenerAdapter() {
|
|
710
|
+
override fun onAnimationEnd(animation: android.animation.Animator) {
|
|
711
|
+
targetScreen.visibility = View.INVISIBLE
|
|
712
|
+
}
|
|
713
|
+
})
|
|
714
|
+
fadeAnimator.start()
|
|
715
|
+
}, (dur * 0.15f).toLong())
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
animator.addListener(object : android.animation.AnimatorListenerAdapter() {
|
|
719
|
+
override fun onAnimationEnd(animation: android.animation.Animator) {
|
|
720
|
+
decorView.removeView(wrapper)
|
|
721
|
+
overlayContainer = null
|
|
722
|
+
removeHierarchyListener()
|
|
723
|
+
this@MorphCardSourceView.alpha = 1f
|
|
724
|
+
isExpanded = false
|
|
725
|
+
sourceScreenContainerRef = null
|
|
726
|
+
targetScreenContainerRef = null
|
|
727
|
+
promise.resolve(true)
|
|
728
|
+
}
|
|
729
|
+
})
|
|
730
|
+
|
|
731
|
+
animator.start()
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Hide the target screen container. Called by MorphCardModule after the
|
|
736
|
+
* retry loop finds the target view, in case prepareExpand ran before
|
|
737
|
+
* the target was registered.
|
|
738
|
+
*/
|
|
739
|
+
fun hideTargetScreen(targetView: View?) {
|
|
740
|
+
val targetScreen = findScreenContainer(targetView)
|
|
741
|
+
val sourceScreen = sourceScreenContainerRef?.get()
|
|
742
|
+
targetScreenContainerRef = if (targetScreen != null) WeakReference(targetScreen) else null
|
|
743
|
+
if (targetScreen != null && targetScreen !== sourceScreen) {
|
|
744
|
+
targetScreen.visibility = View.INVISIBLE
|
|
745
|
+
Log.d(TAG, "hideTargetScreen: set target screen INVISIBLE")
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
companion object {
|
|
750
|
+
private const val TAG = "MorphCard"
|
|
751
|
+
private fun lerp(start: Float, end: Float, fraction: Float): Float {
|
|
752
|
+
return start + (end - start) * fraction
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|