@onekeyfe/react-native-native-overlay 6.7.0-alpha.359 → 6.7.0-alpha.362

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.
@@ -125,7 +125,11 @@ class NativeOverlayView(
125
125
 
126
126
  fun removeReactChild(child: View) {
127
127
  if (contentChild !== child) return
128
- (child.parent as? ViewGroup)?.removeView(child)
128
+ (child.parent as? ViewGroup)?.let { previous ->
129
+ previous.removeOnLayoutChangeListener(contentLayoutListener)
130
+ previous.removeView(child)
131
+ previous.endViewTransition(child)
132
+ }
129
133
  contentChild = null
130
134
  }
131
135
 
@@ -417,6 +421,8 @@ class NativeOverlayView(
417
421
  (child.parent as? ViewGroup)?.let { previous ->
418
422
  previous.removeOnLayoutChangeListener(contentLayoutListener)
419
423
  previous.removeView(child)
424
+ // Android retains a transitioning child's parent until the transition ends.
425
+ previous.endViewTransition(child)
420
426
  }
421
427
  container.addView(child)
422
428
  container.addOnLayoutChangeListener(contentLayoutListener)
@@ -1,8 +1,10 @@
1
1
  package com.onekey.nativeoverlay
2
2
 
3
+ import android.animation.ValueAnimator
3
4
  import android.graphics.Color
4
5
  import android.os.Looper
5
6
  import android.view.View
7
+ import android.view.ViewGroup
6
8
  import androidx.activity.ComponentActivity
7
9
  import androidx.coordinatorlayout.widget.CoordinatorLayout
8
10
  import com.facebook.react.bridge.BridgeReactContext
@@ -11,6 +13,7 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
11
13
  import org.junit.Assert.assertEquals
12
14
  import org.junit.Assert.assertFalse
13
15
  import org.junit.Assert.assertNotSame
16
+ import org.junit.Assert.assertNull
14
17
  import org.junit.Assert.assertSame
15
18
  import org.junit.Assert.assertTrue
16
19
  import org.junit.Test
@@ -67,10 +70,11 @@ class NativeOverlayMotionTest {
67
70
  val reactContext = BridgeReactContext(activity)
68
71
  reactContext.onHostResume(activity)
69
72
  val context = ThemedReactContext(reactContext, activity, "OverlayMotionTest", 1)
73
+ val child = View(context)
70
74
  val overlay = NativeOverlayView(context).apply {
71
75
  presentation = "sheet"
72
76
  sheetHeight = 200.0
73
- addReactChild(View(context))
77
+ addReactChild(child)
74
78
  }
75
79
  activity.setContentView(overlay)
76
80
  var dismissals = 0
@@ -80,13 +84,24 @@ class NativeOverlayMotionTest {
80
84
 
81
85
  val entryField = NativeOverlayView::class.java.getDeclaredField("entry").apply { isAccessible = true }
82
86
  val original = entryField.get(overlay) as NativeOverlayEntryRootView
87
+ Shadows.shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(350))
83
88
  overlay.visible = false
84
89
  overlay.commitConfiguration()
85
90
  assertTrue(original.isClosing)
91
+ val animatorField = NativeOverlayView::class.java.getDeclaredField("contentAnimator").apply { isAccessible = true }
92
+ val exitAnimator = animatorField.get(overlay) as ValueAnimator
93
+ assertTrue(exitAnimator.isStarted)
94
+ assertEquals(292L, exitAnimator.duration)
95
+ exitAnimator.currentPlayTime = exitAnimator.duration / 3
96
+ val alphaBeforeReopen = original.animatedView.alpha
97
+ assertTrue(alphaBeforeReopen > 0f && alphaBeforeReopen < 1f)
98
+ assertTrue(original.isClosing)
86
99
 
87
100
  overlay.visible = true
88
101
  overlay.commitConfiguration()
89
102
  assertSame(original, entryField.get(overlay))
103
+ assertSame(child, original.contentView.getChildAt(0))
104
+ assertEquals(alphaBeforeReopen, original.animatedView.alpha, 0.01f)
90
105
  assertTrue(original.isShownForInput)
91
106
  assertFalse(original.isClosing)
92
107
  assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO, original.contentView.importantForAccessibility)
@@ -98,4 +113,125 @@ class NativeOverlayMotionTest {
98
113
  Shadows.shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(350))
99
114
  assertEquals(1, dismissals)
100
115
  }
116
+
117
+ @Test
118
+ fun finishingExitMovesTransitioningContentBackToStagingView() {
119
+ val activity = Robolectric.buildActivity(ComponentActivity::class.java).setup().visible().get()
120
+ val reactContext = BridgeReactContext(activity)
121
+ reactContext.onHostResume(activity)
122
+ val context = ThemedReactContext(reactContext, activity, "OverlayMotionTest", 1)
123
+ val child = View(context)
124
+ val overlay = NativeOverlayView(context).apply {
125
+ addReactChild(child)
126
+ }
127
+ activity.setContentView(overlay)
128
+ var dismissals = 0
129
+ overlay.onDismissed = { dismissals++ }
130
+
131
+ overlay.visible = true
132
+ overlay.commitConfiguration()
133
+ val entryField = NativeOverlayView::class.java.getDeclaredField("entry").apply { isAccessible = true }
134
+ val entry = entryField.get(overlay) as NativeOverlayEntryRootView
135
+ Shadows.shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(350))
136
+
137
+ entry.contentView.startViewTransition(child)
138
+ overlay.visible = false
139
+ overlay.commitConfiguration()
140
+ Shadows.shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(350))
141
+ assertSame(overlay, child.parent)
142
+ assertEquals(0, entry.contentView.childCount)
143
+ assertEquals(1, overlay.childCount)
144
+ assertSame(child, overlay.getChildAt(0))
145
+ entry.contentView.endViewTransition(child)
146
+ assertSame(overlay, child.parent)
147
+ assertEquals(1, dismissals)
148
+ }
149
+
150
+ @Test
151
+ fun presentingMovesTransitioningContentIntoEntry() {
152
+ val activity = Robolectric.buildActivity(ComponentActivity::class.java).setup().visible().get()
153
+ val reactContext = BridgeReactContext(activity)
154
+ reactContext.onHostResume(activity)
155
+ val context = ThemedReactContext(reactContext, activity, "OverlayMotionTest", 1)
156
+ val child = View(context)
157
+ val overlay = NativeOverlayView(context).apply {
158
+ addReactChild(child)
159
+ }
160
+ activity.setContentView(overlay)
161
+
162
+ overlay.startViewTransition(child)
163
+ overlay.visible = true
164
+ overlay.commitConfiguration()
165
+
166
+ val entryField = NativeOverlayView::class.java.getDeclaredField("entry").apply { isAccessible = true }
167
+ val entry = entryField.get(overlay) as NativeOverlayEntryRootView
168
+ assertSame(entry.contentView, child.parent)
169
+ assertEquals(0, overlay.childCount)
170
+ assertEquals(1, entry.contentView.childCount)
171
+ assertSame(child, entry.contentView.getChildAt(0))
172
+ overlay.endViewTransition(child)
173
+ assertSame(entry.contentView, child.parent)
174
+ }
175
+
176
+ @Test
177
+ fun removingTransitioningStagedContentReleasesItsParentAndLayoutListener() {
178
+ val activity = Robolectric.buildActivity(ComponentActivity::class.java).setup().visible().get()
179
+ val reactContext = BridgeReactContext(activity)
180
+ reactContext.onHostResume(activity)
181
+ val context = ThemedReactContext(reactContext, activity, "OverlayMotionTest", 1)
182
+ val child = View(context)
183
+ val overlay = NativeOverlayView(context).apply { addReactChild(child) }
184
+ activity.setContentView(overlay)
185
+
186
+ overlay.startViewTransition(child)
187
+ overlay.removeReactChild(child)
188
+
189
+ assertNull(child.parent)
190
+ assertEquals(0, overlay.childCount)
191
+ assertEquals(0, overlay.getReactChildCount())
192
+ assertNoContentLayoutListener(overlay, overlay)
193
+ overlay.endViewTransition(child)
194
+ assertNull(child.parent)
195
+ }
196
+
197
+ @Test
198
+ fun replacingTransitioningPresentedContentReleasesTheRemovedChild() {
199
+ val activity = Robolectric.buildActivity(ComponentActivity::class.java).setup().visible().get()
200
+ val reactContext = BridgeReactContext(activity)
201
+ reactContext.onHostResume(activity)
202
+ val context = ThemedReactContext(reactContext, activity, "OverlayMotionTest", 1)
203
+ val child = View(context)
204
+ val overlay = NativeOverlayView(context).apply { addReactChild(child) }
205
+ activity.setContentView(overlay)
206
+ overlay.visible = true
207
+ overlay.commitConfiguration()
208
+ val container = child.parent as ViewGroup
209
+ container.startViewTransition(child)
210
+
211
+ val replacement = View(context)
212
+ overlay.addReactChild(replacement)
213
+
214
+ assertNull(child.parent)
215
+ assertSame(replacement, overlay.getReactChildAt(0))
216
+ assertSame(container, replacement.parent)
217
+ assertEquals(1, container.childCount)
218
+ container.endViewTransition(child)
219
+ assertSame(container, replacement.parent)
220
+
221
+ overlay.removeReactChild(replacement)
222
+ assertNoContentLayoutListener(container, overlay)
223
+ }
224
+
225
+ private fun assertNoContentLayoutListener(container: View, overlay: NativeOverlayView) {
226
+ val listener = NativeOverlayView::class.java.getDeclaredField("contentLayoutListener").apply {
227
+ isAccessible = true
228
+ }.get(overlay)
229
+ val listenerInfo = View::class.java.getDeclaredField("mListenerInfo").apply {
230
+ isAccessible = true
231
+ }.get(container) ?: return
232
+ val layoutListeners = listenerInfo.javaClass.getDeclaredField("mOnLayoutChangeListeners").apply {
233
+ isAccessible = true
234
+ }.get(listenerInfo) as? Collection<*>
235
+ assertFalse(layoutListeners?.contains(listener) == true)
236
+ }
101
237
  }
package/docs/SPEC.md CHANGED
@@ -232,6 +232,15 @@ request ─► queued ─► active ─► (enter anim) presented
232
232
  - Content stays mounted until `finalize`. This replaces today's hardcoded
233
233
  timers: the Dialog 300 ms portal destroy, the ColorPicker 300 ms, the
234
234
  ActionList and Select 150 ms waits, and the DialogLoading 50 ms delays.
235
+ - On Android, moving the same React child between its hidden staging view and
236
+ the entry host must release an in-progress transition held by the previous
237
+ parent before the new parent adds it. Completing that previous transition
238
+ later must not detach the child from its new parent. Animated closing moves
239
+ the child back after exit completion; reopening during exit keeps the same
240
+ child and entry without reporting a completed dismissal. Fabric removal or
241
+ replacement of the React child must also release the old container's layout
242
+ listener and in-progress transition, leaving the removed child without a
243
+ parent; a later transition completion must not affect replacement content.
235
244
  - A queued entry is promoted only after the previous entry in its lane is
236
245
  finalized, so exit and enter never overlap. `replace` is the exception: the
237
246
  new entry enters while the replaced one exits.
@@ -407,6 +416,11 @@ Automated (P0):
407
416
  blockers, input readiness, canceled key-up, global/page precedence,
408
417
  suspension and blocking-only accessibility isolation. It also checks live
409
418
  host reuse, Activity isolation and rebuilding after a cleared weak value.
419
+ - `NativeOverlayMotionTest` exercises the production Android view's transition
420
+ reparenting in both directions, including the exit-completion staging path,
421
+ old-parent transition completion after the move, and reopen during exit.
422
+ It also covers Fabric removal of staged content and replacement of presented
423
+ content during a transition, including layout-listener cleanup.
410
424
  - `src/__tests__/pageIsolation.web.test.ts` mounts a real React portal in a
411
425
  DOM host and verifies late suspension, level ordering and restoration after
412
426
  the blocking entry is removed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-native-overlay",
3
- "version": "6.7.0-alpha.359",
3
+ "version": "6.7.0-alpha.362",
4
4
  "description": "Native and Web overlay hosts with ordered levels, page scope and Fabric measurement",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",