@onekeyfe/react-native-native-overlay 3.0.162-alpha.272 → 6.7.0-12
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/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayAnimation.kt +5 -10
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayEntryRootView.kt +23 -2
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayHost.kt +17 -12
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayView.kt +3 -1
- package/android/src/test/java/com/onekey/nativeoverlay/NativeOverlayBackTest.kt +295 -0
- package/android/src/test/java/com/onekey/nativeoverlay/NativeOverlayMotionTest.kt +101 -0
- package/docs/SPEC.md +171 -30
- package/ios/NativeOverlayContainerView.swift +20 -7
- package/ios/NativeOverlaySheetController.swift +37 -1
- package/lib/module/OverlayStore.js +13 -1
- package/lib/module/OverlayStore.js.map +1 -1
- package/lib/module/OverlayView.web.js +104 -25
- package/lib/module/OverlayView.web.js.map +1 -1
- package/lib/module/useOverlayController.js +11 -3
- package/lib/module/useOverlayController.js.map +1 -1
- package/lib/module/web/animateTransition.js +20 -12
- package/lib/module/web/animateTransition.js.map +1 -1
- package/lib/module/web/overlayLayers.js +16 -0
- package/lib/module/web/overlayLayers.js.map +1 -1
- package/lib/module/web/useKeyboardShift.js +1 -1
- package/lib/module/web/useKeyboardShift.js.map +1 -1
- package/lib/module/web/useSheetDrag.js +22 -2
- package/lib/module/web/useSheetDrag.js.map +1 -1
- package/lib/typescript/src/OverlayStore.d.ts +2 -0
- package/lib/typescript/src/OverlayStore.d.ts.map +1 -1
- package/lib/typescript/src/OverlayView.web.d.ts.map +1 -1
- package/lib/typescript/src/useOverlayController.d.ts.map +1 -1
- package/lib/typescript/src/web/animateTransition.d.ts +2 -2
- package/lib/typescript/src/web/animateTransition.d.ts.map +1 -1
- package/lib/typescript/src/web/overlayLayers.d.ts +1 -0
- package/lib/typescript/src/web/overlayLayers.d.ts.map +1 -1
- package/lib/typescript/src/web/useSheetDrag.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/OverlayStore.ts +13 -1
- package/src/OverlayView.web.tsx +193 -56
- package/src/useOverlayController.ts +11 -4
- package/src/web/animateTransition.ts +30 -11
- package/src/web/overlayLayers.ts +20 -0
- package/src/web/useKeyboardShift.ts +1 -1
- package/src/web/useSheetDrag.ts +23 -2
|
@@ -69,8 +69,9 @@ internal sealed class OverlayMotion {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
/**
|
|
72
|
-
* Analytic step response of `m·x'' + c·x' + k·x = 0`,
|
|
73
|
-
*
|
|
72
|
+
* Analytic step response of `m·x'' + c·x' + k·x = 0`, with overdamped
|
|
73
|
+
* responses capped at critical damping to match UIKit's sheet response.
|
|
74
|
+
* Driving one ValueAnimator with it
|
|
74
75
|
* keeps alpha, translation, scale and backdrop on the same clock; androidx
|
|
75
76
|
* SpringAnimation would need one animation per property.
|
|
76
77
|
*/
|
|
@@ -89,19 +90,13 @@ internal class SpringInterpolator(private val spring: OverlayMotion.Spring) : Ti
|
|
|
89
90
|
|
|
90
91
|
fun progressAt(spring: OverlayMotion.Spring, t: Double): Double {
|
|
91
92
|
val w0 = sqrt(spring.stiffness / spring.mass)
|
|
92
|
-
val zeta = spring.damping / (2 * sqrt(spring.stiffness * spring.mass))
|
|
93
|
+
val zeta = minOf(1.0, spring.damping / (2 * sqrt(spring.stiffness * spring.mass)))
|
|
93
94
|
return when {
|
|
94
95
|
zeta < 1 -> {
|
|
95
96
|
val wd = w0 * sqrt(1 - zeta * zeta)
|
|
96
97
|
1 - exp(-zeta * w0 * t) * (cos(wd * t) + (zeta * w0 / wd) * sin(wd * t))
|
|
97
98
|
}
|
|
98
|
-
|
|
99
|
-
else -> {
|
|
100
|
-
val root = sqrt(zeta * zeta - 1)
|
|
101
|
-
val r1 = -w0 * (zeta - root)
|
|
102
|
-
val r2 = -w0 * (zeta + root)
|
|
103
|
-
1 - (r2 * exp(r1 * t) - r1 * exp(r2 * t)) / (r2 - r1)
|
|
104
|
-
}
|
|
99
|
+
else -> 1 - exp(-w0 * t) * (1 + w0 * t)
|
|
105
100
|
}
|
|
106
101
|
}
|
|
107
102
|
|
|
@@ -60,7 +60,12 @@ internal class NativeOverlayEntryRootView(
|
|
|
60
60
|
* below), non-blocking ones let it continue past the empty area.
|
|
61
61
|
*/
|
|
62
62
|
override val pointerEvents: PointerEvents
|
|
63
|
-
get() =
|
|
63
|
+
get() = when {
|
|
64
|
+
blocking && isClosing -> PointerEvents.BOX_ONLY
|
|
65
|
+
isClosing -> PointerEvents.NONE
|
|
66
|
+
blocking && isShownForInput -> PointerEvents.AUTO
|
|
67
|
+
else -> PointerEvents.BOX_NONE
|
|
68
|
+
}
|
|
64
69
|
var stackOrder = 0
|
|
65
70
|
var levelOrder = 0
|
|
66
71
|
/** Page scope: the owning page, used by the page host to hide the entry. */
|
|
@@ -74,6 +79,19 @@ internal class NativeOverlayEntryRootView(
|
|
|
74
79
|
var dismissOnBackPress = true
|
|
75
80
|
var dismissOnBackdropPress = false
|
|
76
81
|
var isShownForInput = false
|
|
82
|
+
private var contentAccessibilityBeforeClose: Int? = null
|
|
83
|
+
var isClosing = false
|
|
84
|
+
set(value) {
|
|
85
|
+
if (field == value) return
|
|
86
|
+
field = value
|
|
87
|
+
if (value) {
|
|
88
|
+
contentAccessibilityBeforeClose = contentView.importantForAccessibility
|
|
89
|
+
contentView.importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
|
|
90
|
+
} else {
|
|
91
|
+
contentAccessibilityBeforeClose?.let { contentView.importantForAccessibility = it }
|
|
92
|
+
contentAccessibilityBeforeClose = null
|
|
93
|
+
}
|
|
94
|
+
}
|
|
77
95
|
var onRequestDismiss: ((String) -> Unit)? = null
|
|
78
96
|
|
|
79
97
|
val backdropView = View(reactContext).apply { alpha = 0f }
|
|
@@ -90,12 +108,13 @@ internal class NativeOverlayEntryRootView(
|
|
|
90
108
|
/** `sheet` presentation: the draggable surface; otherwise null. */
|
|
91
109
|
var sheetView: FrameLayout? = null
|
|
92
110
|
private set
|
|
111
|
+
private var sheetAnimationView: CoordinatorLayout? = null
|
|
93
112
|
private var sheetBehavior: BottomSheetBehavior<FrameLayout>? = null
|
|
94
113
|
private var handleView: View? = null
|
|
95
114
|
var onSheetHiddenByPan: (() -> Unit)? = null
|
|
96
115
|
|
|
97
116
|
/** The view that enter / exit animations move. */
|
|
98
|
-
val animatedView: View get() =
|
|
117
|
+
val animatedView: View get() = sheetAnimationView ?: contentView
|
|
99
118
|
|
|
100
119
|
private val touchDispatcher = JSTouchDispatcher(this)
|
|
101
120
|
private val pointerDispatcher = if (ReactFeatureFlags.dispatchPointerEvents) {
|
|
@@ -220,6 +239,7 @@ internal class NativeOverlayEntryRootView(
|
|
|
220
239
|
},
|
|
221
240
|
)
|
|
222
241
|
sheetView = sheet
|
|
242
|
+
sheetAnimationView = coordinator
|
|
223
243
|
sheetBehavior = behavior
|
|
224
244
|
handleView = handle
|
|
225
245
|
return sheet
|
|
@@ -350,6 +370,7 @@ internal class NativeOverlayEntryRootView(
|
|
|
350
370
|
}
|
|
351
371
|
|
|
352
372
|
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
|
|
373
|
+
if (isClosing) return blocking
|
|
353
374
|
if (!isShownForInput) return false
|
|
354
375
|
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
|
|
355
376
|
// Non-blocking overlays (toasts) only take touches that land on content.
|
|
@@ -8,6 +8,7 @@ import android.widget.FrameLayout
|
|
|
8
8
|
import androidx.activity.ComponentActivity
|
|
9
9
|
import androidx.activity.OnBackPressedCallback
|
|
10
10
|
import java.util.WeakHashMap
|
|
11
|
+
import java.lang.ref.WeakReference
|
|
11
12
|
|
|
12
13
|
/** Mirrors `OVERLAY_LEVEL_ORDER`. */
|
|
13
14
|
internal enum class NativeOverlayLevel(val order: Int) {
|
|
@@ -48,8 +49,8 @@ internal class NativeOverlayHost private constructor(
|
|
|
48
49
|
)
|
|
49
50
|
private val backCallback = object : OnBackPressedCallback(false) {
|
|
50
51
|
override fun handleOnBackPressed() {
|
|
51
|
-
|
|
52
|
-
if (entry.dismissOnBackPress) entry.onRequestDismiss?.invoke("back")
|
|
52
|
+
topBackEntry()?.let { entry ->
|
|
53
|
+
if (!entry.isClosing && entry.dismissOnBackPress) entry.onRequestDismiss?.invoke("back")
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
56
|
}
|
|
@@ -66,8 +67,8 @@ internal class NativeOverlayHost private constructor(
|
|
|
66
67
|
* predictive back, so the dispatcher alone never runs first.
|
|
67
68
|
*/
|
|
68
69
|
fun handleBackKey(event: KeyEvent): Boolean {
|
|
69
|
-
val entry =
|
|
70
|
-
if (event.action == KeyEvent.ACTION_UP && !event.isCanceled && entry.dismissOnBackPress) {
|
|
70
|
+
val entry = topBackEntry() ?: return false
|
|
71
|
+
if (event.action == KeyEvent.ACTION_UP && !event.isCanceled && !entry.isClosing && entry.dismissOnBackPress) {
|
|
71
72
|
entry.onRequestDismiss?.invoke("back")
|
|
72
73
|
}
|
|
73
74
|
return true
|
|
@@ -108,14 +109,14 @@ internal class NativeOverlayHost private constructor(
|
|
|
108
109
|
}
|
|
109
110
|
|
|
110
111
|
fun onEntriesChanged() {
|
|
111
|
-
val
|
|
112
|
-
if (
|
|
112
|
+
val hasBackEntry = topBackEntry() != null
|
|
113
|
+
if (hasBackEntry && !backCallback.isEnabled) {
|
|
113
114
|
// The most recently added callback wins; re-add so ours runs before
|
|
114
115
|
// ReactActivity's and react-native-screens' callbacks.
|
|
115
116
|
backCallback.remove()
|
|
116
117
|
(activity as? ComponentActivity)?.onBackPressedDispatcher?.addCallback(backCallback)
|
|
117
118
|
}
|
|
118
|
-
backCallback.isEnabled =
|
|
119
|
+
backCallback.isEnabled = hasBackEntry
|
|
119
120
|
updateAccessibility()
|
|
120
121
|
}
|
|
121
122
|
|
|
@@ -133,11 +134,13 @@ internal class NativeOverlayHost private constructor(
|
|
|
133
134
|
}
|
|
134
135
|
|
|
135
136
|
/** Global overlays win; otherwise the topmost visible page overlay. */
|
|
136
|
-
private fun
|
|
137
|
-
orderedEntries().lastOrNull {
|
|
137
|
+
private fun topBackEntry(): NativeOverlayEntryRootView? =
|
|
138
|
+
orderedEntries().lastOrNull {
|
|
139
|
+
(it.blocking || it.dismissOnBackPress) && it.isShownForInput && (!it.isClosing || it.blocking)
|
|
140
|
+
}
|
|
138
141
|
?: shownPageHosts()
|
|
139
142
|
.flatMap { it.shownEntries() }
|
|
140
|
-
.lastOrNull { it.blocking }
|
|
143
|
+
.lastOrNull { (it.blocking || it.dismissOnBackPress) && (!it.isClosing || it.blocking) }
|
|
141
144
|
|
|
142
145
|
private fun shownPageHosts(): List<NativeOverlayPageHostView> =
|
|
143
146
|
NativeOverlayPageHostView.allHosts().filter {
|
|
@@ -160,9 +163,11 @@ internal class NativeOverlayHost private constructor(
|
|
|
160
163
|
}
|
|
161
164
|
|
|
162
165
|
companion object {
|
|
163
|
-
private val hosts = WeakHashMap<Activity, NativeOverlayHost
|
|
166
|
+
private val hosts = WeakHashMap<Activity, WeakReference<NativeOverlayHost>>()
|
|
164
167
|
|
|
165
168
|
fun of(activity: Activity): NativeOverlayHost =
|
|
166
|
-
hosts
|
|
169
|
+
hosts[activity]?.get() ?: NativeOverlayHost(activity).also {
|
|
170
|
+
hosts[activity] = WeakReference(it)
|
|
171
|
+
}
|
|
167
172
|
}
|
|
168
173
|
}
|
|
@@ -200,6 +200,7 @@ class NativeOverlayView(
|
|
|
200
200
|
}
|
|
201
201
|
cancelAnimators()
|
|
202
202
|
phase = Phase.ENTERING
|
|
203
|
+
target.isClosing = false
|
|
203
204
|
target.isShownForInput = true
|
|
204
205
|
notifyHostEntriesChanged()
|
|
205
206
|
if (blocking) {
|
|
@@ -226,7 +227,7 @@ class NativeOverlayView(
|
|
|
226
227
|
val target = entry ?: return finishDismiss("programmatic")
|
|
227
228
|
cancelAnimators()
|
|
228
229
|
phase = Phase.EXITING
|
|
229
|
-
target.
|
|
230
|
+
target.isClosing = true
|
|
230
231
|
notifyHostEntriesChanged()
|
|
231
232
|
val transition = config.exit.reduceMotionAdjusted(isReduceMotionEnabled())
|
|
232
233
|
if (transition.kind == "none") return finishDismiss("programmatic")
|
|
@@ -241,6 +242,7 @@ class NativeOverlayView(
|
|
|
241
242
|
phase = Phase.HIDDEN
|
|
242
243
|
entry?.let { target ->
|
|
243
244
|
target.isShownForInput = false
|
|
245
|
+
target.isClosing = false
|
|
244
246
|
stageContent()
|
|
245
247
|
host?.detach(target)
|
|
246
248
|
pageHost?.detach(target)
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
package com.onekey.nativeoverlay
|
|
2
|
+
|
|
3
|
+
import android.view.KeyEvent
|
|
4
|
+
import android.view.MotionEvent
|
|
5
|
+
import android.view.View
|
|
6
|
+
import android.widget.FrameLayout
|
|
7
|
+
import androidx.activity.ComponentActivity
|
|
8
|
+
import com.facebook.react.bridge.BridgeReactContext
|
|
9
|
+
import com.facebook.react.uimanager.PointerEvents
|
|
10
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
11
|
+
import com.facebook.react.uimanager.TouchTargetHelper
|
|
12
|
+
import com.facebook.react.views.view.ReactViewGroup
|
|
13
|
+
import java.lang.ref.WeakReference
|
|
14
|
+
import java.util.WeakHashMap
|
|
15
|
+
import org.junit.Assert.assertEquals
|
|
16
|
+
import org.junit.Assert.assertFalse
|
|
17
|
+
import org.junit.Assert.assertNotSame
|
|
18
|
+
import org.junit.Assert.assertSame
|
|
19
|
+
import org.junit.Assert.assertTrue
|
|
20
|
+
import org.junit.Before
|
|
21
|
+
import org.junit.Test
|
|
22
|
+
import org.junit.runner.RunWith
|
|
23
|
+
import org.robolectric.Robolectric
|
|
24
|
+
import org.robolectric.RobolectricTestRunner
|
|
25
|
+
import org.robolectric.annotation.Config
|
|
26
|
+
|
|
27
|
+
@RunWith(RobolectricTestRunner::class)
|
|
28
|
+
@Config(sdk = [34])
|
|
29
|
+
class NativeOverlayBackTest {
|
|
30
|
+
private lateinit var activity: ComponentActivity
|
|
31
|
+
private lateinit var context: ThemedReactContext
|
|
32
|
+
private lateinit var host: NativeOverlayHost
|
|
33
|
+
private val dismissals = mutableListOf<String>()
|
|
34
|
+
|
|
35
|
+
@Before
|
|
36
|
+
fun setUp() {
|
|
37
|
+
activity = Robolectric.buildActivity(ComponentActivity::class.java).setup().visible().get()
|
|
38
|
+
val reactContext = BridgeReactContext(activity)
|
|
39
|
+
reactContext.onHostResume(activity)
|
|
40
|
+
context = ThemedReactContext(reactContext, activity, "OverlayBackTest", 1)
|
|
41
|
+
host = NativeOverlayHost.of(activity)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private fun entry(name: String, blocking: Boolean, dismissible: Boolean, order: Int = 0) =
|
|
45
|
+
NativeOverlayEntryRootView(context).apply {
|
|
46
|
+
this.blocking = blocking
|
|
47
|
+
dismissOnBackPress = dismissible
|
|
48
|
+
stackOrder = order
|
|
49
|
+
isShownForInput = true
|
|
50
|
+
onRequestDismiss = { dismissals.add("$name:$it") }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private fun back(action: Int = KeyEvent.ACTION_UP, flags: Int = 0) =
|
|
54
|
+
host.handleBackKey(KeyEvent(0L, 0L, action, KeyEvent.KEYCODE_BACK, 0, 0, 0, 0, flags))
|
|
55
|
+
|
|
56
|
+
@Test
|
|
57
|
+
fun dismissibleNonBlockingGlobalEntryWinsOverLowerBlocker() {
|
|
58
|
+
host.attach(entry("lower", true, true), NativeOverlayLevel.MODAL)
|
|
59
|
+
host.attach(entry("top", false, true), NativeOverlayLevel.TOAST)
|
|
60
|
+
assertTrue(back(KeyEvent.ACTION_DOWN))
|
|
61
|
+
assertEquals(emptyList<String>(), dismissals)
|
|
62
|
+
assertTrue(back())
|
|
63
|
+
assertEquals(listOf("top:back"), dismissals)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@Test
|
|
67
|
+
fun dispatcherHandlesNonBlockingEntryAndKeepsPageAccessible() {
|
|
68
|
+
val page = View(activity).apply { importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES }
|
|
69
|
+
activity.setContentView(page)
|
|
70
|
+
val top = entry("top", false, true)
|
|
71
|
+
host.attach(top, NativeOverlayLevel.MODAL)
|
|
72
|
+
activity.onBackPressedDispatcher.onBackPressed()
|
|
73
|
+
assertEquals(listOf("top:back"), dismissals)
|
|
74
|
+
assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_YES, page.importantForAccessibility)
|
|
75
|
+
host.detach(top)
|
|
76
|
+
assertFalse(back())
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@Test
|
|
80
|
+
fun nonDismissibleBlockerConsumesBackWithoutClosingLowerEntry() {
|
|
81
|
+
host.attach(entry("lower", false, true), NativeOverlayLevel.MODAL)
|
|
82
|
+
host.attach(entry("lock", true, false), NativeOverlayLevel.LOCK)
|
|
83
|
+
assertTrue(back())
|
|
84
|
+
assertEquals(emptyList<String>(), dismissals)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@Test
|
|
88
|
+
fun passiveOrNotInputReadyEntriesDoNotTakeBack() {
|
|
89
|
+
host.attach(entry("passive", false, false), NativeOverlayLevel.TOAST)
|
|
90
|
+
host.attach(entry("hidden", true, true).apply { isShownForInput = false }, NativeOverlayLevel.LOCK)
|
|
91
|
+
assertFalse(back())
|
|
92
|
+
assertEquals(emptyList<String>(), dismissals)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@Test
|
|
96
|
+
fun canceledKeyUpDoesNotDismiss() {
|
|
97
|
+
host.attach(entry("top", false, true), NativeOverlayLevel.MODAL)
|
|
98
|
+
assertTrue(back(flags = KeyEvent.FLAG_CANCELED))
|
|
99
|
+
assertEquals(emptyList<String>(), dismissals)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@Test
|
|
103
|
+
fun dismissiblePageEntryWinsOverLowerPageBlockerAndSuspensionSkipsIt() {
|
|
104
|
+
val pageHost = NativeOverlayPageHostView(context).apply { hostKey = "page-back-test" }
|
|
105
|
+
activity.setContentView(pageHost)
|
|
106
|
+
val lower = entry("lower", true, true).apply { ownerKey = "owner" }
|
|
107
|
+
val top = entry("top", false, true, 1).apply { ownerKey = "owner" }
|
|
108
|
+
pageHost.attach(lower)
|
|
109
|
+
pageHost.attach(top)
|
|
110
|
+
assertTrue(back())
|
|
111
|
+
assertEquals(listOf("top:back"), dismissals)
|
|
112
|
+
pageHost.setSuspendedOwners("owner")
|
|
113
|
+
assertFalse(back())
|
|
114
|
+
pageHost.invalidateHost()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@Test
|
|
118
|
+
fun globalEntryTakesBackBeforePageEntryAndDismissalRestoresPageTarget() {
|
|
119
|
+
val pageHost = NativeOverlayPageHostView(context).apply { hostKey = "page-global-test" }
|
|
120
|
+
activity.setContentView(FrameLayout(activity).apply { addView(pageHost) })
|
|
121
|
+
pageHost.attach(entry("page", false, true))
|
|
122
|
+
val global = entry("global", false, true)
|
|
123
|
+
host.attach(global, NativeOverlayLevel.MODAL)
|
|
124
|
+
assertTrue(back())
|
|
125
|
+
host.detach(global)
|
|
126
|
+
assertTrue(back())
|
|
127
|
+
assertEquals(listOf("global:back", "page:back"), dismissals)
|
|
128
|
+
pageHost.invalidateHost()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@Test
|
|
132
|
+
fun closingGlobalBlockerKeepsTouchBackAndAccessibilityOwnership() {
|
|
133
|
+
var pageTouches = 0
|
|
134
|
+
val page = View(activity).apply {
|
|
135
|
+
setOnTouchListener { _, _ ->
|
|
136
|
+
pageTouches += 1
|
|
137
|
+
true
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
activity.setContentView(page)
|
|
141
|
+
val lower = entry("lower", true, true)
|
|
142
|
+
val top = entry("top", true, true, 1)
|
|
143
|
+
host.attach(lower, NativeOverlayLevel.MODAL)
|
|
144
|
+
host.attach(top, NativeOverlayLevel.MODAL)
|
|
145
|
+
top.isClosing = true
|
|
146
|
+
host.onEntriesChanged()
|
|
147
|
+
|
|
148
|
+
assertEquals(PointerEvents.BOX_ONLY, top.pointerEvents)
|
|
149
|
+
assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS, top.contentView.importantForAccessibility)
|
|
150
|
+
assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS, page.importantForAccessibility)
|
|
151
|
+
assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS, lower.importantForAccessibility)
|
|
152
|
+
assertTrue(back())
|
|
153
|
+
assertEquals(emptyList<String>(), dismissals)
|
|
154
|
+
|
|
155
|
+
val content = activity.findViewById<FrameLayout>(android.R.id.content)
|
|
156
|
+
val size = View.MeasureSpec.makeMeasureSpec(300, View.MeasureSpec.EXACTLY)
|
|
157
|
+
content.measure(size, size)
|
|
158
|
+
content.layout(0, 0, 300, 300)
|
|
159
|
+
val down = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 150f, 150f, 0)
|
|
160
|
+
try {
|
|
161
|
+
assertTrue(content.dispatchTouchEvent(down))
|
|
162
|
+
assertEquals(0, pageTouches)
|
|
163
|
+
} finally {
|
|
164
|
+
down.recycle()
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
top.isClosing = false
|
|
168
|
+
host.onEntriesChanged()
|
|
169
|
+
assertEquals(PointerEvents.AUTO, top.pointerEvents)
|
|
170
|
+
assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO, top.contentView.importantForAccessibility)
|
|
171
|
+
assertTrue(back())
|
|
172
|
+
assertEquals(listOf("top:back"), dismissals)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
@Test
|
|
176
|
+
fun closingPageBlockerStillTakesBack() {
|
|
177
|
+
val pageHost = NativeOverlayPageHostView(context).apply { hostKey = "closing-page" }
|
|
178
|
+
activity.setContentView(pageHost)
|
|
179
|
+
val top = entry("page", true, true)
|
|
180
|
+
pageHost.attach(top)
|
|
181
|
+
top.isClosing = true
|
|
182
|
+
host.onEntriesChanged()
|
|
183
|
+
|
|
184
|
+
assertEquals(PointerEvents.BOX_ONLY, top.pointerEvents)
|
|
185
|
+
assertTrue(back())
|
|
186
|
+
assertEquals(emptyList<String>(), dismissals)
|
|
187
|
+
top.isClosing = false
|
|
188
|
+
host.onEntriesChanged()
|
|
189
|
+
assertEquals(PointerEvents.AUTO, top.pointerEvents)
|
|
190
|
+
assertTrue(back())
|
|
191
|
+
assertEquals(listOf("page:back"), dismissals)
|
|
192
|
+
pageHost.invalidateHost()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
@Test
|
|
196
|
+
fun pageEntryHitTestingKeepsClosingContentAndLowerPageSeparate() {
|
|
197
|
+
val root = ReactViewGroup(context).apply { id = 101 }
|
|
198
|
+
val below = ReactViewGroup(context).apply { id = 102 }
|
|
199
|
+
val child = ReactViewGroup(context).apply { id = 103 }
|
|
200
|
+
val pageHost = NativeOverlayPageHostView(context).apply { hostKey = "page-hit-test" }
|
|
201
|
+
root.addView(below)
|
|
202
|
+
root.addView(pageHost)
|
|
203
|
+
activity.setContentView(root)
|
|
204
|
+
val overlay = entry("page", true, true)
|
|
205
|
+
overlay.contentView.addView(child)
|
|
206
|
+
pageHost.attach(overlay)
|
|
207
|
+
|
|
208
|
+
val size = View.MeasureSpec.makeMeasureSpec(300, View.MeasureSpec.EXACTLY)
|
|
209
|
+
root.measure(size, size)
|
|
210
|
+
root.layout(0, 0, 300, 300)
|
|
211
|
+
below.layout(0, 0, 300, 300)
|
|
212
|
+
pageHost.layout(0, 0, 300, 300)
|
|
213
|
+
child.layout(0, 0, 300, 300)
|
|
214
|
+
|
|
215
|
+
fun target() = TouchTargetHelper.findTargetTagForTouch(150f, 150f, root)
|
|
216
|
+
assertEquals(child.id, target())
|
|
217
|
+
overlay.isClosing = true
|
|
218
|
+
assertEquals(root.id, target())
|
|
219
|
+
overlay.updateBlocking(false)
|
|
220
|
+
assertEquals(below.id, target())
|
|
221
|
+
overlay.updateBlocking(true)
|
|
222
|
+
overlay.isClosing = false
|
|
223
|
+
assertEquals(child.id, target())
|
|
224
|
+
pageHost.invalidateHost()
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
@Test
|
|
228
|
+
fun reopeningCoveredClosingEntryRestoresItsContentAccessibility() {
|
|
229
|
+
activity.setContentView(View(activity))
|
|
230
|
+
val first = entry("first", true, true)
|
|
231
|
+
val second = entry("second", true, true)
|
|
232
|
+
host.attach(first, NativeOverlayLevel.MODAL)
|
|
233
|
+
first.isClosing = true
|
|
234
|
+
host.onEntriesChanged()
|
|
235
|
+
host.attach(second, NativeOverlayLevel.SECURE)
|
|
236
|
+
|
|
237
|
+
first.isClosing = false
|
|
238
|
+
host.onEntriesChanged()
|
|
239
|
+
assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS, first.importantForAccessibility)
|
|
240
|
+
host.detach(second)
|
|
241
|
+
assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO, first.importantForAccessibility)
|
|
242
|
+
assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO, first.contentView.importantForAccessibility)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
@Test
|
|
246
|
+
fun closingNonBlockingEntryPassesTouchAndBackToLowerBlocker() {
|
|
247
|
+
val lower = entry("lower", true, true)
|
|
248
|
+
val toast = entry("toast", false, true)
|
|
249
|
+
host.attach(lower, NativeOverlayLevel.MODAL)
|
|
250
|
+
host.attach(toast, NativeOverlayLevel.TOAST)
|
|
251
|
+
toast.isClosing = true
|
|
252
|
+
host.onEntriesChanged()
|
|
253
|
+
|
|
254
|
+
assertEquals(PointerEvents.NONE, toast.pointerEvents)
|
|
255
|
+
val down = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 150f, 150f, 0)
|
|
256
|
+
try {
|
|
257
|
+
assertFalse(toast.dispatchTouchEvent(down))
|
|
258
|
+
} finally {
|
|
259
|
+
down.recycle()
|
|
260
|
+
}
|
|
261
|
+
assertTrue(back())
|
|
262
|
+
assertEquals(listOf("lower:back"), dismissals)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
@Test
|
|
266
|
+
fun hostCacheReusesLiveHostAndSeparatesActivities() {
|
|
267
|
+
assertSame(host, NativeOverlayHost.of(activity))
|
|
268
|
+
val otherActivity = Robolectric.buildActivity(ComponentActivity::class.java).setup().get()
|
|
269
|
+
assertNotSame(host, NativeOverlayHost.of(otherActivity))
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
@Test
|
|
273
|
+
fun hostCacheRebuildsAfterWeakValueIsCleared() {
|
|
274
|
+
val field = NativeOverlayHost::class.java.getDeclaredField("hosts").apply {
|
|
275
|
+
isAccessible = true
|
|
276
|
+
}
|
|
277
|
+
val cache = field.get(null) as WeakHashMap<*, *>
|
|
278
|
+
(cache[activity] as WeakReference<*>).clear()
|
|
279
|
+
val replacement = NativeOverlayHost.of(activity)
|
|
280
|
+
assertNotSame(host, replacement)
|
|
281
|
+
assertSame(replacement, NativeOverlayHost.of(activity))
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
@Test
|
|
285
|
+
fun pageOnlyBackInterceptorRetainsItsHost() {
|
|
286
|
+
val pageHost = NativeOverlayPageHostView(context).apply { hostKey = "page-only" }
|
|
287
|
+
activity.setContentView(pageHost)
|
|
288
|
+
pageHost.attach(entry("page", false, true))
|
|
289
|
+
host.installBackInterceptor()
|
|
290
|
+
assertTrue(activity.window.callback.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK)))
|
|
291
|
+
assertEquals(listOf("page:back"), dismissals)
|
|
292
|
+
assertSame(host, NativeOverlayHost.of(activity))
|
|
293
|
+
pageHost.invalidateHost()
|
|
294
|
+
}
|
|
295
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
package com.onekey.nativeoverlay
|
|
2
|
+
|
|
3
|
+
import android.graphics.Color
|
|
4
|
+
import android.os.Looper
|
|
5
|
+
import android.view.View
|
|
6
|
+
import androidx.activity.ComponentActivity
|
|
7
|
+
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
|
8
|
+
import com.facebook.react.bridge.BridgeReactContext
|
|
9
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
10
|
+
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|
11
|
+
import org.junit.Assert.assertEquals
|
|
12
|
+
import org.junit.Assert.assertFalse
|
|
13
|
+
import org.junit.Assert.assertNotSame
|
|
14
|
+
import org.junit.Assert.assertSame
|
|
15
|
+
import org.junit.Assert.assertTrue
|
|
16
|
+
import org.junit.Test
|
|
17
|
+
import org.junit.runner.RunWith
|
|
18
|
+
import org.robolectric.Robolectric
|
|
19
|
+
import org.robolectric.RobolectricTestRunner
|
|
20
|
+
import org.robolectric.Shadows
|
|
21
|
+
import org.robolectric.annotation.Config
|
|
22
|
+
import java.time.Duration
|
|
23
|
+
|
|
24
|
+
@RunWith(RobolectricTestRunner::class)
|
|
25
|
+
@Config(sdk = [34])
|
|
26
|
+
class NativeOverlayMotionTest {
|
|
27
|
+
@Test
|
|
28
|
+
fun sheetAnimationMovesCoordinatorWithoutTakingMaterialsSheetTranslation() {
|
|
29
|
+
val activity = Robolectric.buildActivity(ComponentActivity::class.java).setup().get()
|
|
30
|
+
val reactContext = BridgeReactContext(activity)
|
|
31
|
+
reactContext.onHostResume(activity)
|
|
32
|
+
val context = ThemedReactContext(reactContext, activity, "OverlayMotionTest", 1)
|
|
33
|
+
val entry = NativeOverlayEntryRootView(context)
|
|
34
|
+
assertSame(entry.contentView, entry.animatedView)
|
|
35
|
+
|
|
36
|
+
entry.configureSheet(500, 16f, Color.WHITE, showHandle = true, draggable = true)
|
|
37
|
+
val sheet = entry.sheetView!!
|
|
38
|
+
val animated = entry.animatedView
|
|
39
|
+
assertTrue(animated is CoordinatorLayout)
|
|
40
|
+
assertSame(animated, sheet.parent)
|
|
41
|
+
assertNotSame(sheet, animated)
|
|
42
|
+
assertTrue((sheet.layoutParams as CoordinatorLayout.LayoutParams).behavior is BottomSheetBehavior<*>)
|
|
43
|
+
|
|
44
|
+
animated.translationY = 42f
|
|
45
|
+
assertEquals(0f, sheet.translationY)
|
|
46
|
+
sheet.translationY = 7f
|
|
47
|
+
assertEquals(42f, animated.translationY)
|
|
48
|
+
assertEquals(7f, sheet.translationY)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@Test
|
|
52
|
+
fun quickMatchesMeasuredCriticalResponseWithoutChangingUnderdampedResponse() {
|
|
53
|
+
val quick = OverlayMotion.QUICK
|
|
54
|
+
assertEquals(292L, quick.durationMs())
|
|
55
|
+
assertEquals(0.8238140348, SpringInterpolator.progressAt(quick, 0.1), 0.000001)
|
|
56
|
+
assertEquals(0.9990003945, SpringInterpolator.progressAt(quick, 0.292), 0.000001)
|
|
57
|
+
|
|
58
|
+
val underdamped = OverlayMotion.Spring(1.0, 100.0, 5.0)
|
|
59
|
+
assertEquals(0.3929451508, SpringInterpolator.progressAt(underdamped, 0.1), 0.000001)
|
|
60
|
+
val timing = OverlayMotion.Timing(250L, 0.25f, 0.1f, 0.25f, 1f)
|
|
61
|
+
assertEquals(250L, timing.durationMs())
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@Test
|
|
65
|
+
fun reopeningTheSameEntryDuringExitRetargetsItsAnimation() {
|
|
66
|
+
val activity = Robolectric.buildActivity(ComponentActivity::class.java).setup().visible().get()
|
|
67
|
+
val reactContext = BridgeReactContext(activity)
|
|
68
|
+
reactContext.onHostResume(activity)
|
|
69
|
+
val context = ThemedReactContext(reactContext, activity, "OverlayMotionTest", 1)
|
|
70
|
+
val overlay = NativeOverlayView(context).apply {
|
|
71
|
+
presentation = "sheet"
|
|
72
|
+
sheetHeight = 200.0
|
|
73
|
+
addReactChild(View(context))
|
|
74
|
+
}
|
|
75
|
+
activity.setContentView(overlay)
|
|
76
|
+
var dismissals = 0
|
|
77
|
+
overlay.onDismissed = { dismissals++ }
|
|
78
|
+
overlay.visible = true
|
|
79
|
+
overlay.commitConfiguration()
|
|
80
|
+
|
|
81
|
+
val entryField = NativeOverlayView::class.java.getDeclaredField("entry").apply { isAccessible = true }
|
|
82
|
+
val original = entryField.get(overlay) as NativeOverlayEntryRootView
|
|
83
|
+
overlay.visible = false
|
|
84
|
+
overlay.commitConfiguration()
|
|
85
|
+
assertTrue(original.isClosing)
|
|
86
|
+
|
|
87
|
+
overlay.visible = true
|
|
88
|
+
overlay.commitConfiguration()
|
|
89
|
+
assertSame(original, entryField.get(overlay))
|
|
90
|
+
assertTrue(original.isShownForInput)
|
|
91
|
+
assertFalse(original.isClosing)
|
|
92
|
+
assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO, original.contentView.importantForAccessibility)
|
|
93
|
+
Shadows.shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(350))
|
|
94
|
+
assertEquals(0, dismissals)
|
|
95
|
+
|
|
96
|
+
overlay.visible = false
|
|
97
|
+
overlay.commitConfiguration()
|
|
98
|
+
Shadows.shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(350))
|
|
99
|
+
assertEquals(1, dismissals)
|
|
100
|
+
}
|
|
101
|
+
}
|