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,32 @@
|
|
|
1
|
+
apply plugin: 'com.android.library'
|
|
2
|
+
|
|
3
|
+
group = 'expo.modules.themereveal'
|
|
4
|
+
version = '0.1.0'
|
|
5
|
+
|
|
6
|
+
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
|
7
|
+
apply from: expoModulesCorePlugin
|
|
8
|
+
applyKotlinExpoModulesCorePlugin()
|
|
9
|
+
useCoreDependencies()
|
|
10
|
+
useExpoPublishing()
|
|
11
|
+
|
|
12
|
+
buildscript {
|
|
13
|
+
ext.safeExtGet = { prop, fallback ->
|
|
14
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
android {
|
|
19
|
+
namespace "expo.modules.themereveal"
|
|
20
|
+
compileSdkVersion safeExtGet("compileSdkVersion", 36)
|
|
21
|
+
defaultConfig {
|
|
22
|
+
minSdkVersion safeExtGet("minSdkVersion", 24)
|
|
23
|
+
targetSdkVersion safeExtGet("targetSdkVersion", 36)
|
|
24
|
+
versionCode 1
|
|
25
|
+
versionName "0.1.0"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
dependencies {
|
|
30
|
+
implementation 'com.facebook.react:react-android'
|
|
31
|
+
testImplementation 'junit:junit:4.13.2'
|
|
32
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
package expo.modules.themereveal
|
|
2
|
+
|
|
3
|
+
import android.graphics.HardwareRenderer
|
|
4
|
+
import android.os.Build
|
|
5
|
+
import kotlin.math.min
|
|
6
|
+
|
|
7
|
+
// js: name JS sees for it (src/types.ts CapturePath).
|
|
8
|
+
internal enum class CapturePath(val js: String) {
|
|
9
|
+
GPU("gpu"),
|
|
10
|
+
PIXEL_COPY("pixelcopy"),
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Why a GPU capture gave no bitmap.
|
|
14
|
+
internal enum class GpuFailure {
|
|
15
|
+
// Renderer lost its surface, was stopped or dropped the frame. Next switch tries the GPU again.
|
|
16
|
+
SYNC,
|
|
17
|
+
|
|
18
|
+
// No image arrived within GPU_TIMEOUT_MS.
|
|
19
|
+
TIMEOUT,
|
|
20
|
+
|
|
21
|
+
// Capture threw.
|
|
22
|
+
ERROR,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// What a warm-up prepares for the first switch.
|
|
26
|
+
internal enum class WarmUp {
|
|
27
|
+
// GPU capture's classes, reader and renderer. No bitmap kept, the GPU capture makes its own.
|
|
28
|
+
GPU,
|
|
29
|
+
|
|
30
|
+
// One window-size bitmap for PixelCopy, kept with one copy into it.
|
|
31
|
+
KEPT_BITMAP,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
internal class LightSource(val x: Float, val y: Float, val z: Float, val radius: Float)
|
|
35
|
+
|
|
36
|
+
internal object CapturePlan {
|
|
37
|
+
// First capture in a fresh process took 50 ms on a Pixel 8 Pro, a warm one 12-32 ms on both lab phones.
|
|
38
|
+
const val GPU_TIMEOUT_MS = 250L
|
|
39
|
+
|
|
40
|
+
private const val SYNC_FAILED = HardwareRenderer.SYNC_LOST_SURFACE_REWARD_IF_FOUND or
|
|
41
|
+
HardwareRenderer.SYNC_CONTEXT_IS_STOPPED or HardwareRenderer.SYNC_FRAME_DROPPED
|
|
42
|
+
|
|
43
|
+
// GPU capture renders in sRGB, so a wide color window stays on PixelCopy. So does a window without hardware
|
|
44
|
+
// acceleration, since its software canvas can't draw the hardware bitmap. Same for a view change waiting for its
|
|
45
|
+
// frame while force dark can apply: the capture would sync it first, without the force dark transform.
|
|
46
|
+
fun path(
|
|
47
|
+
apiLevel: Int,
|
|
48
|
+
defaultColorMode: Boolean,
|
|
49
|
+
hardwareAccelerated: Boolean,
|
|
50
|
+
pendingUnderForceDark: Boolean,
|
|
51
|
+
gpuOff: Boolean,
|
|
52
|
+
): CapturePath =
|
|
53
|
+
if (apiLevel >= Build.VERSION_CODES.Q && defaultColorMode && hardwareAccelerated && !pendingUnderForceDark &&
|
|
54
|
+
!gpuOff
|
|
55
|
+
) {
|
|
56
|
+
CapturePath.GPU
|
|
57
|
+
} else {
|
|
58
|
+
CapturePath.PIXEL_COPY
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ViewRootImpl turns on force dark for a light theme that allows it, in night mode. From API 36 the color
|
|
62
|
+
// inversion setting can darken a light theme too, and apps can't read that setting.
|
|
63
|
+
fun forceDarkCanApply(apiLevel: Int, nightMode: Boolean, lightTheme: Boolean, forceDarkAllowed: Boolean): Boolean =
|
|
64
|
+
apiLevel >= Build.VERSION_CODES.Q && nightMode && lightTheme &&
|
|
65
|
+
(forceDarkAllowed || apiLevel >= Build.VERSION_CODES.BAKLAVA)
|
|
66
|
+
|
|
67
|
+
// Warm-up prepares the path the activity window's first switch takes: GPU capture on API 29+, otherwise
|
|
68
|
+
// PixelCopy. hasLight: the theme has the light values for shadows (GpuSnapshot.lightOf).
|
|
69
|
+
fun warmUp(
|
|
70
|
+
apiLevel: Int,
|
|
71
|
+
defaultColorMode: Boolean,
|
|
72
|
+
hardwareAccelerated: Boolean,
|
|
73
|
+
hasLight: Boolean,
|
|
74
|
+
gpuOff: Boolean,
|
|
75
|
+
): WarmUp =
|
|
76
|
+
if (hasLight && path(apiLevel, defaultColorMode, hardwareAccelerated, false, gpuOff) == CapturePath.GPU) {
|
|
77
|
+
WarmUp.GPU
|
|
78
|
+
} else {
|
|
79
|
+
WarmUp.KEPT_BITMAP
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
fun syncFailed(result: Int): Boolean = result and SYNC_FAILED != 0
|
|
83
|
+
|
|
84
|
+
// A timeout or error can repeat on every switch, so the process stops trying the GPU. A timeout while the warm-up's
|
|
85
|
+
// pre-create holds the copy thread (copyThreadBusy) says nothing about the GPU, so the next switch tries it.
|
|
86
|
+
fun turnsGpuOff(failure: GpuFailure, copyThreadBusy: Boolean): Boolean = when (failure) {
|
|
87
|
+
GpuFailure.SYNC -> false
|
|
88
|
+
GpuFailure.TIMEOUT -> !copyThreadBusy
|
|
89
|
+
GpuFailure.ERROR -> true
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Where ThreadedRenderer.setLightCenter puts a window's light: above the display's horizontal center, lightY from
|
|
93
|
+
// its top, shifted into window space. From API 34 the height scales with the display's smallest side.
|
|
94
|
+
fun light(
|
|
95
|
+
apiLevel: Int,
|
|
96
|
+
displayWidth: Int,
|
|
97
|
+
displayHeight: Int,
|
|
98
|
+
density: Float,
|
|
99
|
+
windowLeft: Int,
|
|
100
|
+
windowTop: Int,
|
|
101
|
+
lightY: Float,
|
|
102
|
+
lightZ: Float,
|
|
103
|
+
lightRadius: Float,
|
|
104
|
+
): LightSource {
|
|
105
|
+
val z = if (apiLevel >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
106
|
+
val ratio = min(displayWidth, displayHeight) / (450f * density)
|
|
107
|
+
lightZ * (ratio + 2) / 3f
|
|
108
|
+
} else {
|
|
109
|
+
lightZ
|
|
110
|
+
}
|
|
111
|
+
return LightSource(displayWidth / 2f - windowLeft, lightY - windowTop, z, lightRadius)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// GPU jobs on the copy thread: the warm-up's pre-creates, plus the RenderThread reveal's prepared targets and
|
|
116
|
+
// pre-imports. All of them count as pre-creates here. Releases of captures and targets (GpuSnapshot.free) run on that
|
|
117
|
+
// thread too and aren't counted. A GPU capture's image arrives on that thread, so it can wait behind a pre-create
|
|
118
|
+
// past the timeout. The first such timeout of each pre-create is forgiven and the GPU path stays on. If that
|
|
119
|
+
// pre-create still holds the copy thread at a later switch, that switch skips the GPU capture. So a pre-create that
|
|
120
|
+
// never returns costs only one switch the timeout. Main thread only.
|
|
121
|
+
internal class PreCreates {
|
|
122
|
+
// How many have started.
|
|
123
|
+
var count = 0
|
|
124
|
+
private set
|
|
125
|
+
private var running = 0
|
|
126
|
+
private var forgivenAt = -1
|
|
127
|
+
|
|
128
|
+
// One is queued or running.
|
|
129
|
+
val busy: Boolean
|
|
130
|
+
get() = running > 0
|
|
131
|
+
|
|
132
|
+
fun started() {
|
|
133
|
+
count++
|
|
134
|
+
running++
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
fun finished() {
|
|
138
|
+
running--
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// At a switch's start, the copy thread is still running a pre-create that already cost a switch the timeout.
|
|
142
|
+
fun skipsGpu(): Boolean = running > 0 && forgivenAt == count
|
|
143
|
+
|
|
144
|
+
// A failed GPU capture. busyAtStart and countAtStart are read when the capture started.
|
|
145
|
+
fun turnsGpuOff(failure: GpuFailure, busyAtStart: Boolean, countAtStart: Int): Boolean {
|
|
146
|
+
val copyThreadBusy = busyAtStart || running > 0 || count != countAtStart
|
|
147
|
+
if (failure == GpuFailure.TIMEOUT && copyThreadBusy) forgivenAt = count
|
|
148
|
+
return CapturePlan.turnsGpuOff(failure, copyThreadBusy)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// How a GPU capture ends: image, timeout or failure, whichever comes first. The rest get dropped and a late bitmap
|
|
153
|
+
// is recycled. onResult runs once. release runs once, after onResult for an image and before it for a failure.
|
|
154
|
+
// afterImage runs that release after an image: right away by default, or later (after the overlay's frame). Main
|
|
155
|
+
// thread only.
|
|
156
|
+
internal class CaptureEnd<B : Any>(
|
|
157
|
+
private val release: () -> Unit,
|
|
158
|
+
private val recycle: (B) -> Unit,
|
|
159
|
+
private val cancelTimeout: () -> Unit,
|
|
160
|
+
private val onResult: (B?, GpuFailure?) -> Unit,
|
|
161
|
+
private val afterImage: (() -> Unit) -> Unit = { it() },
|
|
162
|
+
) {
|
|
163
|
+
private var finished = false
|
|
164
|
+
|
|
165
|
+
fun image(bitmap: B?) {
|
|
166
|
+
if (finished) {
|
|
167
|
+
bitmap?.let(recycle)
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
finished = true
|
|
171
|
+
cancelTimeout()
|
|
172
|
+
if (bitmap == null) {
|
|
173
|
+
release()
|
|
174
|
+
onResult(null, GpuFailure.ERROR)
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
// Overlay goes up first, then the renderer goes away, once afterImage runs it.
|
|
178
|
+
try {
|
|
179
|
+
onResult(bitmap, null)
|
|
180
|
+
} finally {
|
|
181
|
+
afterImage(release)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
fun timeout() {
|
|
186
|
+
if (finished) return
|
|
187
|
+
finished = true
|
|
188
|
+
release()
|
|
189
|
+
onResult(null, GpuFailure.TIMEOUT)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
fun fail(failure: GpuFailure) {
|
|
193
|
+
if (finished) return
|
|
194
|
+
finished = true
|
|
195
|
+
cancelTimeout()
|
|
196
|
+
release()
|
|
197
|
+
onResult(null, failure)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
package expo.modules.themereveal
|
|
2
|
+
|
|
3
|
+
internal object CaptureRules {
|
|
4
|
+
// A press-in capture no switch uses is freed after this long, so a long press doesn't hold on to a full-window
|
|
5
|
+
// bitmap.
|
|
6
|
+
const val TIMEOUT_MS = 3_000L
|
|
7
|
+
|
|
8
|
+
// A capture's PixelCopy result is kept only if nothing voided the capture (ticket), no prepare started during the
|
|
9
|
+
// copy (generation) and no overlay is showing. Otherwise it could hold a frame from before another switch.
|
|
10
|
+
fun keepResult(
|
|
11
|
+
ticket: Int,
|
|
12
|
+
currentTicket: Int,
|
|
13
|
+
generation: Int,
|
|
14
|
+
currentGeneration: Int,
|
|
15
|
+
overlayShown: Boolean,
|
|
16
|
+
): Boolean = ticket == currentTicket && generation == currentGeneration && !overlayShown
|
|
17
|
+
|
|
18
|
+
// A held capture can stand in for prepare's own capture if nothing voided it (ticket, a guard: voiding also frees
|
|
19
|
+
// it), no other prepare ran since it started (generation is the one before this prepare's), it shows the window
|
|
20
|
+
// prepare would capture, that window still has the snapshot's size, and it's no older than TIMEOUT_MS.
|
|
21
|
+
fun usable(
|
|
22
|
+
ticket: Int,
|
|
23
|
+
currentTicket: Int,
|
|
24
|
+
generation: Int,
|
|
25
|
+
generationBefore: Int,
|
|
26
|
+
sameWindow: Boolean,
|
|
27
|
+
snapshotW: Int,
|
|
28
|
+
snapshotH: Int,
|
|
29
|
+
coverW: Int,
|
|
30
|
+
coverH: Int,
|
|
31
|
+
takenAtMs: Long,
|
|
32
|
+
nowMs: Long,
|
|
33
|
+
): Boolean {
|
|
34
|
+
val age = nowMs - takenAtMs
|
|
35
|
+
return ticket == currentTicket && generation == generationBefore && sameWindow &&
|
|
36
|
+
Motion.sameSize(snapshotW, snapshotH, coverW, coverH) && age >= 0 && age < TIMEOUT_MS
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// The capture target for the next switch (KeptTarget) is only prepared while no switch or press-in is using the
|
|
40
|
+
// copy thread or the RenderThread: no overlay showing, no press-in capture running or held, no switch capture
|
|
41
|
+
// running. The prepare blocks the copy thread, which is where those captures get their images. Foreground only,
|
|
42
|
+
// too.
|
|
43
|
+
fun keepsTargetNow(
|
|
44
|
+
overlayShown: Boolean,
|
|
45
|
+
captureRunning: Boolean,
|
|
46
|
+
captureHeld: Boolean,
|
|
47
|
+
switchCapturing: Boolean,
|
|
48
|
+
resumed: Boolean,
|
|
49
|
+
): Boolean = !overlayShown && !captureRunning && !captureHeld && !switchCapturing && resumed
|
|
50
|
+
}
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
package expo.modules.themereveal
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.content.res.Configuration
|
|
5
|
+
import android.graphics.Bitmap
|
|
6
|
+
import android.graphics.Canvas
|
|
7
|
+
import android.graphics.ColorSpace
|
|
8
|
+
import android.graphics.HardwareRenderer
|
|
9
|
+
import android.graphics.PixelFormat
|
|
10
|
+
import android.graphics.Rect
|
|
11
|
+
import android.graphics.RenderNode
|
|
12
|
+
import android.hardware.HardwareBuffer
|
|
13
|
+
import android.media.ImageReader
|
|
14
|
+
import android.os.Build
|
|
15
|
+
import android.os.Handler
|
|
16
|
+
import android.util.DisplayMetrics
|
|
17
|
+
import android.util.TypedValue
|
|
18
|
+
import android.util.Log
|
|
19
|
+
import android.view.Choreographer
|
|
20
|
+
import android.view.View
|
|
21
|
+
import androidx.annotation.RequiresApi
|
|
22
|
+
|
|
23
|
+
// Elevation shadows: where the light is and how dark its shadows are.
|
|
24
|
+
internal class Lighting(val source: LightSource, val ambientAlpha: Float, val spotAlpha: Float)
|
|
25
|
+
|
|
26
|
+
// GPU capture target made ready ahead of the capture (GpuSnapshot.prepareOutput): its reader, its renderer with the
|
|
27
|
+
// surface already made, and its root node. release frees it, once, unless a capture took it. Called on main, the work
|
|
28
|
+
// runs on the copy thread (GpuSnapshot.free). what names it in the debug log.
|
|
29
|
+
@RequiresApi(Build.VERSION_CODES.Q)
|
|
30
|
+
internal class GpuOutput(
|
|
31
|
+
val width: Int,
|
|
32
|
+
val height: Int,
|
|
33
|
+
val reader: ImageReader,
|
|
34
|
+
val renderer: HardwareRenderer,
|
|
35
|
+
val node: RenderNode,
|
|
36
|
+
private val readerHandler: Handler,
|
|
37
|
+
) {
|
|
38
|
+
fun release(what: String = "an unused prepared target") =
|
|
39
|
+
GpuSnapshot.free(readerHandler, renderer, reader, node, what)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// API 29+: window snapshot drawn on the GPU into a hardware bitmap. No readback to the CPU and no upload back to the
|
|
43
|
+
// GPU like PixelCopy needs.
|
|
44
|
+
@RequiresApi(Build.VERSION_CODES.Q)
|
|
45
|
+
internal object GpuSnapshot {
|
|
46
|
+
private const val USAGE = HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE or HardwareBuffer.USAGE_GPU_COLOR_OUTPUT
|
|
47
|
+
|
|
48
|
+
private const val TAG = "ThemeReveal"
|
|
49
|
+
|
|
50
|
+
// Debug log name for releasing a capture's own renderer, as opposed to an unused or kept target.
|
|
51
|
+
const val CAPTURE_RENDERER = "a capture's renderer"
|
|
52
|
+
|
|
53
|
+
// Longest a capture's release waits for the frame after its image. No frame comes while the screen is off.
|
|
54
|
+
private const val RELEASE_TIMEOUT_MS = 100L
|
|
55
|
+
|
|
56
|
+
// Light the window's renderer uses for elevation shadows (ThreadedRenderer), or null if the theme doesn't define
|
|
57
|
+
// it.
|
|
58
|
+
fun lightOf(activity: Activity, decor: View): Lighting? {
|
|
59
|
+
val display = decor.display ?: return null
|
|
60
|
+
val theme = activity.theme
|
|
61
|
+
val value = TypedValue()
|
|
62
|
+
fun dimension(name: String): Float? {
|
|
63
|
+
// lightY, lightZ and lightRadius are private framework attributes.
|
|
64
|
+
@Suppress("DiscouragedApi")
|
|
65
|
+
val id = activity.resources.getIdentifier(name, "attr", "android")
|
|
66
|
+
if (id == 0 || !theme.resolveAttribute(id, value, true)) return null
|
|
67
|
+
return value.getDimension(activity.resources.displayMetrics)
|
|
68
|
+
}
|
|
69
|
+
val lightY = dimension("lightY") ?: return null
|
|
70
|
+
val lightZ = dimension("lightZ") ?: return null
|
|
71
|
+
val radius = dimension("lightRadius") ?: return null
|
|
72
|
+
if (!theme.resolveAttribute(android.R.attr.ambientShadowAlpha, value, true)) return null
|
|
73
|
+
val ambient = value.float
|
|
74
|
+
if (!theme.resolveAttribute(android.R.attr.spotShadowAlpha, value, true)) return null
|
|
75
|
+
val spot = value.float
|
|
76
|
+
val metrics = DisplayMetrics()
|
|
77
|
+
@Suppress("DEPRECATION")
|
|
78
|
+
display.getRealMetrics(metrics)
|
|
79
|
+
val inWindow = IntArray(2).also { decor.getLocationInWindow(it) }
|
|
80
|
+
val onScreen = WindowSnapshot.locationOf(decor)
|
|
81
|
+
val source = CapturePlan.light(
|
|
82
|
+
Build.VERSION.SDK_INT,
|
|
83
|
+
metrics.widthPixels,
|
|
84
|
+
metrics.heightPixels,
|
|
85
|
+
metrics.density,
|
|
86
|
+
onScreen[0] - inWindow[0],
|
|
87
|
+
onScreen[1] - inWindow[1],
|
|
88
|
+
lightY,
|
|
89
|
+
lightZ,
|
|
90
|
+
radius,
|
|
91
|
+
)
|
|
92
|
+
return Lighting(source, ambient, spot)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Draws a window's views the way its renderer does: in place in the window, with Z order and shadows. Child views
|
|
96
|
+
// draw the display lists they already have. UI thread only.
|
|
97
|
+
fun drawWindow(canvas: Canvas, decor: View) {
|
|
98
|
+
val at = IntArray(2).also { decor.getLocationInWindow(it) }
|
|
99
|
+
val count = canvas.save()
|
|
100
|
+
canvas.translate(at[0].toFloat(), at[1].toFloat())
|
|
101
|
+
canvas.enableZ()
|
|
102
|
+
decor.draw(canvas)
|
|
103
|
+
canvas.disableZ()
|
|
104
|
+
canvas.restoreToCount(count)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Warm-up, off the main thread: a 1x1 capture with no window in it. Loads the reader's and renderer's classes and
|
|
108
|
+
// native code and draws one frame, so the first switch doesn't have to. Draws no view, so it can run off main.
|
|
109
|
+
fun preCreate() {
|
|
110
|
+
val node = RenderNode("ThemeRevealWarmUp")
|
|
111
|
+
val images = ImageReader.newInstance(1, 1, PixelFormat.RGBA_8888, 1, USAGE)
|
|
112
|
+
var renderer: HardwareRenderer? = null
|
|
113
|
+
try {
|
|
114
|
+
node.setPosition(0, 0, 1, 1)
|
|
115
|
+
node.beginRecording(1, 1)
|
|
116
|
+
node.endRecording()
|
|
117
|
+
val output = HardwareRenderer()
|
|
118
|
+
renderer = output
|
|
119
|
+
output.setSurface(images.surface)
|
|
120
|
+
output.setContentRoot(node)
|
|
121
|
+
output.setOpaque(false)
|
|
122
|
+
output.setLightSourceGeometry(0f, 0f, 1f, 1f)
|
|
123
|
+
output.setLightSourceAlpha(0f, 0f)
|
|
124
|
+
output.createRenderRequest().syncAndDraw()
|
|
125
|
+
// Waits until the frame is drawn.
|
|
126
|
+
output.stop()
|
|
127
|
+
images.acquireLatestImage()?.use { image ->
|
|
128
|
+
image.hardwareBuffer?.use { Bitmap.wrapHardwareBuffer(it, ColorSpace.get(ColorSpace.Named.SRGB))?.recycle() }
|
|
129
|
+
}
|
|
130
|
+
} finally {
|
|
131
|
+
renderer?.destroy()
|
|
132
|
+
images.close()
|
|
133
|
+
node.discardDisplayList()
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Target side of pre-importing a held snapshot.
|
|
138
|
+
private const val PRE_IMPORT_SIZE = 8
|
|
139
|
+
|
|
140
|
+
// Records the snapshot, drawn scaled down, into a tiny node. Runs on the UI thread so freeing the bitmap can't
|
|
141
|
+
// race the recording. The display list keeps the bitmap's buffer alive until preImport discards it. Draws no
|
|
142
|
+
// view.
|
|
143
|
+
fun preImportNode(bitmap: Bitmap): RenderNode {
|
|
144
|
+
val node = RenderNode("ThemeRevealPreImport")
|
|
145
|
+
try {
|
|
146
|
+
node.setPosition(0, 0, PRE_IMPORT_SIZE, PRE_IMPORT_SIZE)
|
|
147
|
+
val canvas = node.beginRecording(PRE_IMPORT_SIZE, PRE_IMPORT_SIZE)
|
|
148
|
+
try {
|
|
149
|
+
canvas.drawBitmap(bitmap, null, Rect(0, 0, PRE_IMPORT_SIZE, PRE_IMPORT_SIZE), null)
|
|
150
|
+
} finally {
|
|
151
|
+
node.endRecording()
|
|
152
|
+
}
|
|
153
|
+
} catch (thrown: RuntimeException) {
|
|
154
|
+
node.discardDisplayList()
|
|
155
|
+
throw thrown
|
|
156
|
+
}
|
|
157
|
+
return node
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Draws the node into a tiny offscreen target and waits for the frame, then frees target and node. The shared
|
|
161
|
+
// RenderThread imports the snapshot's buffer as a texture now, during the hold, instead of in the overlay's first
|
|
162
|
+
// frame. Draws no view, so it runs off main.
|
|
163
|
+
fun preImport(node: RenderNode) {
|
|
164
|
+
var images: ImageReader? = null
|
|
165
|
+
var renderer: HardwareRenderer? = null
|
|
166
|
+
try {
|
|
167
|
+
val reader = ImageReader.newInstance(PRE_IMPORT_SIZE, PRE_IMPORT_SIZE, PixelFormat.RGBA_8888, 1, USAGE)
|
|
168
|
+
images = reader
|
|
169
|
+
val output = HardwareRenderer()
|
|
170
|
+
renderer = output
|
|
171
|
+
output.setSurface(reader.surface)
|
|
172
|
+
output.setContentRoot(node)
|
|
173
|
+
output.setOpaque(false)
|
|
174
|
+
output.setLightSourceGeometry(0f, 0f, 1f, 1f)
|
|
175
|
+
output.setLightSourceAlpha(0f, 0f)
|
|
176
|
+
output.createRenderRequest().syncAndDraw()
|
|
177
|
+
// Waits until the frame is drawn.
|
|
178
|
+
output.stop()
|
|
179
|
+
reader.acquireLatestImage()?.close()
|
|
180
|
+
} finally {
|
|
181
|
+
renderer?.destroy()
|
|
182
|
+
images?.close()
|
|
183
|
+
node.discardDisplayList()
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Gets a width x height capture target ready for a later capture: draws one empty frame into it and waits for that
|
|
188
|
+
// frame, so the surface exists on the RenderThread and its buffer is allocated. The capture's frame reuses that
|
|
189
|
+
// buffer. Draws no view, so it runs off main, on readerHandler's thread. The renderer stays started, since stop()
|
|
190
|
+
// would let its surface go.
|
|
191
|
+
fun prepareOutput(width: Int, height: Int, readerHandler: Handler): GpuOutput {
|
|
192
|
+
val node = RenderNode("ThemeRevealSnapshot")
|
|
193
|
+
var images: ImageReader? = null
|
|
194
|
+
var renderer: HardwareRenderer? = null
|
|
195
|
+
try {
|
|
196
|
+
val reader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 1, USAGE)
|
|
197
|
+
images = reader
|
|
198
|
+
node.setPosition(0, 0, width, height)
|
|
199
|
+
node.beginRecording(width, height)
|
|
200
|
+
node.endRecording()
|
|
201
|
+
val output = HardwareRenderer()
|
|
202
|
+
renderer = output
|
|
203
|
+
output.setSurface(reader.surface)
|
|
204
|
+
output.setContentRoot(node)
|
|
205
|
+
output.setOpaque(false)
|
|
206
|
+
output.setLightSourceGeometry(0f, 0f, 1f, 1f)
|
|
207
|
+
output.setLightSourceAlpha(0f, 0f)
|
|
208
|
+
// Waits until the frame is presented, so its image is already in the reader.
|
|
209
|
+
output.createRenderRequest().setWaitForPresent(true).syncAndDraw()
|
|
210
|
+
// Back to the reader, so the capture's frame dequeues this buffer again.
|
|
211
|
+
reader.acquireNextImage()?.close()
|
|
212
|
+
return GpuOutput(width, height, reader, output, node, readerHandler)
|
|
213
|
+
} catch (thrown: Throwable) {
|
|
214
|
+
renderer?.destroy()
|
|
215
|
+
images?.close()
|
|
216
|
+
node.discardDisplayList()
|
|
217
|
+
throw thrown
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Runs release on handler after the next frame, which is the overlay's frame when an image just attached the
|
|
222
|
+
// overlay. release only posts the work to the copy thread (free), so its RenderThread work lands after that frame.
|
|
223
|
+
// A timeout runs it if no frame comes.
|
|
224
|
+
private fun afterNextFrame(handler: Handler, release: () -> Unit) {
|
|
225
|
+
val once = Once(release)
|
|
226
|
+
lateinit var timeout: Runnable
|
|
227
|
+
val frame = Choreographer.FrameCallback {
|
|
228
|
+
handler.removeCallbacks(timeout)
|
|
229
|
+
// A message posted after this one runs after the frame's traversal.
|
|
230
|
+
handler.post { once.run() }
|
|
231
|
+
}
|
|
232
|
+
timeout = Runnable {
|
|
233
|
+
Choreographer.getInstance().removeFrameCallback(frame)
|
|
234
|
+
once.run()
|
|
235
|
+
}
|
|
236
|
+
Choreographer.getInstance().postFrameCallback(frame)
|
|
237
|
+
handler.postDelayed(timeout, RELEASE_TIMEOUT_MS)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Frees a capture's renderer, reader and node on readerHandler's thread (the reader's own) instead of main:
|
|
241
|
+
// renderer.destroy waits for the RenderThread, 1.0-19.4 ms on a Mi 11 Lite. By then nothing else uses them. Main is
|
|
242
|
+
// done with the renderer, and its stop() and the image listener already ran on that same thread, so the close can't
|
|
243
|
+
// free an image the listener is still reading. A bitmap from the image holds its own reference to the buffer.
|
|
244
|
+
fun free(
|
|
245
|
+
readerHandler: Handler,
|
|
246
|
+
renderer: HardwareRenderer?,
|
|
247
|
+
reader: ImageReader?,
|
|
248
|
+
node: RenderNode,
|
|
249
|
+
what: String = CAPTURE_RENDERER,
|
|
250
|
+
) {
|
|
251
|
+
readerHandler.post {
|
|
252
|
+
val start = System.nanoTime()
|
|
253
|
+
try {
|
|
254
|
+
renderer?.destroy()
|
|
255
|
+
} finally {
|
|
256
|
+
reader?.close()
|
|
257
|
+
node.discardDisplayList()
|
|
258
|
+
}
|
|
259
|
+
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
|
260
|
+
val ms = (System.nanoTime() - start) / 1e6
|
|
261
|
+
Log.d(TAG, "released $what in %.1f ms on the copy thread".format(ms))
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Force dark can darken this activity's windows (see CapturePlan.forceDarkCanApply).
|
|
267
|
+
fun forceDarkCanApply(activity: Activity): Boolean {
|
|
268
|
+
val value = TypedValue()
|
|
269
|
+
val theme = activity.theme
|
|
270
|
+
val lightTheme = !theme.resolveAttribute(android.R.attr.isLightTheme, value, true) || value.data != 0
|
|
271
|
+
val allowed = theme.resolveAttribute(android.R.attr.forceDarkAllowed, value, true) && value.data != 0
|
|
272
|
+
val night = activity.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
|
|
273
|
+
Configuration.UI_MODE_NIGHT_YES
|
|
274
|
+
return CapturePlan.forceDarkCanApply(Build.VERSION.SDK_INT, night, lightTheme, allowed)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Records draw on the UI thread, renders it into a hardware buffer and calls onResult once on handler, with the
|
|
278
|
+
// bitmap or with null and the reason. readerHandler gets the image, since taking it waits for the GPU to finish.
|
|
279
|
+
// redraw lists the decor views of windows that had a view change waiting for its frame. prepared: a target from
|
|
280
|
+
// prepareOutput, made with the same readerHandler, to draw into instead of a new one. The capture takes it and frees
|
|
281
|
+
// it when done. One of another size is freed right away.
|
|
282
|
+
fun capture(
|
|
283
|
+
width: Int,
|
|
284
|
+
height: Int,
|
|
285
|
+
light: Lighting,
|
|
286
|
+
redraw: List<View>,
|
|
287
|
+
handler: Handler,
|
|
288
|
+
readerHandler: Handler,
|
|
289
|
+
draw: (Canvas) -> Unit,
|
|
290
|
+
prepared: GpuOutput?,
|
|
291
|
+
onResult: (Bitmap?, GpuFailure?) -> Unit,
|
|
292
|
+
) {
|
|
293
|
+
val ready = prepared?.takeIf { it.width == width && it.height == height }
|
|
294
|
+
if (prepared != null && ready == null) prepared.release()
|
|
295
|
+
val node = ready?.node ?: RenderNode("ThemeRevealSnapshot")
|
|
296
|
+
var reader: ImageReader? = null
|
|
297
|
+
var renderer: HardwareRenderer? = null
|
|
298
|
+
lateinit var timeout: Runnable
|
|
299
|
+
val end = CaptureEnd<Bitmap>(
|
|
300
|
+
release = {
|
|
301
|
+
if (ready != null) ready.release(CAPTURE_RENDERER) else free(readerHandler, renderer, reader, node)
|
|
302
|
+
},
|
|
303
|
+
recycle = { it.recycle() },
|
|
304
|
+
cancelTimeout = { handler.removeCallbacks(timeout) },
|
|
305
|
+
onResult = onResult,
|
|
306
|
+
afterImage = { release -> afterNextFrame(handler, release) },
|
|
307
|
+
)
|
|
308
|
+
timeout = Runnable { end.timeout() }
|
|
309
|
+
try {
|
|
310
|
+
node.setPosition(0, 0, width, height)
|
|
311
|
+
val canvas = node.beginRecording(width, height)
|
|
312
|
+
try {
|
|
313
|
+
draw(canvas)
|
|
314
|
+
} finally {
|
|
315
|
+
node.endRecording()
|
|
316
|
+
}
|
|
317
|
+
val images = ready?.reader ?: ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 1, USAGE)
|
|
318
|
+
reader = images
|
|
319
|
+
images.setOnImageAvailableListener({ source ->
|
|
320
|
+
val bitmap = try {
|
|
321
|
+
source.acquireLatestImage()?.use { image ->
|
|
322
|
+
image.hardwareBuffer?.use { Bitmap.wrapHardwareBuffer(it, ColorSpace.get(ColorSpace.Named.SRGB)) }
|
|
323
|
+
}
|
|
324
|
+
} catch (_: RuntimeException) {
|
|
325
|
+
null
|
|
326
|
+
}
|
|
327
|
+
// Front of the queue, like the PixelCopy listener, so the overlay can still make a frame that's due.
|
|
328
|
+
handler.postAtFrontOfQueue { end.image(bitmap) }
|
|
329
|
+
}, readerHandler)
|
|
330
|
+
val output = ready?.renderer ?: HardwareRenderer()
|
|
331
|
+
renderer = output
|
|
332
|
+
if (ready == null) {
|
|
333
|
+
output.setSurface(images.surface)
|
|
334
|
+
output.setContentRoot(node)
|
|
335
|
+
output.setOpaque(false)
|
|
336
|
+
}
|
|
337
|
+
output.setLightSourceGeometry(light.source.x, light.source.y, light.source.z, light.source.radius)
|
|
338
|
+
output.setLightSourceAlpha(light.ambientAlpha, light.spotAlpha)
|
|
339
|
+
val result = output.createRenderRequest().syncAndDraw()
|
|
340
|
+
// The sync consumed the damage of views that changed since the window's last frame, so the window would skip
|
|
341
|
+
// redrawing them. Redraw these windows in full.
|
|
342
|
+
redraw.forEach { it.invalidate() }
|
|
343
|
+
if (CapturePlan.syncFailed(result)) {
|
|
344
|
+
end.fail(GpuFailure.SYNC)
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
// A RenderThread animation in the window (a ripple, say) would make this renderer redraw the whole window
|
|
348
|
+
// every vsync until release. Stop it once its frame is drawn. stop() waits for that on the RenderThread, so
|
|
349
|
+
// it runs off the main thread. The release (free) runs later on the same thread.
|
|
350
|
+
readerHandler.post {
|
|
351
|
+
try {
|
|
352
|
+
output.stop()
|
|
353
|
+
} catch (_: RuntimeException) {
|
|
354
|
+
// Nothing to stop.
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
} catch (_: RuntimeException) {
|
|
358
|
+
end.fail(GpuFailure.ERROR)
|
|
359
|
+
return
|
|
360
|
+
} catch (_: OutOfMemoryError) {
|
|
361
|
+
// HardwareRenderer throws it when HWUI can't make its native proxy.
|
|
362
|
+
end.fail(GpuFailure.ERROR)
|
|
363
|
+
return
|
|
364
|
+
}
|
|
365
|
+
handler.postDelayed(timeout, CapturePlan.GPU_TIMEOUT_MS)
|
|
366
|
+
}
|
|
367
|
+
}
|