react-native-morph-card 0.2.7 → 0.3.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/README.md +40 -31
- package/android/src/main/java/com/melivalesca/morphcard/MorphCardSourceView.kt +27 -72
- package/android/src/main/java/com/melivalesca/morphcard/MorphCardTargetManager.kt +2 -2
- package/android/src/main/java/com/melivalesca/morphcard/MorphCardTargetView.kt +34 -39
- package/android/src/main/java/com/melivalesca/morphcard/MorphCardUtils.kt +43 -0
- package/ios/Fabric/RNCMorphCardSourceComponentView.h +0 -3
- package/ios/Fabric/RNCMorphCardSourceComponentView.mm +110 -80
- package/ios/Fabric/RNCMorphCardTargetComponentView.h +1 -0
- package/ios/Fabric/RNCMorphCardTargetComponentView.mm +26 -14
- package/ios/RNCMorphCardSource.m +1 -1
- package/lib/commonjs/MorphCardSource.js +25 -2
- package/lib/commonjs/MorphCardSource.js.map +1 -1
- package/lib/commonjs/MorphCardTarget.js +31 -7
- package/lib/commonjs/MorphCardTarget.js.map +1 -1
- package/lib/commonjs/MorphChildrenRegistry.js +34 -0
- package/lib/commonjs/MorphChildrenRegistry.js.map +1 -0
- package/lib/module/MorphCardSource.js +25 -2
- package/lib/module/MorphCardSource.js.map +1 -1
- package/lib/module/MorphCardTarget.js +31 -7
- package/lib/module/MorphCardTarget.js.map +1 -1
- package/lib/module/MorphChildrenRegistry.js +27 -0
- package/lib/module/MorphChildrenRegistry.js.map +1 -0
- package/lib/typescript/src/MorphCardSource.d.ts +4 -4
- package/lib/typescript/src/MorphCardSource.d.ts.map +1 -1
- package/lib/typescript/src/MorphCardTarget.d.ts.map +1 -1
- package/lib/typescript/src/MorphChildrenRegistry.d.ts +13 -0
- package/lib/typescript/src/MorphChildrenRegistry.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/MorphCardSource.tsx +27 -5
- package/src/MorphCardTarget.tsx +44 -9
- package/src/MorphChildrenRegistry.ts +43 -0
- package/src/index.tsx +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Native card-to-modal morph transition for React Native. Smoothly animates a card from a list into a fullscreen detail view, morphing size, position, and corner radius — then collapses back.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
<video src="assets/demo.mov">
|
|
6
6
|
|
|
7
7
|
- Native animations on both platforms (UIKit `UIViewPropertyAnimator` / Android `ValueAnimator`)
|
|
8
8
|
- No JS-driven animation, no webview, no experimental flags
|
|
@@ -46,12 +46,16 @@ No additional steps required.
|
|
|
46
46
|
|
|
47
47
|
Wrap your card content in `MorphCardSource`. On the detail screen, use `MorphCardTarget` where the card should land. Use `useMorphTarget` for easy collapse handling.
|
|
48
48
|
|
|
49
|
-
> **Important:** `MorphCardSource` can wrap any React Native component (images, views, text, etc.)
|
|
49
|
+
> **Important:** `MorphCardSource` can wrap any React Native component (images, views, text, etc.). During the animation, the content is captured as a **bitmap snapshot** — but once the animation completes, the snapshot fades out and the source's children are automatically cloned into `MorphCardTarget` as live React components. The cloned children are rendered at the source card's original layout dimensions, so your component layout stays consistent. This means observable values (timers, animated values, live data) will update in real time after the transition finishes. If you use `resizeMode`, the bitmap is kept instead (native image scaling doesn't apply to React components).
|
|
50
50
|
|
|
51
51
|
```tsx
|
|
52
52
|
import React from 'react';
|
|
53
53
|
import { View, Image, Text, Pressable } from 'react-native';
|
|
54
|
-
import {
|
|
54
|
+
import {
|
|
55
|
+
MorphCardSource,
|
|
56
|
+
MorphCardTarget,
|
|
57
|
+
useMorphTarget,
|
|
58
|
+
} from 'react-native-morph-card';
|
|
55
59
|
|
|
56
60
|
// ── List screen ──
|
|
57
61
|
const ListScreen = ({ navigation }) => {
|
|
@@ -98,30 +102,30 @@ const DetailScreen = ({ route, navigation }) => {
|
|
|
98
102
|
|
|
99
103
|
Wraps the card content on the list/grid screen. Captures a snapshot and drives the expand animation.
|
|
100
104
|
|
|
101
|
-
| Prop
|
|
102
|
-
|
|
103
|
-
| `width`
|
|
104
|
-
| `height`
|
|
105
|
-
| `borderRadius`
|
|
106
|
-
| `backgroundColor` | `string`
|
|
107
|
-
| `duration`
|
|
108
|
-
| `expandDuration`
|
|
109
|
-
| `
|
|
110
|
-
| `onPress`
|
|
105
|
+
| Prop | Type | Default | Description |
|
|
106
|
+
| ----------------- | ------------------------------------------ | -------------- | ------------------------------------------------------------------------------------------------------ |
|
|
107
|
+
| `width` | `DimensionValue` | — | Card width |
|
|
108
|
+
| `height` | `DimensionValue` | — | Card height |
|
|
109
|
+
| `borderRadius` | `number` | `0` | Corner radius of the card |
|
|
110
|
+
| `backgroundColor` | `string` | — | Background color (enables "wrapper mode" where the background expands separately from the content) |
|
|
111
|
+
| `duration` | `number` | `300` | Default animation duration in ms (used for both expand and collapse if specific durations are not set) |
|
|
112
|
+
| `expandDuration` | `number` | — | Duration of the expand animation in ms. Overrides `duration` for expand. |
|
|
113
|
+
| `resizeMode` | `'cover' \| 'contain' \| 'stretch'` | `'cover'` | How the snapshot scales during animation. When set, the bitmap is kept after expand (no live children). **Recommended when wrapping an `<Image>` — without it, the image may not scale properly during the animation.** |
|
|
114
|
+
| `onPress` | `(sourceTag: number) => void` | — | Called on tap with the native view tag. Use this to navigate to the detail screen. |
|
|
111
115
|
|
|
112
116
|
### `<MorphCardTarget>`
|
|
113
117
|
|
|
114
|
-
Placed on the detail screen where the card should land. Triggers the expand animation on mount.
|
|
118
|
+
Placed on the detail screen where the card should land. Triggers the expand animation on mount. After the animation, the source's children are automatically cloned here as live React components.
|
|
115
119
|
|
|
116
|
-
| Prop
|
|
117
|
-
|
|
118
|
-
| `sourceTag`
|
|
119
|
-
| `width`
|
|
120
|
-
| `height`
|
|
121
|
-
| `borderRadius`
|
|
122
|
-
| `collapseDuration` | `number`
|
|
123
|
-
| `contentOffsetY`
|
|
124
|
-
| `contentCentered`
|
|
120
|
+
| Prop | Type | Default | Description |
|
|
121
|
+
| ------------------ | ---------------- | ------------- | -------------------------------------------------------------------------------- |
|
|
122
|
+
| `sourceTag` | `number` | **required** | The source view tag from navigation params |
|
|
123
|
+
| `width` | `DimensionValue` | source width | Target width after expand |
|
|
124
|
+
| `height` | `DimensionValue` | source height | Target height after expand |
|
|
125
|
+
| `borderRadius` | `number` | source radius | Target corner radius. Set to `0` for no rounding. |
|
|
126
|
+
| `collapseDuration` | `number` | — | Duration of the collapse animation in ms. Falls back to the source's `duration`. |
|
|
127
|
+
| `contentOffsetY` | `number` | `0` | Vertical offset for content snapshot in wrapper mode |
|
|
128
|
+
| `contentCentered` | `boolean` | `false` | Center content snapshot horizontally in wrapper mode |
|
|
125
129
|
|
|
126
130
|
### `useMorphTarget(options)`
|
|
127
131
|
|
|
@@ -141,7 +145,11 @@ const { dismiss } = useMorphTarget({
|
|
|
141
145
|
For more control, use the imperative functions:
|
|
142
146
|
|
|
143
147
|
```tsx
|
|
144
|
-
import {
|
|
148
|
+
import {
|
|
149
|
+
morphExpand,
|
|
150
|
+
morphCollapse,
|
|
151
|
+
getViewTag,
|
|
152
|
+
} from 'react-native-morph-card';
|
|
145
153
|
|
|
146
154
|
// Expand from source to target
|
|
147
155
|
await morphExpand(sourceRef, targetRef);
|
|
@@ -152,6 +160,7 @@ await morphCollapse(sourceTag);
|
|
|
152
160
|
// Get the native view tag from a ref
|
|
153
161
|
const tag = getViewTag(viewRef);
|
|
154
162
|
```
|
|
163
|
+
|
|
155
164
|
```
|
|
156
165
|
|
|
157
166
|
## Running the example app
|
|
@@ -183,7 +192,7 @@ yarn install
|
|
|
183
192
|
# Install example app dependencies
|
|
184
193
|
cd example
|
|
185
194
|
yarn install
|
|
186
|
-
|
|
195
|
+
````
|
|
187
196
|
|
|
188
197
|
### 2. Run on iOS
|
|
189
198
|
|
|
@@ -235,12 +244,12 @@ yarn build:android
|
|
|
235
244
|
|
|
236
245
|
### Troubleshooting
|
|
237
246
|
|
|
238
|
-
| Problem
|
|
239
|
-
|
|
240
|
-
| `pod install` fails
|
|
241
|
-
| Android build fails on first run
|
|
242
|
-
| Metro can't find `react-native-morph-card` | Run `yarn install` at the repo root first
|
|
243
|
-
| Duplicate module errors
|
|
247
|
+
| Problem | Fix |
|
|
248
|
+
| ------------------------------------------ | ------------------------------------------------------------------- |
|
|
249
|
+
| `pod install` fails | Run `bundle install` first, then `bundle exec pod install` |
|
|
250
|
+
| Android build fails on first run | Make sure `ANDROID_HOME` is set and an emulator/device is available |
|
|
251
|
+
| Metro can't find `react-native-morph-card` | Run `yarn install` at the repo root first |
|
|
252
|
+
| Duplicate module errors | Delete `node_modules` in both root and `example/`, then reinstall |
|
|
244
253
|
|
|
245
254
|
## Contributing
|
|
246
255
|
|
|
@@ -5,7 +5,6 @@ import android.content.Context
|
|
|
5
5
|
import android.graphics.Bitmap
|
|
6
6
|
import android.graphics.Canvas
|
|
7
7
|
import android.graphics.Color
|
|
8
|
-
import android.graphics.Outline
|
|
9
8
|
import android.graphics.RectF
|
|
10
9
|
import android.graphics.Paint
|
|
11
10
|
import android.graphics.PorterDuff
|
|
@@ -17,7 +16,6 @@ import android.os.Looper
|
|
|
17
16
|
import android.util.Log
|
|
18
17
|
import android.view.View
|
|
19
18
|
import android.view.ViewGroup
|
|
20
|
-
import android.view.ViewOutlineProvider
|
|
21
19
|
import android.view.ViewTreeObserver
|
|
22
20
|
import android.view.animation.PathInterpolator
|
|
23
21
|
import android.widget.FrameLayout
|
|
@@ -87,16 +85,7 @@ class MorphCardSourceView(context: Context) : ReactViewGroup(context) {
|
|
|
87
85
|
|
|
88
86
|
private fun applyBorderRadiusClipping() {
|
|
89
87
|
val radiusPx = if (borderRadiusDp > 0f) borderRadiusDp * density else 0f
|
|
90
|
-
|
|
91
|
-
clipToOutline = true
|
|
92
|
-
outlineProvider = object : ViewOutlineProvider() {
|
|
93
|
-
override fun getOutline(v: View, outline: Outline) {
|
|
94
|
-
outline.setRoundRect(0, 0, v.width, v.height, radiusPx)
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
} else {
|
|
98
|
-
clipToOutline = false
|
|
99
|
-
}
|
|
88
|
+
setRoundedCorners(this, radiusPx)
|
|
100
89
|
}
|
|
101
90
|
|
|
102
91
|
// ── Snapshot ──
|
|
@@ -125,27 +114,6 @@ class MorphCardSourceView(context: Context) : ReactViewGroup(context) {
|
|
|
125
114
|
return loc
|
|
126
115
|
}
|
|
127
116
|
|
|
128
|
-
/**
|
|
129
|
-
* Find the screen container for a view (ScreensCoordinatorLayout).
|
|
130
|
-
* Walks up until parent is ScreenStack/ScreenContainer.
|
|
131
|
-
*/
|
|
132
|
-
private fun findScreenContainer(view: View?): View? {
|
|
133
|
-
if (view == null) return null
|
|
134
|
-
var current: View? = view
|
|
135
|
-
while (current != null) {
|
|
136
|
-
val parent = current.parent
|
|
137
|
-
if (parent is ViewGroup) {
|
|
138
|
-
val parentName = parent.javaClass.name
|
|
139
|
-
if (parentName.contains("ScreenStack") || parentName.contains("ScreenContainer")) {
|
|
140
|
-
return current
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
current = if (current.parent is View) current.parent as View else null
|
|
144
|
-
}
|
|
145
|
-
return null
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
149
117
|
private fun extractBackgroundColor(): Int? {
|
|
150
118
|
val bg = background ?: return null
|
|
151
119
|
if (bg is ColorDrawable) return bg.color
|
|
@@ -216,19 +184,6 @@ class MorphCardSourceView(context: Context) : ReactViewGroup(context) {
|
|
|
216
184
|
}
|
|
217
185
|
}
|
|
218
186
|
|
|
219
|
-
private fun setRoundedCorners(view: View, radiusPx: Float) {
|
|
220
|
-
if (radiusPx <= 0f) {
|
|
221
|
-
view.clipToOutline = false
|
|
222
|
-
return
|
|
223
|
-
}
|
|
224
|
-
view.clipToOutline = true
|
|
225
|
-
view.outlineProvider = object : ViewOutlineProvider() {
|
|
226
|
-
override fun getOutline(v: View, outline: Outline) {
|
|
227
|
-
outline.setRoundRect(0, 0, v.width, v.height, radiusPx)
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
187
|
/**
|
|
233
188
|
* Walk the view tree and hide any screen container that isn't already known.
|
|
234
189
|
* This catches modal screens added to separate ScreenStacks (e.g. transparentModal).
|
|
@@ -387,8 +342,9 @@ class MorphCardSourceView(context: Context) : ReactViewGroup(context) {
|
|
|
387
342
|
// Ensure the overlay renders above any views with elevation (e.g. ScreenStack children)
|
|
388
343
|
fullScreenOverlay.translationZ = 1000f
|
|
389
344
|
|
|
390
|
-
// PixelCopy captures
|
|
391
|
-
//
|
|
345
|
+
// PixelCopy captures the current screen with hardware rendering preserved
|
|
346
|
+
// (clipToOutline, borderRadius, etc.). The result is delivered on a background
|
|
347
|
+
// HandlerThread, then posted to main to add the blocker image.
|
|
392
348
|
// Note: context may be ThemedReactContext, not Activity directly.
|
|
393
349
|
val activity = (context as? android.app.Activity)
|
|
394
350
|
?: (context as? com.facebook.react.bridge.ReactContext)?.currentActivity
|
|
@@ -398,23 +354,25 @@ class MorphCardSourceView(context: Context) : ReactViewGroup(context) {
|
|
|
398
354
|
val copyThread = android.os.HandlerThread("PixelCopyThread")
|
|
399
355
|
copyThread.start()
|
|
400
356
|
val copyHandler = Handler(copyThread.looper)
|
|
401
|
-
val latch = java.util.concurrent.CountDownLatch(1)
|
|
402
357
|
android.view.PixelCopy.request(window, blockerBitmap, { result ->
|
|
403
|
-
|
|
404
|
-
|
|
358
|
+
copyThread.quitSafely()
|
|
359
|
+
if (result == android.view.PixelCopy.SUCCESS) {
|
|
360
|
+
mainHandler.post {
|
|
361
|
+
val blockerImg = ImageView(context)
|
|
362
|
+
blockerImg.setImageBitmap(blockerBitmap)
|
|
363
|
+
blockerImg.scaleType = ImageView.ScaleType.FIT_XY
|
|
364
|
+
blockerImg.layoutParams = FrameLayout.LayoutParams(
|
|
365
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
366
|
+
FrameLayout.LayoutParams.MATCH_PARENT
|
|
367
|
+
)
|
|
368
|
+
// Insert behind the card wrapper (index 0)
|
|
369
|
+
fullScreenOverlay.addView(blockerImg, 0)
|
|
370
|
+
}
|
|
371
|
+
} else {
|
|
372
|
+
Log.w(TAG, "prepareExpand: PixelCopy failed with result=$result")
|
|
373
|
+
blockerBitmap.recycle()
|
|
374
|
+
}
|
|
405
375
|
}, copyHandler)
|
|
406
|
-
// Wait for the copy (typically <5ms)
|
|
407
|
-
try { latch.await(100, java.util.concurrent.TimeUnit.MILLISECONDS) } catch (_: Exception) {}
|
|
408
|
-
copyThread.quitSafely()
|
|
409
|
-
|
|
410
|
-
val blockerImg = ImageView(context)
|
|
411
|
-
blockerImg.setImageBitmap(blockerBitmap)
|
|
412
|
-
blockerImg.scaleType = ImageView.ScaleType.FIT_XY
|
|
413
|
-
blockerImg.layoutParams = FrameLayout.LayoutParams(
|
|
414
|
-
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
415
|
-
FrameLayout.LayoutParams.MATCH_PARENT
|
|
416
|
-
)
|
|
417
|
-
fullScreenOverlay.addView(blockerImg)
|
|
418
376
|
}
|
|
419
377
|
|
|
420
378
|
// Create card overlay at source position (on top of screen capture)
|
|
@@ -695,8 +653,10 @@ class MorphCardSourceView(context: Context) : ReactViewGroup(context) {
|
|
|
695
653
|
target.width.toFloat(), target.height.toFloat())
|
|
696
654
|
}
|
|
697
655
|
|
|
698
|
-
target.showSnapshot(bitmap,
|
|
656
|
+
target.showSnapshot(bitmap, frame, cornerRadius, null)
|
|
699
657
|
Log.d(TAG, "transferSnapshot: handed snapshot to MorphCardTargetView")
|
|
658
|
+
// Crossfade snapshot out to reveal live React children underneath
|
|
659
|
+
mainHandler.postDelayed({ target.fadeOutSnapshot() }, 50)
|
|
700
660
|
}
|
|
701
661
|
|
|
702
662
|
val fadeOut = ValueAnimator.ofFloat(1f, 0f)
|
|
@@ -768,8 +728,10 @@ class MorphCardSourceView(context: Context) : ReactViewGroup(context) {
|
|
|
768
728
|
val cardImage = captureSnapshot()
|
|
769
729
|
alpha = 0f
|
|
770
730
|
|
|
771
|
-
// Clear the snapshot
|
|
731
|
+
// Clear the snapshot and hide the target view so live children
|
|
732
|
+
// don't show behind the animating collapse overlay
|
|
772
733
|
target?.clearSnapshot()
|
|
734
|
+
targetView?.visibility = View.INVISIBLE
|
|
773
735
|
|
|
774
736
|
wrapper = FrameLayout(context)
|
|
775
737
|
wrapper.layoutParams = FrameLayout.LayoutParams(twPx.toInt(), thPx.toInt())
|
|
@@ -806,13 +768,6 @@ class MorphCardSourceView(context: Context) : ReactViewGroup(context) {
|
|
|
806
768
|
overlayContainer = wrapper
|
|
807
769
|
}
|
|
808
770
|
|
|
809
|
-
// Ensure wrapper is valid
|
|
810
|
-
if (wrapper == null) {
|
|
811
|
-
isExpanded = false
|
|
812
|
-
promise.resolve(false)
|
|
813
|
-
return
|
|
814
|
-
}
|
|
815
|
-
|
|
816
771
|
// Show source screen underneath
|
|
817
772
|
val sourceScreen = sourceScreenContainerRef?.get()
|
|
818
773
|
val targetScreen = targetScreenContainerRef?.get()
|
|
@@ -35,11 +35,11 @@ class MorphCardTargetManager :
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
override fun setTargetWidth(view: MorphCardTargetView, value: Double) {
|
|
38
|
-
view
|
|
38
|
+
// Managed by the source view; required by codegen interface
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
override fun setTargetHeight(view: MorphCardTargetView, value: Double) {
|
|
42
|
-
view
|
|
42
|
+
// Managed by the source view; required by codegen interface
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
override fun setTargetBorderRadius(view: MorphCardTargetView, value: Double) {
|
|
@@ -1,24 +1,18 @@
|
|
|
1
1
|
package com.melivalesca.morphcard
|
|
2
2
|
|
|
3
|
+
import android.animation.ValueAnimator
|
|
3
4
|
import android.content.Context
|
|
4
5
|
import android.graphics.Bitmap
|
|
5
6
|
import android.graphics.Canvas
|
|
6
|
-
import android.graphics.Matrix
|
|
7
|
-
import android.graphics.Outline
|
|
8
7
|
import android.graphics.Paint
|
|
9
8
|
import android.graphics.Path
|
|
10
9
|
import android.graphics.RectF
|
|
11
10
|
import android.util.Log
|
|
12
11
|
import android.view.View
|
|
13
|
-
import android.view.ViewGroup
|
|
14
|
-
import android.view.ViewOutlineProvider
|
|
15
|
-
import android.widget.ImageView
|
|
16
12
|
import com.facebook.react.views.view.ReactViewGroup
|
|
17
13
|
|
|
18
14
|
class MorphCardTargetView(context: Context) : ReactViewGroup(context) {
|
|
19
15
|
|
|
20
|
-
var targetWidth: Float = 0f
|
|
21
|
-
var targetHeight: Float = 0f
|
|
22
16
|
var targetBorderRadius: Float = -1f
|
|
23
17
|
var collapseDuration: Double = 0.0
|
|
24
18
|
var sourceTag: Int = 0
|
|
@@ -28,6 +22,7 @@ class MorphCardTargetView(context: Context) : ReactViewGroup(context) {
|
|
|
28
22
|
private var snapshotFrame: RectF? = null
|
|
29
23
|
private var snapshotCornerRadius: Float = 0f
|
|
30
24
|
private var snapshotBgColor: Int? = null
|
|
25
|
+
private var snapshotAlpha: Float = 1f
|
|
31
26
|
private val snapshotPaint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG)
|
|
32
27
|
private val bgPaint = Paint(Paint.ANTI_ALIAS_FLAG)
|
|
33
28
|
|
|
@@ -37,13 +32,13 @@ class MorphCardTargetView(context: Context) : ReactViewGroup(context) {
|
|
|
37
32
|
override fun onAttachedToWindow() {
|
|
38
33
|
super.onAttachedToWindow()
|
|
39
34
|
MorphCardViewRegistry.register(this, id)
|
|
40
|
-
Log.d(
|
|
35
|
+
Log.d(TAG, "TargetView attached: id=$id sourceTag=$sourceTag")
|
|
41
36
|
|
|
42
37
|
if (sourceTag > 0) {
|
|
43
38
|
val screenContainer = findScreenContainer(this)
|
|
44
39
|
if (screenContainer != null) {
|
|
45
40
|
screenContainer.visibility = View.INVISIBLE
|
|
46
|
-
Log.d(
|
|
41
|
+
Log.d(TAG, "TargetView: set screen INVISIBLE")
|
|
47
42
|
}
|
|
48
43
|
}
|
|
49
44
|
}
|
|
@@ -65,7 +60,9 @@ class MorphCardTargetView(context: Context) : ReactViewGroup(context) {
|
|
|
65
60
|
override fun dispatchDraw(canvas: Canvas) {
|
|
66
61
|
val bmp = snapshotBitmap
|
|
67
62
|
val frame = snapshotFrame
|
|
68
|
-
if (bmp != null && frame != null) {
|
|
63
|
+
if (bmp != null && frame != null && snapshotAlpha > 0f) {
|
|
64
|
+
snapshotPaint.alpha = (snapshotAlpha * 255).toInt()
|
|
65
|
+
bgPaint.alpha = (snapshotAlpha * 255).toInt()
|
|
69
66
|
val radiusPx = snapshotCornerRadius
|
|
70
67
|
|
|
71
68
|
// Clip to rounded rect if needed
|
|
@@ -104,42 +101,16 @@ class MorphCardTargetView(context: Context) : ReactViewGroup(context) {
|
|
|
104
101
|
|
|
105
102
|
private fun applyBorderRadiusClipping() {
|
|
106
103
|
val radiusPx = if (targetBorderRadius > 0f) targetBorderRadius * density else 0f
|
|
107
|
-
|
|
108
|
-
clipToOutline = true
|
|
109
|
-
outlineProvider = object : ViewOutlineProvider() {
|
|
110
|
-
override fun getOutline(v: View, outline: Outline) {
|
|
111
|
-
outline.setRoundRect(0, 0, v.width, v.height, radiusPx)
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
} else {
|
|
115
|
-
clipToOutline = false
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
private fun findScreenContainer(view: View?): View? {
|
|
120
|
-
if (view == null) return null
|
|
121
|
-
var current: View? = view
|
|
122
|
-
while (current != null) {
|
|
123
|
-
val parent = current.parent
|
|
124
|
-
if (parent is ViewGroup) {
|
|
125
|
-
val parentName = parent.javaClass.name
|
|
126
|
-
if (parentName.contains("ScreenStack") || parentName.contains("ScreenContainer")) {
|
|
127
|
-
return current
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
current = if (current.parent is View) current.parent as View else null
|
|
131
|
-
}
|
|
132
|
-
return null
|
|
104
|
+
setRoundedCorners(this, radiusPx)
|
|
133
105
|
}
|
|
134
106
|
|
|
135
107
|
fun showSnapshot(
|
|
136
108
|
image: Bitmap,
|
|
137
|
-
scaleType: ImageView.ScaleType,
|
|
138
109
|
frame: RectF,
|
|
139
110
|
cornerRadius: Float,
|
|
140
111
|
backgroundColor: Int?
|
|
141
112
|
) {
|
|
142
|
-
Log.d(
|
|
113
|
+
Log.d(TAG, "showSnapshot: viewSize=${width}x${height} frame=$frame cornerR=$cornerRadius bg=$backgroundColor")
|
|
143
114
|
snapshotBitmap = image
|
|
144
115
|
snapshotFrame = frame
|
|
145
116
|
snapshotCornerRadius = cornerRadius
|
|
@@ -147,9 +118,29 @@ class MorphCardTargetView(context: Context) : ReactViewGroup(context) {
|
|
|
147
118
|
invalidate()
|
|
148
119
|
}
|
|
149
120
|
|
|
121
|
+
fun fadeOutSnapshot() {
|
|
122
|
+
if (snapshotBitmap == null) return
|
|
123
|
+
// Only fade out if there are React children underneath to reveal.
|
|
124
|
+
// If no children (scaleMode bitmap-only), keep the snapshot.
|
|
125
|
+
if (childCount == 0) return
|
|
126
|
+
val anim = ValueAnimator.ofFloat(1f, 0f)
|
|
127
|
+
anim.duration = 150
|
|
128
|
+
anim.addUpdateListener {
|
|
129
|
+
snapshotAlpha = it.animatedValue as Float
|
|
130
|
+
invalidate()
|
|
131
|
+
}
|
|
132
|
+
anim.addListener(object : android.animation.AnimatorListenerAdapter() {
|
|
133
|
+
override fun onAnimationEnd(animation: android.animation.Animator) {
|
|
134
|
+
clearSnapshot()
|
|
135
|
+
snapshotAlpha = 1f
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
anim.start()
|
|
139
|
+
}
|
|
140
|
+
|
|
150
141
|
fun clearSnapshot() {
|
|
151
142
|
if (snapshotBitmap != null) {
|
|
152
|
-
Log.d(
|
|
143
|
+
Log.d(TAG, "clearSnapshot: clearing bitmap")
|
|
153
144
|
snapshotBitmap = null
|
|
154
145
|
snapshotFrame = null
|
|
155
146
|
snapshotCornerRadius = 0f
|
|
@@ -157,4 +148,8 @@ class MorphCardTargetView(context: Context) : ReactViewGroup(context) {
|
|
|
157
148
|
invalidate()
|
|
158
149
|
}
|
|
159
150
|
}
|
|
151
|
+
|
|
152
|
+
companion object {
|
|
153
|
+
private const val TAG = "MorphCard"
|
|
154
|
+
}
|
|
160
155
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
package com.melivalesca.morphcard
|
|
2
|
+
|
|
3
|
+
import android.graphics.Outline
|
|
4
|
+
import android.view.View
|
|
5
|
+
import android.view.ViewGroup
|
|
6
|
+
import android.view.ViewOutlineProvider
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Walk up the view hierarchy until the parent is a ScreenStack or ScreenContainer,
|
|
10
|
+
* returning the immediate child (the "screen container" for that view).
|
|
11
|
+
*/
|
|
12
|
+
internal fun findScreenContainer(view: View?): View? {
|
|
13
|
+
if (view == null) return null
|
|
14
|
+
var current: View? = view
|
|
15
|
+
while (current != null) {
|
|
16
|
+
val parent = current.parent
|
|
17
|
+
if (parent is ViewGroup) {
|
|
18
|
+
val parentName = parent.javaClass.name
|
|
19
|
+
if (parentName.contains("ScreenStack") || parentName.contains("ScreenContainer")) {
|
|
20
|
+
return current
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
current = if (current.parent is View) current.parent as View else null
|
|
24
|
+
}
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Apply rounded-corner clipping to a view via its outline provider.
|
|
30
|
+
* If [radiusPx] is <= 0, clipping is disabled.
|
|
31
|
+
*/
|
|
32
|
+
internal fun setRoundedCorners(view: View, radiusPx: Float) {
|
|
33
|
+
if (radiusPx <= 0f) {
|
|
34
|
+
view.clipToOutline = false
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
view.clipToOutline = true
|
|
38
|
+
view.outlineProvider = object : ViewOutlineProvider() {
|
|
39
|
+
override fun getOutline(v: View, outline: Outline) {
|
|
40
|
+
outline.setRoundRect(0, 0, v.width, v.height, radiusPx)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -14,9 +14,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
14
14
|
- (void)expandToTarget:(nullable UIView *)targetView
|
|
15
15
|
resolve:(RCTPromiseResolveBlock)resolve;
|
|
16
16
|
|
|
17
|
-
- (void)collapseFromTarget:(nullable UIView *)targetView
|
|
18
|
-
resolve:(RCTPromiseResolveBlock)resolve;
|
|
19
|
-
|
|
20
17
|
/// Collapse using the stored target from the last expand call.
|
|
21
18
|
- (void)collapseWithResolve:(RCTPromiseResolveBlock)resolve;
|
|
22
19
|
|