react-native-theme-reveal 0.0.0-stage → 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 +734 -2
- package/android/build.gradle +32 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/baseline-prof.txt +4 -0
- package/android/src/main/java/expo/modules/themereveal/CapturePlan.kt +199 -0
- package/android/src/main/java/expo/modules/themereveal/CaptureRules.kt +50 -0
- package/android/src/main/java/expo/modules/themereveal/GpuSnapshot.kt +367 -0
- package/android/src/main/java/expo/modules/themereveal/KeptBitmaps.kt +102 -0
- package/android/src/main/java/expo/modules/themereveal/KeptTarget.kt +103 -0
- package/android/src/main/java/expo/modules/themereveal/Motion.kt +79 -0
- package/android/src/main/java/expo/modules/themereveal/Once.kt +13 -0
- package/android/src/main/java/expo/modules/themereveal/RenderThreadReveal.kt +107 -0
- package/android/src/main/java/expo/modules/themereveal/RevealOverlayView.kt +192 -0
- package/android/src/main/java/expo/modules/themereveal/SecondSnapshotWait.kt +112 -0
- package/android/src/main/java/expo/modules/themereveal/ShapePath.kt +47 -0
- package/android/src/main/java/expo/modules/themereveal/ThemeRevealModule.kt +904 -0
- package/android/src/main/java/expo/modules/themereveal/TransitionArgs.kt +50 -0
- package/android/src/main/java/expo/modules/themereveal/WindowSnapshot.kt +536 -0
- package/build/ThemeRevealModule.d.ts +11 -0
- package/build/ThemeRevealModule.d.ts.map +1 -0
- package/build/ThemeRevealModule.js +11 -0
- package/build/ThemeRevealModule.js.map +1 -0
- package/build/createThemeReveal.d.ts +39 -0
- package/build/createThemeReveal.d.ts.map +1 -0
- package/build/createThemeReveal.js +186 -0
- package/build/createThemeReveal.js.map +1 -0
- package/build/index.d.ts +9 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +5 -0
- package/build/index.js.map +1 -0
- package/build/originFromPress.d.ts +12 -0
- package/build/originFromPress.d.ts.map +1 -0
- package/build/originFromPress.js +26 -0
- package/build/originFromPress.js.map +1 -0
- package/build/resolveTransition.d.ts +13 -0
- package/build/resolveTransition.d.ts.map +1 -0
- package/build/resolveTransition.js +43 -0
- package/build/resolveTransition.js.map +1 -0
- package/build/store.d.ts +8 -0
- package/build/store.d.ts.map +1 -0
- package/build/store.js +23 -0
- package/build/store.js.map +1 -0
- package/build/types.d.ts +31 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/Motion.swift +171 -0
- package/ios/RevealOverlay.swift +115 -0
- package/ios/SVGPath.swift +373 -0
- package/ios/ThemeReveal.podspec +26 -0
- package/ios/ThemeRevealModule.swift +264 -0
- package/mocks/ThemeReveal.js +10 -0
- package/package.json +67 -4
- package/src/ThemeRevealModule.ts +24 -0
- package/src/createThemeReveal.ts +262 -0
- package/src/index.ts +14 -0
- package/src/originFromPress.ts +40 -0
- package/src/resolveTransition.ts +63 -0
- package/src/store.ts +30 -0
- package/src/types.ts +17 -0
|
@@ -0,0 +1,904 @@
|
|
|
1
|
+
package expo.modules.themereveal
|
|
2
|
+
|
|
3
|
+
import android.animation.Animator
|
|
4
|
+
import android.animation.AnimatorListenerAdapter
|
|
5
|
+
import android.animation.TimeInterpolator
|
|
6
|
+
import android.animation.ValueAnimator
|
|
7
|
+
import android.app.Activity
|
|
8
|
+
import android.content.ComponentCallbacks2
|
|
9
|
+
import android.content.Context
|
|
10
|
+
import android.content.res.Configuration
|
|
11
|
+
import android.graphics.PointF
|
|
12
|
+
import android.os.Build
|
|
13
|
+
import android.os.Handler
|
|
14
|
+
import android.os.Looper
|
|
15
|
+
import android.os.MessageQueue
|
|
16
|
+
import android.os.SystemClock
|
|
17
|
+
import android.util.Log
|
|
18
|
+
import android.view.Choreographer
|
|
19
|
+
import android.view.View
|
|
20
|
+
import android.view.ViewAnimationUtils
|
|
21
|
+
import android.view.ViewGroup
|
|
22
|
+
import androidx.lifecycle.Lifecycle
|
|
23
|
+
import androidx.lifecycle.LifecycleOwner
|
|
24
|
+
import expo.modules.kotlin.Promise
|
|
25
|
+
import expo.modules.kotlin.functions.Queues
|
|
26
|
+
import expo.modules.kotlin.modules.Module
|
|
27
|
+
import expo.modules.kotlin.modules.ModuleDefinition
|
|
28
|
+
import kotlin.math.roundToInt
|
|
29
|
+
|
|
30
|
+
// Overlay has to stay the size of cover. The React root the origin comes from sits at (rootX, rootY) on screen, the
|
|
31
|
+
// overlay at (screenX, screenY). host is the view whose ViewOverlay holds the overlay, the window root the snapshot
|
|
32
|
+
// shows, in activity's window.
|
|
33
|
+
private class OverlayGeometry(
|
|
34
|
+
val cover: View,
|
|
35
|
+
val rootX: Int,
|
|
36
|
+
val rootY: Int,
|
|
37
|
+
val screenX: Int,
|
|
38
|
+
val screenY: Int,
|
|
39
|
+
val host: ViewGroup,
|
|
40
|
+
val activity: Activity,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
// Frame-commit callback that release can empty: nothing unregisters one, and the window holds on to it until its
|
|
44
|
+
// next commit.
|
|
45
|
+
private class CommitRelay(var target: (() -> Unit)?) : Runnable {
|
|
46
|
+
override fun run() {
|
|
47
|
+
target?.invoke()
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Snapshot from capture(), kept out of the window until prepare() attaches it. root is the window root it shows,
|
|
52
|
+
// generation the prepare generation when the capture started.
|
|
53
|
+
private class HeldCapture(
|
|
54
|
+
val ticket: Int,
|
|
55
|
+
val generation: Int,
|
|
56
|
+
val activity: Activity,
|
|
57
|
+
val root: View?,
|
|
58
|
+
val snapshot: Snapshot,
|
|
59
|
+
val takenAtMs: Long,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
// capture() whose PixelCopy is still running. A prepare() for the same window sets waiter and gets the snapshot.
|
|
63
|
+
private class PendingCapture(val activity: Activity, val root: View?) {
|
|
64
|
+
var waiter: ((Snapshot?) -> Unit)? = null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
class ThemeRevealModule : Module() {
|
|
68
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
69
|
+
private var overlay: RevealOverlayView? = null
|
|
70
|
+
private var geometry: OverlayGeometry? = null
|
|
71
|
+
private var started: Promise? = null
|
|
72
|
+
private var generation = 0
|
|
73
|
+
private val failsafe = Runnable { removeOverlay() }
|
|
74
|
+
private var held: HeldCapture? = null
|
|
75
|
+
private var pendingCapture: PendingCapture? = null
|
|
76
|
+
private var captureTicket = 0
|
|
77
|
+
private val captureExpiry = Runnable { voidCapture("expired") }
|
|
78
|
+
|
|
79
|
+
// RenderThread reveal: the wait for its second snapshot, the view showing that snapshot, and the animator on the
|
|
80
|
+
// RenderThread (reveal or inverse reveal).
|
|
81
|
+
private var secondSnapshot: SecondSnapshot? = null
|
|
82
|
+
private var secondView: RevealOverlayView? = null
|
|
83
|
+
private var renderThreadAnimator: Animator? = null
|
|
84
|
+
|
|
85
|
+
// Set in OnDestroy, and again first thing in its main message. From then on this module prepares no kept target.
|
|
86
|
+
@Volatile
|
|
87
|
+
private var destroyed = false
|
|
88
|
+
|
|
89
|
+
// Prepare whose own capture is running (its generation), or -1. A cancel or a newer prepare makes it stale.
|
|
90
|
+
private var captureFor = -1
|
|
91
|
+
|
|
92
|
+
// The kept capture target's prepare: KEEP_TARGET_DELAY_MS after a switch or capture ends, then in main's next
|
|
93
|
+
// idle time, and only if CaptureRules.keepsTargetNow still holds by then.
|
|
94
|
+
private val keepTargetIdle = MessageQueue.IdleHandler {
|
|
95
|
+
// After OnDestroy, this module's state tells nothing about the new module's switch, and appContext can already
|
|
96
|
+
// be gone.
|
|
97
|
+
if (destroyed) return@IdleHandler false
|
|
98
|
+
val activity = appContext.currentActivity
|
|
99
|
+
val now = activity != null && CaptureRules.keepsTargetNow(
|
|
100
|
+
overlay != null,
|
|
101
|
+
pendingCapture != null,
|
|
102
|
+
held != null,
|
|
103
|
+
captureFor == generation,
|
|
104
|
+
isResumed(activity),
|
|
105
|
+
)
|
|
106
|
+
if (now) WindowSnapshot.keepTarget(activity, mainHandler)
|
|
107
|
+
false
|
|
108
|
+
}
|
|
109
|
+
private val keepTargetSoon = Runnable {
|
|
110
|
+
Looper.getMainLooper().queue.removeIdleHandler(keepTargetIdle)
|
|
111
|
+
Looper.getMainLooper().queue.addIdleHandler(keepTargetIdle)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Frees the kept bitmaps when the app is hidden or low on memory, or the screen changes size. Runs on main.
|
|
115
|
+
private val memoryCallbacks = object : ComponentCallbacks2 {
|
|
116
|
+
override fun onTrimMemory(level: Int) {
|
|
117
|
+
WindowSnapshot.kept.onTrimMemory(level)
|
|
118
|
+
WindowSnapshot.target.onTrimMemory(level)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
override fun onConfigurationChanged(newConfig: Configuration) {
|
|
122
|
+
WindowSnapshot.kept.onScreenSize(newConfig.screenWidthDp, newConfig.screenHeightDp)
|
|
123
|
+
WindowSnapshot.target.onScreenSize(newConfig.screenWidthDp, newConfig.screenHeightDp)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@Deprecated("Deprecated in Java")
|
|
127
|
+
override fun onLowMemory() {
|
|
128
|
+
WindowSnapshot.kept.clear()
|
|
129
|
+
WindowSnapshot.target.clear()
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Context memoryCallbacks are registered with.
|
|
134
|
+
private var callbacksContext: Context? = null
|
|
135
|
+
|
|
136
|
+
override fun definition() = ModuleDefinition {
|
|
137
|
+
Name("ThemeReveal")
|
|
138
|
+
|
|
139
|
+
AsyncFunction("prepare") { viewTag: Int?, promise: Promise ->
|
|
140
|
+
prepare(viewTag, promise)
|
|
141
|
+
}.runOnQueue(Queues.MAIN)
|
|
142
|
+
|
|
143
|
+
AsyncFunction("start") { transition: Map<String, Any?>, promise: Promise ->
|
|
144
|
+
start(TransitionArgs.from(transition), promise)
|
|
145
|
+
}.runOnQueue(Queues.MAIN)
|
|
146
|
+
|
|
147
|
+
AsyncFunction("capture") { viewTag: Int?, promise: Promise ->
|
|
148
|
+
capture(viewTag, promise)
|
|
149
|
+
}.runOnQueue(Queues.MAIN)
|
|
150
|
+
|
|
151
|
+
AsyncFunction("dropCapture") {
|
|
152
|
+
voidCapture("dropped")
|
|
153
|
+
}.runOnQueue(Queues.MAIN)
|
|
154
|
+
|
|
155
|
+
AsyncFunction("cancel") {
|
|
156
|
+
generation++
|
|
157
|
+
voidCapture("cancel")
|
|
158
|
+
removeOverlay()
|
|
159
|
+
}.runOnQueue(Queues.MAIN)
|
|
160
|
+
|
|
161
|
+
// Runs off main. JS only calls it when the warmUp option is on.
|
|
162
|
+
AsyncFunction("warmUp") { promise: Promise ->
|
|
163
|
+
warmUp(promise)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
OnCreate {
|
|
167
|
+
callbacksContext = appContext.reactContext?.applicationContext?.also {
|
|
168
|
+
it.registerComponentCallbacks(memoryCallbacks)
|
|
169
|
+
// Size to compare the first change against.
|
|
170
|
+
val config = it.resources.configuration
|
|
171
|
+
mainHandler.post {
|
|
172
|
+
WindowSnapshot.kept.onScreenSize(config.screenWidthDp, config.screenHeightDp)
|
|
173
|
+
WindowSnapshot.target.onScreenSize(config.screenWidthDp, config.screenHeightDp)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
OnActivityEntersBackground {
|
|
179
|
+
voidCapture("pause")
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// A prepare refused by a pause runs now. After a trim the target stays hidden until the next snapshot.
|
|
183
|
+
OnActivityEntersForeground {
|
|
184
|
+
scheduleKeepTarget()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
OnActivityDestroys {
|
|
188
|
+
generation++
|
|
189
|
+
voidCapture("destroy")
|
|
190
|
+
removeOverlay()
|
|
191
|
+
WindowSnapshot.kept.hide()
|
|
192
|
+
WindowSnapshot.target.hide()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
OnDestroy {
|
|
196
|
+
// Here too, off main, for an idle handler that runs before the main message below.
|
|
197
|
+
destroyed = true
|
|
198
|
+
callbacksContext?.unregisterComponentCallbacks(memoryCallbacks)
|
|
199
|
+
callbacksContext = null
|
|
200
|
+
mainHandler.post {
|
|
201
|
+
destroyed = true
|
|
202
|
+
generation++
|
|
203
|
+
voidCapture("destroy")
|
|
204
|
+
removeOverlay()
|
|
205
|
+
cancelKeepTarget()
|
|
206
|
+
WindowSnapshot.kept.hide()
|
|
207
|
+
WindowSnapshot.target.hide()
|
|
208
|
+
// Next module keeps a target only if its own warm-up runs.
|
|
209
|
+
WindowSnapshot.target.keep = false
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Off main: starts the copy thread. Then, on main, prepares the first switch's capture: GPU capture on API 29+,
|
|
215
|
+
// otherwise a kept bitmap of the window's size. Failures go to the log and the promise.
|
|
216
|
+
private fun warmUp(promise: Promise) {
|
|
217
|
+
loadClasses()
|
|
218
|
+
val error = try {
|
|
219
|
+
WindowSnapshot.warmUp()
|
|
220
|
+
null
|
|
221
|
+
} catch (thrown: Exception) {
|
|
222
|
+
Log.w(TAG, "Cannot start the copy thread", thrown)
|
|
223
|
+
thrown
|
|
224
|
+
}
|
|
225
|
+
mainHandler.post {
|
|
226
|
+
WindowSnapshot.kept.keep = true
|
|
227
|
+
WindowSnapshot.target.keep = true
|
|
228
|
+
val activity = appContext.currentActivity
|
|
229
|
+
// A JS reload hid the pool in the old module's OnDestroy. This warm-up comes from a mounted UI.
|
|
230
|
+
if (activity != null && isResumed(activity)) {
|
|
231
|
+
WindowSnapshot.kept.show()
|
|
232
|
+
WindowSnapshot.target.show()
|
|
233
|
+
}
|
|
234
|
+
// Copy thread didn't start, so the first switch's path can't be prepared either.
|
|
235
|
+
if (error == null) {
|
|
236
|
+
WindowSnapshot.prepareFirst(activity, mainHandler)
|
|
237
|
+
// Not right away: the app's first frames after the mount share the RenderThread with it.
|
|
238
|
+
scheduleKeepTarget()
|
|
239
|
+
}
|
|
240
|
+
if (error == null) promise.resolve() else promise.reject("ERR_THEME_REVEAL_WARM_UP", error.message, error)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Off main: loads most of the classes a switch uses, so the first switch doesn't load them on the main thread.
|
|
245
|
+
// Synthetic lambda classes still load on first use. A class in this list must not touch the main looper in its
|
|
246
|
+
// static initializer, and none can fail there: a class whose initializer threw can't be used later either.
|
|
247
|
+
private fun loadClasses() {
|
|
248
|
+
val classes = listOf(
|
|
249
|
+
HeldCapture::class.java,
|
|
250
|
+
PendingCapture::class.java,
|
|
251
|
+
OverlayGeometry::class.java,
|
|
252
|
+
CommitRelay::class.java,
|
|
253
|
+
SecondSnapshot::class.java,
|
|
254
|
+
Snapshot::class.java,
|
|
255
|
+
RevealOverlayView::class.java,
|
|
256
|
+
Once::class.java,
|
|
257
|
+
CaptureRules::class.java,
|
|
258
|
+
CaptureEnd::class.java,
|
|
259
|
+
Motion::class.java,
|
|
260
|
+
ShapePath::class.java,
|
|
261
|
+
RevealPlan::class.java,
|
|
262
|
+
RevealPath::class.java,
|
|
263
|
+
GrowthInterpolator::class.java,
|
|
264
|
+
ShrinkInterpolator::class.java,
|
|
265
|
+
FallbackCounter::class.java,
|
|
266
|
+
SecondSnapshotWait::class.java,
|
|
267
|
+
WaitState::class.java,
|
|
268
|
+
KeptTarget::class.java,
|
|
269
|
+
)
|
|
270
|
+
for (type in classes) {
|
|
271
|
+
try {
|
|
272
|
+
Class.forName(type.name, true, type.classLoader)
|
|
273
|
+
} catch (_: LinkageError) {
|
|
274
|
+
// Failed verification or link: the first use reports it.
|
|
275
|
+
} catch (_: ClassNotFoundException) {
|
|
276
|
+
// Not in this build.
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Takes the snapshot now and keeps it out of the window, so the screen stays live while the finger is down.
|
|
282
|
+
private fun capture(viewTag: Int?, promise: Promise) {
|
|
283
|
+
voidCapture("replaced")
|
|
284
|
+
// The prepare would hold up this capture's image on the copy thread.
|
|
285
|
+
cancelKeepTarget()
|
|
286
|
+
val ticket = captureTicket
|
|
287
|
+
val gen = generation
|
|
288
|
+
val activity = appContext.currentActivity
|
|
289
|
+
// An overlay on screen would end up in the snapshot, so the switch captures after it instead.
|
|
290
|
+
if (overlay != null || activity == null || activity.window?.decorView == null || activity.isFinishing ||
|
|
291
|
+
!Motion.canAnimate(Build.VERSION.SDK_INT, isResumed(activity))
|
|
292
|
+
) {
|
|
293
|
+
promise.resolve(false)
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
val root = rootOf(activity, viewTag)
|
|
297
|
+
val pending = PendingCapture(activity, root)
|
|
298
|
+
pendingCapture = pending
|
|
299
|
+
WindowSnapshot.capture(activity, root, mainHandler) { snapshot ->
|
|
300
|
+
val waiter = pending.waiter
|
|
301
|
+
if (waiter != null) {
|
|
302
|
+
promise.resolve(snapshot != null)
|
|
303
|
+
waiter(snapshot)
|
|
304
|
+
return@capture
|
|
305
|
+
}
|
|
306
|
+
if (pendingCapture === pending) pendingCapture = null
|
|
307
|
+
if (snapshot == null) {
|
|
308
|
+
scheduleKeepTarget()
|
|
309
|
+
promise.resolve(false)
|
|
310
|
+
return@capture
|
|
311
|
+
}
|
|
312
|
+
// A prepare that started during the copy (its PixelCopy can run before or after this one) makes this a frame
|
|
313
|
+
// from before that switch.
|
|
314
|
+
if (!CaptureRules.keepResult(ticket, captureTicket, gen, generation, overlay != null) || activity.isFinishing ||
|
|
315
|
+
activity.isDestroyed || !isResumed(activity) || !snapshot.host.isAttachedToWindow
|
|
316
|
+
) {
|
|
317
|
+
WindowSnapshot.kept.give(snapshot.bitmap)
|
|
318
|
+
debug("dropped a capture result: voided, another switch or no window")
|
|
319
|
+
// The capture could have used the kept target, and no overlay is coming.
|
|
320
|
+
scheduleKeepTarget()
|
|
321
|
+
promise.resolve(false)
|
|
322
|
+
return@capture
|
|
323
|
+
}
|
|
324
|
+
// Upload a PixelCopy bitmap during the press, so the overlay's first frame doesn't have to. A GPU snapshot is
|
|
325
|
+
// already on the GPU.
|
|
326
|
+
snapshot.bitmap.prepareToDraw()
|
|
327
|
+
held = HeldCapture(ticket, gen, activity, root, snapshot, SystemClock.uptimeMillis())
|
|
328
|
+
debug("held a capture: ${WindowSnapshot.pathOf(snapshot.bitmap).js}")
|
|
329
|
+
// A GPU snapshot's texture is imported on the RenderThread at its first draw, i.e. the overlay's first frame.
|
|
330
|
+
// Do it now, during the hold.
|
|
331
|
+
WindowSnapshot.preImport(snapshot.bitmap, mainHandler)
|
|
332
|
+
mainHandler.postDelayed(captureExpiry, CaptureRules.TIMEOUT_MS)
|
|
333
|
+
promise.resolve(true)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Frees the held capture and voids a running one no prepare took. A PixelCopy bitmap goes back to the kept
|
|
338
|
+
// bitmaps, a GPU snapshot gets freed.
|
|
339
|
+
private fun voidCapture(reason: String) {
|
|
340
|
+
captureTicket++
|
|
341
|
+
val running = pendingCapture != null
|
|
342
|
+
pendingCapture = null
|
|
343
|
+
mainHandler.removeCallbacks(captureExpiry)
|
|
344
|
+
// That capture could have used the kept target, and no overlay is coming for it.
|
|
345
|
+
if (running || held != null) scheduleKeepTarget()
|
|
346
|
+
val current = held ?: return
|
|
347
|
+
held = null
|
|
348
|
+
WindowSnapshot.kept.give(current.snapshot.bitmap)
|
|
349
|
+
debug("freed the held capture: $reason")
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private fun rootOf(activity: Activity, viewTag: Int?): View? {
|
|
353
|
+
val tapped = viewTag?.let { findView(it) }?.takeIf { it.isAttachedToWindow }
|
|
354
|
+
return tapped?.rootView ?: WindowSnapshot.focusedRoot(activity)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private fun prepare(viewTag: Int?, promise: Promise) {
|
|
358
|
+
val generationBefore = generation
|
|
359
|
+
val ticket = ++generation
|
|
360
|
+
removeOverlay()
|
|
361
|
+
// This switch's capture is using the copy thread now.
|
|
362
|
+
cancelKeepTarget()
|
|
363
|
+
val activity = appContext.currentActivity
|
|
364
|
+
val window = activity?.window
|
|
365
|
+
val decor = window?.decorView as? ViewGroup
|
|
366
|
+
if (activity == null || window == null || decor == null || activity.isFinishing ||
|
|
367
|
+
!Motion.canAnimate(Build.VERSION.SDK_INT, isResumed(activity))
|
|
368
|
+
) {
|
|
369
|
+
voidCapture("no window")
|
|
370
|
+
promise.resolve(null)
|
|
371
|
+
return
|
|
372
|
+
}
|
|
373
|
+
val root = rootOf(activity, viewTag)
|
|
374
|
+
val tapped = viewTag?.let { findView(it) }?.takeIf { it.isAttachedToWindow }
|
|
375
|
+
val reactRoot = tapped?.let { WindowSnapshot.reactRootOf(it) } ?: root ?: decor
|
|
376
|
+
// A switch that ends without an overlay still changes the theme (JS applies it), so a capture that started
|
|
377
|
+
// during its copy shows the screen from before. A newer prepare (ticket != generation) handles its own.
|
|
378
|
+
fun fail() {
|
|
379
|
+
if (captureFor == ticket) captureFor = -1
|
|
380
|
+
if (ticket == generation) voidCapture("switch failed")
|
|
381
|
+
// The capture could have used the kept target, and no overlay is coming.
|
|
382
|
+
scheduleKeepTarget()
|
|
383
|
+
promise.resolve(null)
|
|
384
|
+
}
|
|
385
|
+
fun attach(snapshot: Snapshot?) {
|
|
386
|
+
if (captureFor == ticket) captureFor = -1
|
|
387
|
+
if (snapshot == null) {
|
|
388
|
+
fail()
|
|
389
|
+
return
|
|
390
|
+
}
|
|
391
|
+
val bitmap = snapshot.bitmap
|
|
392
|
+
if (ticket != generation || activity.isFinishing || activity.isDestroyed || !isResumed(activity) ||
|
|
393
|
+
!snapshot.host.isAttachedToWindow
|
|
394
|
+
) {
|
|
395
|
+
WindowSnapshot.kept.give(bitmap)
|
|
396
|
+
fail()
|
|
397
|
+
return
|
|
398
|
+
}
|
|
399
|
+
// Kick off the snapshot's GPU upload now, on the RenderThread, so it doesn't land in the overlay's first
|
|
400
|
+
// frame. A no-op for a held capture, whose upload started at capture time, and for a GPU snapshot, which is
|
|
401
|
+
// already on the GPU.
|
|
402
|
+
bitmap.prepareToDraw()
|
|
403
|
+
val view = RevealOverlayView(activity, bitmap)
|
|
404
|
+
// In the host's overlay, not among its children: adding or removing a child lays out the whole window, and
|
|
405
|
+
// after that layout the traversal made two binder calls to the system (3-4 ms on a Mi 11 Lite) in the overlay's
|
|
406
|
+
// first frame and again in the frame that removes it. The overlay draws above every child and lays nothing out.
|
|
407
|
+
snapshot.host.overlay.add(view)
|
|
408
|
+
view.layout(snapshot.hostX, snapshot.hostY, snapshot.hostX + bitmap.width, snapshot.hostY + bitmap.height)
|
|
409
|
+
val rootAt = WindowSnapshot.locationOf(reactRoot)
|
|
410
|
+
overlay = view
|
|
411
|
+
geometry = OverlayGeometry(
|
|
412
|
+
snapshot.cover,
|
|
413
|
+
rootAt[0],
|
|
414
|
+
rootAt[1],
|
|
415
|
+
snapshot.screenX,
|
|
416
|
+
snapshot.screenY,
|
|
417
|
+
snapshot.host,
|
|
418
|
+
activity,
|
|
419
|
+
)
|
|
420
|
+
mainHandler.postDelayed(failsafe, FAILSAFE_MS)
|
|
421
|
+
// Any capture held or running right now shows the screen from before this switch.
|
|
422
|
+
voidCapture("switch attached")
|
|
423
|
+
// JS applies the theme when this resolves, and that render blocks the JS thread. Resolve in the next frame's
|
|
424
|
+
// callbacks, after React Native's timers for that frame, so the render starts after that frame's JS callbacks
|
|
425
|
+
// instead of holding them up. The overlay draws in that same frame. No frame comes while the screen is off,
|
|
426
|
+
// so a timeout also resolves, whichever fires first.
|
|
427
|
+
val path = WindowSnapshot.pathOf(bitmap).js
|
|
428
|
+
val resolve = Once { promise.resolve(path) }
|
|
429
|
+
lateinit var timeout: Runnable
|
|
430
|
+
val frame = Choreographer.FrameCallback {
|
|
431
|
+
mainHandler.removeCallbacks(timeout)
|
|
432
|
+
resolve.run()
|
|
433
|
+
}
|
|
434
|
+
timeout = Runnable {
|
|
435
|
+
Choreographer.getInstance().removeFrameCallback(frame)
|
|
436
|
+
resolve.run()
|
|
437
|
+
}
|
|
438
|
+
Choreographer.getInstance().postFrameCallback(frame)
|
|
439
|
+
mainHandler.postDelayed(timeout, RESOLVE_TIMEOUT_MS)
|
|
440
|
+
}
|
|
441
|
+
val kept = held
|
|
442
|
+
if (kept != null && CaptureRules.usable(
|
|
443
|
+
kept.ticket,
|
|
444
|
+
captureTicket,
|
|
445
|
+
kept.generation,
|
|
446
|
+
generationBefore,
|
|
447
|
+
kept.activity === activity && kept.root === root,
|
|
448
|
+
kept.snapshot.bitmap.width,
|
|
449
|
+
kept.snapshot.bitmap.height,
|
|
450
|
+
kept.snapshot.cover.width,
|
|
451
|
+
kept.snapshot.cover.height,
|
|
452
|
+
kept.takenAtMs,
|
|
453
|
+
SystemClock.uptimeMillis(),
|
|
454
|
+
)
|
|
455
|
+
) {
|
|
456
|
+
held = null
|
|
457
|
+
mainHandler.removeCallbacks(captureExpiry)
|
|
458
|
+
debug("prepare uses the held capture")
|
|
459
|
+
attach(kept.snapshot)
|
|
460
|
+
return
|
|
461
|
+
}
|
|
462
|
+
val pending = pendingCapture
|
|
463
|
+
if (pending != null && pending.activity === activity && pending.root === root) {
|
|
464
|
+
pendingCapture = null
|
|
465
|
+
captureFor = ticket
|
|
466
|
+
pending.waiter = ::attach
|
|
467
|
+
debug("prepare waits for the running capture")
|
|
468
|
+
return
|
|
469
|
+
}
|
|
470
|
+
voidCapture("not usable")
|
|
471
|
+
captureFor = ticket
|
|
472
|
+
WindowSnapshot.capture(activity, root, mainHandler, ::attach)
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
private fun start(transition: TransitionArgs, promise: Promise) {
|
|
476
|
+
val view = overlay
|
|
477
|
+
if (view == null) {
|
|
478
|
+
promise.resolve()
|
|
479
|
+
return
|
|
480
|
+
}
|
|
481
|
+
mainHandler.removeCallbacks(failsafe)
|
|
482
|
+
started?.resolve()
|
|
483
|
+
started = promise
|
|
484
|
+
// A start while an earlier start's RenderThread animator is still running on this overlay ends the switch right
|
|
485
|
+
// away. A cancelled RevealAnimator clears its view's reveal clip later, which would break the next animation
|
|
486
|
+
// for a frame.
|
|
487
|
+
if (renderThreadAnimator != null) {
|
|
488
|
+
removeOverlay()
|
|
489
|
+
return
|
|
490
|
+
}
|
|
491
|
+
clearRenderThreadReveal()
|
|
492
|
+
val cover = geometry?.cover
|
|
493
|
+
val durationMs = Motion.durationMs(transition.durationMs)
|
|
494
|
+
// A modal closing between prepare and start detaches the overlay, so resolve right away.
|
|
495
|
+
if (view.parent == null || !view.isAttachedToWindow || cover == null || durationMs == null ||
|
|
496
|
+
!animatorsEnabled() || !Motion.sameSize(view.snapshotWidth, view.snapshotHeight, cover.width, cover.height)
|
|
497
|
+
) {
|
|
498
|
+
removeOverlay()
|
|
499
|
+
return
|
|
500
|
+
}
|
|
501
|
+
val onEnd = { if (overlay === view) removeOverlay() }
|
|
502
|
+
// Second start on the same overlay: the first one's animation stops here.
|
|
503
|
+
view.still()
|
|
504
|
+
val playing = try {
|
|
505
|
+
play(view, transition, durationMs, onEnd)
|
|
506
|
+
} catch (_: RuntimeException) {
|
|
507
|
+
false
|
|
508
|
+
}
|
|
509
|
+
if (!playing) removeOverlay()
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
private fun play(view: RevealOverlayView, transition: TransitionArgs, durationMs: Long, onEnd: () -> Unit): Boolean {
|
|
513
|
+
when (transition.type) {
|
|
514
|
+
"reveal", "inverseReveal" -> {
|
|
515
|
+
val origin = transition.origin ?: return false
|
|
516
|
+
val space = geometry ?: return false
|
|
517
|
+
val density = view.resources.displayMetrics.density
|
|
518
|
+
val point = Motion.toOverlay(
|
|
519
|
+
(origin.x * density).toFloat(),
|
|
520
|
+
(origin.y * density).toFloat(),
|
|
521
|
+
space.rootX,
|
|
522
|
+
space.rootY,
|
|
523
|
+
space.screenX,
|
|
524
|
+
space.screenY,
|
|
525
|
+
)
|
|
526
|
+
val x = point[0]
|
|
527
|
+
val y = point[1]
|
|
528
|
+
val path = RevealPlan.path(
|
|
529
|
+
transition.type,
|
|
530
|
+
transition.shape != null,
|
|
531
|
+
Build.VERSION.SDK_INT,
|
|
532
|
+
view.snapshotPath,
|
|
533
|
+
animatorsEnabled(),
|
|
534
|
+
fallbacks.off,
|
|
535
|
+
)
|
|
536
|
+
when (path) {
|
|
537
|
+
RevealPath.RENDER_THREAD_REVEAL -> {
|
|
538
|
+
revealOnRenderThread(view, space, x, y, durationMs, onEnd)
|
|
539
|
+
return true
|
|
540
|
+
}
|
|
541
|
+
RevealPath.RENDER_THREAD_INVERSE ->
|
|
542
|
+
if (inverseRevealOnRenderThread(view, x, y, durationMs, onEnd)) return true
|
|
543
|
+
RevealPath.UI_THREAD -> Unit
|
|
544
|
+
}
|
|
545
|
+
view.reveal(shapeOf(transition.shape), x, y, transition.type == "inverseReveal", durationMs, onEnd)
|
|
546
|
+
}
|
|
547
|
+
"wipe" -> view.wipe(wipeDirectionOf(transition.direction) ?: return false, durationMs, onEnd)
|
|
548
|
+
"crossfade" -> view.crossfade(durationMs, onEnd)
|
|
549
|
+
else -> return false
|
|
550
|
+
}
|
|
551
|
+
return true
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Circle reveal on the RenderThread. The overlay stays still while the next frame draws React Native's mount of the
|
|
555
|
+
// new theme. In the frame after that frame's commit, a second GPU snapshot of the same window(s), minus the overlay,
|
|
556
|
+
// goes into a second overlay view above the first, and a circular reveal clip grows on it. If that fails, the
|
|
557
|
+
// UI-thread reveal runs instead (SecondSnapshotWait).
|
|
558
|
+
private fun revealOnRenderThread(
|
|
559
|
+
view: RevealOverlayView,
|
|
560
|
+
space: OverlayGeometry,
|
|
561
|
+
x: Float,
|
|
562
|
+
y: Float,
|
|
563
|
+
durationMs: Long,
|
|
564
|
+
onEnd: () -> Unit,
|
|
565
|
+
) {
|
|
566
|
+
val run = SecondSnapshot(view, space, x, y, durationMs, onEnd)
|
|
567
|
+
secondSnapshot = run
|
|
568
|
+
run.begin()
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Inverse circle reveal on the RenderThread: a circular reveal clip on the overlay itself, from the cover radius
|
|
572
|
+
// down to 0. No second snapshot. False if it can't start, and then the caller runs the UI-thread one.
|
|
573
|
+
private fun inverseRevealOnRenderThread(
|
|
574
|
+
view: RevealOverlayView,
|
|
575
|
+
x: Float,
|
|
576
|
+
y: Float,
|
|
577
|
+
durationMs: Long,
|
|
578
|
+
onEnd: () -> Unit,
|
|
579
|
+
): Boolean {
|
|
580
|
+
val radii = RevealPlan.circleRadii(x, y, view.snapshotWidth, view.snapshotHeight, view.startHeightPx)
|
|
581
|
+
val r0 = radii[0]
|
|
582
|
+
val r1 = radii[1]
|
|
583
|
+
if (!RevealPlan.radiiUsable(r0, r1)) return false
|
|
584
|
+
try {
|
|
585
|
+
startCircularReveal(view, x, y, r1, 0f, ShrinkInterpolator(r0, r1), durationMs, onEnd)
|
|
586
|
+
} catch (_: RuntimeException) {
|
|
587
|
+
clearRenderThreadReveal()
|
|
588
|
+
return false
|
|
589
|
+
}
|
|
590
|
+
debug("inverse reveal runs on the RenderThread")
|
|
591
|
+
return true
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
private fun startCircularReveal(
|
|
595
|
+
view: View,
|
|
596
|
+
x: Float,
|
|
597
|
+
y: Float,
|
|
598
|
+
from: Float,
|
|
599
|
+
to: Float,
|
|
600
|
+
curve: TimeInterpolator,
|
|
601
|
+
durationMs: Long,
|
|
602
|
+
onEnd: () -> Unit,
|
|
603
|
+
) {
|
|
604
|
+
val animator = ViewAnimationUtils.createCircularReveal(view, x.roundToInt(), y.roundToInt(), from, to)
|
|
605
|
+
animator.duration = durationMs
|
|
606
|
+
animator.interpolator = curve
|
|
607
|
+
animator.addListener(object : AnimatorListenerAdapter() {
|
|
608
|
+
override fun onAnimationEnd(animation: Animator) = onEnd()
|
|
609
|
+
})
|
|
610
|
+
renderThreadAnimator = animator
|
|
611
|
+
animator.start()
|
|
612
|
+
// A RenderThread animator doesn't advance while the window isn't drawing, so the overlay is removed then at the
|
|
613
|
+
// latest.
|
|
614
|
+
mainHandler.removeCallbacks(failsafe)
|
|
615
|
+
mainHandler.postDelayed(failsafe, durationMs + FAILSAFE_MS)
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Shows the second snapshot above the overlay and grows a circular reveal clip on it, on the RenderThread. Null if it
|
|
619
|
+
// runs, otherwise the reason it doesn't. Frees the snapshot if it doesn't use it.
|
|
620
|
+
private fun revealSecond(
|
|
621
|
+
view: RevealOverlayView,
|
|
622
|
+
space: OverlayGeometry,
|
|
623
|
+
x: Float,
|
|
624
|
+
y: Float,
|
|
625
|
+
durationMs: Long,
|
|
626
|
+
onEnd: () -> Unit,
|
|
627
|
+
snapshot: Snapshot,
|
|
628
|
+
): String? {
|
|
629
|
+
val bitmap = snapshot.bitmap
|
|
630
|
+
val matches = RevealPlan.secondMatches(
|
|
631
|
+
overlay === view,
|
|
632
|
+
view.isAttachedToWindow,
|
|
633
|
+
snapshot.host === space.host,
|
|
634
|
+
WindowSnapshot.pathOf(bitmap) == CapturePath.GPU,
|
|
635
|
+
bitmap.width,
|
|
636
|
+
bitmap.height,
|
|
637
|
+
view.snapshotWidth,
|
|
638
|
+
view.snapshotHeight,
|
|
639
|
+
snapshot.hostX == view.left && snapshot.hostY == view.top,
|
|
640
|
+
)
|
|
641
|
+
if (!matches) {
|
|
642
|
+
WindowSnapshot.kept.give(bitmap)
|
|
643
|
+
return "the second snapshot doesn't match the first"
|
|
644
|
+
}
|
|
645
|
+
val radii = RevealPlan.circleRadii(x, y, view.snapshotWidth, view.snapshotHeight, view.startHeightPx)
|
|
646
|
+
val r0 = radii[0]
|
|
647
|
+
val r1 = radii[1]
|
|
648
|
+
if (!RevealPlan.radiiUsable(r0, r1)) {
|
|
649
|
+
WindowSnapshot.kept.give(bitmap)
|
|
650
|
+
return "radii $r0 to $r1"
|
|
651
|
+
}
|
|
652
|
+
val next = RevealOverlayView(view.context, bitmap)
|
|
653
|
+
// Second view owns the bitmap from here on: clearRenderThreadReveal frees it.
|
|
654
|
+
secondView = next
|
|
655
|
+
try {
|
|
656
|
+
space.host.overlay.add(next)
|
|
657
|
+
next.layout(view.left, view.top, view.right, view.bottom)
|
|
658
|
+
startCircularReveal(next, x, y, r0, r1, GrowthInterpolator(r0, r1), durationMs, onEnd)
|
|
659
|
+
} catch (_: RuntimeException) {
|
|
660
|
+
clearRenderThreadReveal()
|
|
661
|
+
return "cannot start the circular reveal"
|
|
662
|
+
}
|
|
663
|
+
if (Log.isLoggable(TAG, Log.DEBUG)) probe(view, next, x, y)
|
|
664
|
+
return null
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Debug only, slow: reads one pixel of both snapshots away from the press, to show the second one has the new theme.
|
|
668
|
+
private fun probe(old: RevealOverlayView, next: RevealOverlayView, x: Float, y: Float) {
|
|
669
|
+
val w = old.snapshotWidth
|
|
670
|
+
val h = old.snapshotHeight
|
|
671
|
+
val px = (if (x < w / 2f) w * 0.9f else w * 0.1f).toInt()
|
|
672
|
+
val py = (if (y < h / 2f) h * 0.9f else h * 0.1f).toInt()
|
|
673
|
+
val before = old.pixelAt(px, py)
|
|
674
|
+
val after = next.pixelAt(px, py)
|
|
675
|
+
val hex = { color: Int? -> color?.let { "#%08x".format(it) } ?: "none" }
|
|
676
|
+
Log.d(TAG, "second snapshot probe at ($px, $py): old ${hex(before)}, new ${hex(after)}, differs ${before != after}")
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Stops the RenderThread reveal: the wait for the second snapshot (its callbacks and prepared target), the animator,
|
|
680
|
+
// and the second view with its bitmap.
|
|
681
|
+
private fun clearRenderThreadReveal() {
|
|
682
|
+
secondSnapshot?.wait?.cancel()
|
|
683
|
+
secondSnapshot = null
|
|
684
|
+
renderThreadAnimator?.let {
|
|
685
|
+
it.removeAllListeners()
|
|
686
|
+
it.cancel()
|
|
687
|
+
}
|
|
688
|
+
renderThreadAnimator = null
|
|
689
|
+
val next = secondView ?: return
|
|
690
|
+
secondView = null
|
|
691
|
+
(next.parent as? ViewGroup)?.removeView(next)
|
|
692
|
+
next.release(WindowSnapshot.kept::give)
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// One RenderThread reveal's wait for its second snapshot (SecondSnapshotWait): the frame callbacks, the commit
|
|
696
|
+
// callback, the timeout and the target prepared for the capture. Main thread only.
|
|
697
|
+
private inner class SecondSnapshot(
|
|
698
|
+
private val view: RevealOverlayView,
|
|
699
|
+
private val space: OverlayGeometry,
|
|
700
|
+
private val x: Float,
|
|
701
|
+
private val y: Float,
|
|
702
|
+
private val durationMs: Long,
|
|
703
|
+
private val onEnd: () -> Unit,
|
|
704
|
+
) : SecondSnapshotSteps<Snapshot> {
|
|
705
|
+
val wait = SecondSnapshotWait(this)
|
|
706
|
+
private val startedAt = SystemClock.uptimeMillis()
|
|
707
|
+
|
|
708
|
+
// Capture's target, from when it's ready until the capture takes it.
|
|
709
|
+
private var output: GpuOutput? = null
|
|
710
|
+
private val frame = Choreographer.FrameCallback {
|
|
711
|
+
val ok = windowOk()
|
|
712
|
+
// That way this frame's traversal always draws and its commit arrives. Without it, a theme mount that a frame
|
|
713
|
+
// drew before start() leaves this frame nothing to draw, and the wait runs into its timeout.
|
|
714
|
+
if (ok) view.invalidate()
|
|
715
|
+
wait.frame(ok)
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Nothing unregisters a frame-commit callback. release() empties the relay so it doesn't keep this wait alive.
|
|
719
|
+
private val commit = CommitRelay {
|
|
720
|
+
if (wait.state == WaitState.WAITING_FOR_COMMIT) debug("frame commit after ${sinceStart()} ms")
|
|
721
|
+
wait.commit(windowOk())
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// React Native's frame callbacks for this frame (mount dispatch, timers, the JS rAF tick) were posted before this
|
|
725
|
+
// one, so they run first.
|
|
726
|
+
private val captureFrame = Choreographer.FrameCallback { wait.captureFrame(windowOk()) }
|
|
727
|
+
private val timeout = Runnable { wait.timeout() }
|
|
728
|
+
|
|
729
|
+
// Starts the wait. The capture's target is made ready now, while JS renders the theme.
|
|
730
|
+
fun begin() {
|
|
731
|
+
WindowSnapshot.prepareOutput(view.snapshotWidth, view.snapshotHeight, mainHandler) { prepared, ms ->
|
|
732
|
+
val used = wait.beforeCapture
|
|
733
|
+
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
|
734
|
+
Log.d(
|
|
735
|
+
TAG,
|
|
736
|
+
"prepared the second snapshot's target after %d ms, %.1f ms on the copy thread: %b, used %b".format(
|
|
737
|
+
sinceStart(),
|
|
738
|
+
ms,
|
|
739
|
+
prepared != null,
|
|
740
|
+
used && prepared != null,
|
|
741
|
+
),
|
|
742
|
+
)
|
|
743
|
+
}
|
|
744
|
+
if (used) output = prepared else WindowSnapshot.releaseOutput(prepared)
|
|
745
|
+
}
|
|
746
|
+
Choreographer.getInstance().postFrameCallback(frame)
|
|
747
|
+
mainHandler.postDelayed(timeout, SecondSnapshotWait.TIMEOUT_MS)
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
private fun sinceStart() = SystemClock.uptimeMillis() - startedAt
|
|
751
|
+
|
|
752
|
+
private fun windowOk(): Boolean {
|
|
753
|
+
val activity = space.activity
|
|
754
|
+
return overlay === view && !activity.isFinishing && !activity.isDestroyed && space.host.isAttachedToWindow &&
|
|
755
|
+
view.isAttachedToWindow
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
override fun registerCommit(): Boolean {
|
|
759
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return false
|
|
760
|
+
return try {
|
|
761
|
+
// Frame callback invalidated the overlay, so this frame's traversal draws and its commit arrives.
|
|
762
|
+
space.host.viewTreeObserver.registerFrameCommitCallback(commit)
|
|
763
|
+
true
|
|
764
|
+
} catch (_: IllegalStateException) {
|
|
765
|
+
// A dead ViewTreeObserver.
|
|
766
|
+
false
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
override fun postCaptureFrame() = Choreographer.getInstance().postFrameCallback(captureFrame)
|
|
771
|
+
|
|
772
|
+
override fun capture() {
|
|
773
|
+
val prepared = output
|
|
774
|
+
output = null
|
|
775
|
+
val start = System.nanoTime()
|
|
776
|
+
// Window without the overlay: the capture hides it only while recording the window, on this thread.
|
|
777
|
+
WindowSnapshot.captureSecond(space.activity, space.host, mainHandler, view, prepared) { snapshot ->
|
|
778
|
+
debug("second snapshot result after ${sinceStart()} ms: ${snapshot != null}")
|
|
779
|
+
wait.result(snapshot)
|
|
780
|
+
}
|
|
781
|
+
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
|
782
|
+
Log.d(
|
|
783
|
+
TAG,
|
|
784
|
+
"second snapshot frame after %d ms, capture %.1f ms on main, prepared target %b".format(
|
|
785
|
+
sinceStart(),
|
|
786
|
+
(System.nanoTime() - start) / 1e6,
|
|
787
|
+
prepared != null,
|
|
788
|
+
),
|
|
789
|
+
)
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
override fun reveal(bitmap: Snapshot): String? {
|
|
794
|
+
val failure = revealSecond(view, space, x, y, durationMs, onEnd, bitmap)
|
|
795
|
+
if (failure == null) fallbacks.revealed()
|
|
796
|
+
return failure
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
override fun fallBack(reason: String) {
|
|
800
|
+
debug("RenderThread reveal falls back to the UI-thread reveal: $reason")
|
|
801
|
+
if (overlay !== view) return
|
|
802
|
+
// A modal that closed during the wait took the overlay out of the window, so it goes now, same as in start().
|
|
803
|
+
if (!view.isAttachedToWindow || !space.host.isAttachedToWindow) {
|
|
804
|
+
removeOverlay()
|
|
805
|
+
return
|
|
806
|
+
}
|
|
807
|
+
if (RevealPlan.countsFallBack(windowOk(), isResumed(space.activity))) {
|
|
808
|
+
fallbacks.fellBack()
|
|
809
|
+
if (fallbacks.off) debug("the RenderThread reveal is off for this process: too many fall backs in a row")
|
|
810
|
+
}
|
|
811
|
+
view.reveal(ShapePath.circle(), x, y, false, durationMs, onEnd)
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
override fun release() {
|
|
815
|
+
if (secondSnapshot === this) secondSnapshot = null
|
|
816
|
+
commit.target = null
|
|
817
|
+
Choreographer.getInstance().removeFrameCallback(frame)
|
|
818
|
+
Choreographer.getInstance().removeFrameCallback(captureFrame)
|
|
819
|
+
mainHandler.removeCallbacks(timeout)
|
|
820
|
+
WindowSnapshot.releaseOutput(output)
|
|
821
|
+
output = null
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
override fun free(bitmap: Snapshot) = WindowSnapshot.kept.give(bitmap.bitmap)
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
private fun removeOverlay() {
|
|
828
|
+
mainHandler.removeCallbacks(failsafe)
|
|
829
|
+
clearRenderThreadReveal()
|
|
830
|
+
started?.resolve()
|
|
831
|
+
started = null
|
|
832
|
+
val view = overlay ?: return
|
|
833
|
+
overlay = null
|
|
834
|
+
geometry = null
|
|
835
|
+
(view.parent as? ViewGroup)?.removeView(view)
|
|
836
|
+
view.release(WindowSnapshot.kept::give)
|
|
837
|
+
scheduleKeepTarget()
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Prepares the capture target for the next switch's first snapshot (keepTargetIdle), KEEP_TARGET_DELAY_MS from now,
|
|
841
|
+
// so the frames right after a switch or mount don't share the RenderThread with it. A later call pushes it back.
|
|
842
|
+
private fun scheduleKeepTarget() {
|
|
843
|
+
if (destroyed) return
|
|
844
|
+
mainHandler.removeCallbacks(keepTargetSoon)
|
|
845
|
+
mainHandler.postDelayed(keepTargetSoon, KEEP_TARGET_DELAY_MS)
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
private fun cancelKeepTarget() {
|
|
849
|
+
mainHandler.removeCallbacks(keepTargetSoon)
|
|
850
|
+
Looper.getMainLooper().queue.removeIdleHandler(keepTargetIdle)
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
private fun debug(message: String) {
|
|
854
|
+
if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, message)
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
private fun findView(viewTag: Int): View? = try {
|
|
858
|
+
appContext.findView<View>(viewTag)
|
|
859
|
+
} catch (_: RuntimeException) {
|
|
860
|
+
null
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
private fun isResumed(activity: Activity): Boolean =
|
|
864
|
+
(activity as? LifecycleOwner)?.lifecycle?.currentState?.isAtLeast(Lifecycle.State.RESUMED) == true
|
|
865
|
+
|
|
866
|
+
private fun animatorsEnabled(): Boolean =
|
|
867
|
+
Build.VERSION.SDK_INT < Build.VERSION_CODES.O || ValueAnimator.areAnimatorsEnabled()
|
|
868
|
+
|
|
869
|
+
private fun shapeOf(record: ShapeArgs?): ShapePath {
|
|
870
|
+
if (record == null) return ShapePath.circle()
|
|
871
|
+
return try {
|
|
872
|
+
ShapePath.parse(
|
|
873
|
+
record.path,
|
|
874
|
+
PointF(record.anchor.x.toFloat(), record.anchor.y.toFloat()),
|
|
875
|
+
record.inscribedRadius.toFloat(),
|
|
876
|
+
)
|
|
877
|
+
} catch (error: RuntimeException) {
|
|
878
|
+
Log.w(TAG, "Cannot parse the shape path \"${record.path}\", using the circle", error)
|
|
879
|
+
ShapePath.circle()
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
private fun wipeDirectionOf(direction: String?): WipeDirection? = when (direction) {
|
|
884
|
+
"up" -> WipeDirection.UP
|
|
885
|
+
"down" -> WipeDirection.DOWN
|
|
886
|
+
"left" -> WipeDirection.LEFT
|
|
887
|
+
"right" -> WipeDirection.RIGHT
|
|
888
|
+
else -> null
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
private companion object {
|
|
892
|
+
const val FAILSAFE_MS = 1000L
|
|
893
|
+
|
|
894
|
+
// Longest prepare waits for the frame callback before resolving.
|
|
895
|
+
const val RESOLVE_TIMEOUT_MS = 100L
|
|
896
|
+
|
|
897
|
+
// How long the kept capture target waits after a switch, capture or mount before its prepare.
|
|
898
|
+
const val KEEP_TARGET_DELAY_MS = 500L
|
|
899
|
+
|
|
900
|
+
// The RenderThread reveal's fallbacks in a row, process-wide (RevealPlan.path's secondSnapshotOff).
|
|
901
|
+
val fallbacks = FallbackCounter(RevealPlan.FALLBACKS_TO_OFF)
|
|
902
|
+
const val TAG = "ThemeReveal"
|
|
903
|
+
}
|
|
904
|
+
}
|