react-native-worklets 0.8.0 → 0.9.0-nightly-20260320-548038843
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/Common/cpp/worklets/SharedItems/Serializable.h +2 -2
- package/Common/cpp/worklets/Tools/WorkletsJSIUtils.h +3 -3
- package/Common/cpp/worklets/WorkletRuntime/UIRuntimeDecorator.cpp +3 -2
- package/Common/cpp/worklets/WorkletRuntime/UIRuntimeDecorator.h +2 -1
- package/android/src/main/java/com/swmansion/worklets/AndroidUIScheduler.kt +54 -0
- package/android/src/main/java/com/swmansion/worklets/JSCallInvokerResolver.kt +28 -0
- package/android/src/main/java/com/swmansion/worklets/ScriptBufferWrapper.kt +89 -0
- package/android/src/main/java/com/swmansion/worklets/WorkletsMessageQueueThread.kt +11 -0
- package/android/src/main/java/com/swmansion/worklets/WorkletsMessageQueueThreadBase.kt +59 -0
- package/android/src/main/java/com/swmansion/worklets/WorkletsPackage.kt +39 -0
- package/android/src/main/java/com/swmansion/worklets/runloop/AnimationFrameCallback.kt +11 -0
- package/android/src/main/java/com/swmansion/worklets/runloop/AnimationFrameQueue.kt +106 -0
- package/android/src/networking/com/swmansion/worklets/WorkletsModule.kt +164 -0
- package/android/src/no-networking/com/swmansion/worklets/WorkletsModule.kt +127 -0
- package/lib/module/debug/jsVersion.js +1 -1
- package/lib/module/debug/jsVersion.js.map +1 -1
- package/lib/module/memory/serializable.native.js +3 -4
- package/lib/module/memory/serializable.native.js.map +1 -1
- package/lib/typescript/debug/jsVersion.d.ts +1 -1
- package/lib/typescript/debug/jsVersion.d.ts.map +1 -1
- package/lib/typescript/memory/serializable.native.d.ts.map +1 -1
- package/package.json +9 -9
- package/src/debug/jsVersion.ts +1 -1
- package/src/memory/serializable.native.ts +3 -4
- package/android/src/main/java/com/swmansion/worklets/AndroidUIScheduler.java +0 -60
- package/android/src/main/java/com/swmansion/worklets/JSCallInvokerResolver.java +0 -27
- package/android/src/main/java/com/swmansion/worklets/ScriptBufferWrapper.java +0 -88
- package/android/src/main/java/com/swmansion/worklets/WorkletsMessageQueueThread.java +0 -16
- package/android/src/main/java/com/swmansion/worklets/WorkletsMessageQueueThreadBase.java +0 -73
- package/android/src/main/java/com/swmansion/worklets/WorkletsPackage.java +0 -48
- package/android/src/main/java/com/swmansion/worklets/runloop/AnimationFrameCallback.java +0 -19
- package/android/src/main/java/com/swmansion/worklets/runloop/AnimationFrameQueue.java +0 -113
- package/android/src/networking/com/swmansion/worklets/WorkletsModule.java +0 -174
- package/android/src/no-networking/com/swmansion/worklets/WorkletsModule.java +0 -137
|
@@ -52,7 +52,7 @@ class RetainingSerializable : virtual public BaseClass {
|
|
|
52
52
|
explicit RetainingSerializable(jsi::Runtime &rt, Args &&...args)
|
|
53
53
|
: BaseClass(rt, std::forward<Args>(args)...), primaryRuntime_(&rt) {}
|
|
54
54
|
|
|
55
|
-
jsi::Value toJSValue(jsi::Runtime &rt) {
|
|
55
|
+
jsi::Value toJSValue(jsi::Runtime &rt) override {
|
|
56
56
|
if (&rt == primaryRuntime_) {
|
|
57
57
|
// TODO: it is suboptimal to generate new object every time getJS is
|
|
58
58
|
// called on host runtime – the objects we are generating already exists
|
|
@@ -74,7 +74,7 @@ class RetainingSerializable : virtual public BaseClass {
|
|
|
74
74
|
return BaseClass::toJSValue(rt);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
~RetainingSerializable() {
|
|
77
|
+
~RetainingSerializable() override {
|
|
78
78
|
cleanupIfRuntimeExists(secondaryRuntime_, secondaryValue_);
|
|
79
79
|
}
|
|
80
80
|
};
|
|
@@ -84,7 +84,7 @@ getArgsForFunction(std::function<Ret(Args...)>, jsi::Runtime &rt, const jsi::Val
|
|
|
84
84
|
// passing `rt` as the first argument
|
|
85
85
|
template <typename Ret, typename... Args>
|
|
86
86
|
std::tuple<jsi::Runtime &, Args...> getArgsForFunction(
|
|
87
|
-
std::function<Ret(jsi::Runtime &, Args...)
|
|
87
|
+
const std::function<Ret(jsi::Runtime &, Args...)> &,
|
|
88
88
|
jsi::Runtime &rt,
|
|
89
89
|
const jsi::Value *args,
|
|
90
90
|
const size_t count) {
|
|
@@ -116,7 +116,7 @@ inline jsi::Value apply(std::function<void(Args...)> function, std::tuple<Args..
|
|
|
116
116
|
// returns a function with JSI calling convention
|
|
117
117
|
// from a native function `function`
|
|
118
118
|
template <typename Fun>
|
|
119
|
-
jsi::HostFunctionType createHostFunction(Fun function) {
|
|
119
|
+
jsi::HostFunctionType createHostFunction(const Fun &function) {
|
|
120
120
|
return [function](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *args, const size_t count) {
|
|
121
121
|
auto argz = getArgsForFunction(function, rt, args, count);
|
|
122
122
|
return apply(function, std::move(argz));
|
|
@@ -150,7 +150,7 @@ struct takes_runtime<jsi::Runtime &, Rest...> {
|
|
|
150
150
|
// and installs it as a global function named `name`
|
|
151
151
|
// in the `rt` JS runtime
|
|
152
152
|
template <typename Ret, typename... Args>
|
|
153
|
-
void installJsiFunction(jsi::Runtime &rt, std::string_view name, std::function<Ret(Args...)> function) {
|
|
153
|
+
void installJsiFunction(jsi::Runtime &rt, std::string_view name, const std::function<Ret(Args...)> &function) {
|
|
154
154
|
auto clb = createHostFunction(function);
|
|
155
155
|
auto argsCount = sizeof...(Args) - takes_runtime<Args...>::value;
|
|
156
156
|
jsi::Value jsiFunction =
|
|
@@ -8,12 +8,13 @@ namespace worklets {
|
|
|
8
8
|
|
|
9
9
|
void UIRuntimeDecorator::decorate(
|
|
10
10
|
facebook::jsi::Runtime &uiRuntime,
|
|
11
|
-
std::function<void(facebook::jsi::Runtime &rt, const facebook::jsi::Value &callback)>
|
|
11
|
+
const std::function<void(facebook::jsi::Runtime &rt, const facebook::jsi::Value &callback)>
|
|
12
|
+
&requestAnimationFrame) {
|
|
12
13
|
uiRuntime.global().setProperty(uiRuntime, runtimeKindBindingName, static_cast<int>(RuntimeKind::UI));
|
|
13
14
|
|
|
14
15
|
uiRuntime.global().setProperty(uiRuntime, "_UI", true);
|
|
15
16
|
|
|
16
|
-
jsi_utils::installJsiFunction(uiRuntime, "requestAnimationFrame",
|
|
17
|
+
jsi_utils::installJsiFunction(uiRuntime, "requestAnimationFrame", requestAnimationFrame);
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
} // namespace worklets
|
|
@@ -8,7 +8,8 @@ class UIRuntimeDecorator {
|
|
|
8
8
|
public:
|
|
9
9
|
static void decorate(
|
|
10
10
|
facebook::jsi::Runtime &uiRuntime,
|
|
11
|
-
std::function<void(facebook::jsi::Runtime &rt, const facebook::jsi::Value &callback)>
|
|
11
|
+
const std::function<void(facebook::jsi::Runtime &rt, const facebook::jsi::Value &callback)>
|
|
12
|
+
&requestAnimationFrame);
|
|
12
13
|
};
|
|
13
14
|
|
|
14
15
|
} // namespace worklets
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
package com.swmansion.worklets
|
|
2
|
+
|
|
3
|
+
import com.facebook.jni.HybridData
|
|
4
|
+
import com.facebook.proguard.annotations.DoNotStrip
|
|
5
|
+
import com.facebook.react.bridge.GuardedRunnable
|
|
6
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
7
|
+
import com.facebook.react.bridge.UiThreadUtil
|
|
8
|
+
import java.util.concurrent.atomic.AtomicBoolean
|
|
9
|
+
|
|
10
|
+
@Suppress("KotlinJniMissingFunction")
|
|
11
|
+
class AndroidUIScheduler(context: ReactApplicationContext) {
|
|
12
|
+
|
|
13
|
+
@field:DoNotStrip
|
|
14
|
+
private val mHybridData: HybridData = initHybrid()
|
|
15
|
+
|
|
16
|
+
private val mContext: ReactApplicationContext = context
|
|
17
|
+
private val mActive = AtomicBoolean(true)
|
|
18
|
+
|
|
19
|
+
private val mUIThreadRunnable = Runnable {
|
|
20
|
+
// This callback is called on the UI thread, but the module is invalidated on the JS
|
|
21
|
+
// thread. Therefore, we must synchronize for reloads. Without synchronization the cpp part
|
|
22
|
+
// gets torn down while the UI thread is still executing it, leading to crashes.
|
|
23
|
+
synchronized(mActive) {
|
|
24
|
+
if (mActive.get()) {
|
|
25
|
+
triggerUI()
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private external fun initHybrid(): HybridData
|
|
31
|
+
|
|
32
|
+
external fun triggerUI()
|
|
33
|
+
|
|
34
|
+
external fun invalidate()
|
|
35
|
+
|
|
36
|
+
@DoNotStrip
|
|
37
|
+
@Suppress("unused")
|
|
38
|
+
private fun scheduleTriggerOnUI() {
|
|
39
|
+
UiThreadUtil.runOnUiThread(
|
|
40
|
+
object : GuardedRunnable(mContext.exceptionHandler) {
|
|
41
|
+
override fun runGuarded() {
|
|
42
|
+
mUIThreadRunnable.run()
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
fun deactivate() {
|
|
49
|
+
synchronized(mActive) {
|
|
50
|
+
mActive.set(false)
|
|
51
|
+
invalidate()
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
package com.swmansion.worklets
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
4
|
+
import com.facebook.react.common.annotations.FrameworkAPI
|
|
5
|
+
import com.facebook.react.turbomodule.core.CallInvokerHolderImpl
|
|
6
|
+
|
|
7
|
+
object JSCallInvokerResolver {
|
|
8
|
+
|
|
9
|
+
@OptIn(FrameworkAPI::class)
|
|
10
|
+
@JvmStatic
|
|
11
|
+
fun getJSCallInvokerHolder(context: ReactApplicationContext): CallInvokerHolderImpl {
|
|
12
|
+
try {
|
|
13
|
+
val method = context.javaClass.getMethod("getJSCallInvokerHolder")
|
|
14
|
+
return method.invoke(context) as CallInvokerHolderImpl
|
|
15
|
+
} catch (_: Exception) {
|
|
16
|
+
// In newer implementations, the method is in CatalystInstance, continue.
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
val catalystInstance =
|
|
20
|
+
context.javaClass.getMethod("getCatalystInstance").invoke(context)
|
|
21
|
+
checkNotNull(catalystInstance)
|
|
22
|
+
val method = catalystInstance.javaClass.getMethod("getJSCallInvokerHolder")
|
|
23
|
+
return method.invoke(catalystInstance) as CallInvokerHolderImpl
|
|
24
|
+
} catch (e: Exception) {
|
|
25
|
+
throw RuntimeException("Failed to get JSCallInvokerHolder", e)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
package com.swmansion.worklets
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import android.content.res.AssetManager
|
|
5
|
+
import android.os.Build
|
|
6
|
+
import com.facebook.jni.HybridData
|
|
7
|
+
import com.facebook.proguard.annotations.DoNotStrip
|
|
8
|
+
import com.facebook.proguard.annotations.DoNotStripAny
|
|
9
|
+
import java.io.IOException
|
|
10
|
+
import java.io.InputStream
|
|
11
|
+
import java.net.HttpURLConnection
|
|
12
|
+
import java.net.URL
|
|
13
|
+
import java.nio.charset.StandardCharsets
|
|
14
|
+
|
|
15
|
+
/** A wrapper around a JavaScript bundle that is backed by a native C++ object. */
|
|
16
|
+
@Suppress("KotlinJniMissingFunction")
|
|
17
|
+
@SuppressLint("MissingNativeLoadLibrary")
|
|
18
|
+
@DoNotStripAny
|
|
19
|
+
class ScriptBufferWrapper(uri: String, assetManager: AssetManager) {
|
|
20
|
+
|
|
21
|
+
@field:DoNotStrip
|
|
22
|
+
private val mHybridData: HybridData
|
|
23
|
+
|
|
24
|
+
init {
|
|
25
|
+
val filePrefix = "file://"
|
|
26
|
+
val assetsPrefix = "assets://"
|
|
27
|
+
|
|
28
|
+
mHybridData = when {
|
|
29
|
+
uri.startsWith(filePrefix) -> {
|
|
30
|
+
val fileName = uri.substring(filePrefix.length)
|
|
31
|
+
initHybridFromFile(fileName)
|
|
32
|
+
}
|
|
33
|
+
uri.startsWith(assetsPrefix) -> {
|
|
34
|
+
val assetURL = uri.substring(assetsPrefix.length)
|
|
35
|
+
initHybridFromAssets(assetManager, assetURL)
|
|
36
|
+
}
|
|
37
|
+
else -> {
|
|
38
|
+
val scriptContent = try {
|
|
39
|
+
downloadScript(uri)
|
|
40
|
+
} catch (e: IOException) {
|
|
41
|
+
throw RuntimeException(e)
|
|
42
|
+
}
|
|
43
|
+
initHybridFromString(scriptContent, uri)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private external fun initHybridFromAssets(assetManager: AssetManager, assetURL: String): HybridData
|
|
49
|
+
|
|
50
|
+
private external fun initHybridFromFile(fileName: String): HybridData
|
|
51
|
+
|
|
52
|
+
private external fun initHybridFromString(script: String, url: String): HybridData
|
|
53
|
+
|
|
54
|
+
companion object {
|
|
55
|
+
private fun downloadScript(url: String): String {
|
|
56
|
+
val scriptUrl = URL(url)
|
|
57
|
+
val connection = scriptUrl.openConnection() as HttpURLConnection
|
|
58
|
+
try {
|
|
59
|
+
val content: ByteArray
|
|
60
|
+
|
|
61
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
62
|
+
content = connection.inputStream.readAllBytes()
|
|
63
|
+
} else {
|
|
64
|
+
content = readBytes(connection.inputStream)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return String(content, StandardCharsets.UTF_8)
|
|
68
|
+
} finally {
|
|
69
|
+
connection.disconnect()
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Reads all bytes from an InputStream into a byte array for SDKs below 33. */
|
|
74
|
+
private fun readBytes(inputStream: InputStream): ByteArray {
|
|
75
|
+
val bufferSize = 4096
|
|
76
|
+
try {
|
|
77
|
+
val byteBuffer = java.io.ByteArrayOutputStream()
|
|
78
|
+
val buffer = ByteArray(bufferSize)
|
|
79
|
+
var len: Int
|
|
80
|
+
while (inputStream.read(buffer).also { len = it } != -1) {
|
|
81
|
+
byteBuffer.write(buffer, 0, len)
|
|
82
|
+
}
|
|
83
|
+
return byteBuffer.toByteArray()
|
|
84
|
+
} finally {
|
|
85
|
+
inputStream.close()
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
package com.swmansion.worklets
|
|
2
|
+
|
|
3
|
+
import com.facebook.proguard.annotations.DoNotStrip
|
|
4
|
+
|
|
5
|
+
@DoNotStrip
|
|
6
|
+
class WorkletsMessageQueueThread : WorkletsMessageQueueThreadBase() {
|
|
7
|
+
|
|
8
|
+
override fun runOnQueue(runnable: Runnable): Boolean = messageQueueThread.runOnQueue(runnable)
|
|
9
|
+
|
|
10
|
+
override fun isIdle(): Boolean = messageQueueThread.isIdle()
|
|
11
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
package com.swmansion.worklets
|
|
2
|
+
|
|
3
|
+
import com.facebook.proguard.annotations.DoNotStrip
|
|
4
|
+
import com.facebook.react.bridge.queue.MessageQueueThread
|
|
5
|
+
import com.facebook.react.bridge.queue.MessageQueueThreadImpl
|
|
6
|
+
import com.facebook.react.bridge.queue.MessageQueueThreadPerfStats
|
|
7
|
+
import com.facebook.react.bridge.queue.MessageQueueThreadSpec
|
|
8
|
+
import java.util.concurrent.Callable
|
|
9
|
+
import java.util.concurrent.Future
|
|
10
|
+
|
|
11
|
+
// This class is an almost exact copy of MessageQueueThreadImpl taken from here:
|
|
12
|
+
// https://github.com/facebook/react-native/blob/main/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThreadImpl.kt
|
|
13
|
+
// The only method that has changed is `quitSynchronous()` (see comment above
|
|
14
|
+
// function implementation for details).
|
|
15
|
+
@DoNotStrip
|
|
16
|
+
abstract class WorkletsMessageQueueThreadBase : MessageQueueThread {
|
|
17
|
+
|
|
18
|
+
protected val messageQueueThread: MessageQueueThreadImpl =
|
|
19
|
+
MessageQueueThreadImpl.create(
|
|
20
|
+
MessageQueueThreadSpec.mainThreadSpec()
|
|
21
|
+
) { exception ->
|
|
22
|
+
throw RuntimeException(exception)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
override fun <T> callOnQueue(callable: Callable<T>): Future<T> =
|
|
26
|
+
messageQueueThread.callOnQueue(callable)
|
|
27
|
+
|
|
28
|
+
override fun isOnThread(): Boolean = messageQueueThread.isOnThread()
|
|
29
|
+
|
|
30
|
+
override fun assertIsOnThread() {
|
|
31
|
+
messageQueueThread.assertIsOnThread()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
override fun assertIsOnThread(s: String) {
|
|
35
|
+
messageQueueThread.assertIsOnThread(s)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// We don't want to quit the main looper (which is what MessageQueueThreadImpl would have done),
|
|
39
|
+
// but we still want to prevent anything else from executing.
|
|
40
|
+
@Suppress("CallToPrintStackTrace")
|
|
41
|
+
override fun quitSynchronous() {
|
|
42
|
+
try {
|
|
43
|
+
val mIsFinished = messageQueueThread.javaClass.getDeclaredField("mIsFinished")
|
|
44
|
+
mIsFinished.isAccessible = true
|
|
45
|
+
mIsFinished.set(messageQueueThread, true)
|
|
46
|
+
mIsFinished.isAccessible = false
|
|
47
|
+
} catch (e: NoSuchFieldException) {
|
|
48
|
+
e.printStackTrace()
|
|
49
|
+
} catch (e: IllegalAccessException) {
|
|
50
|
+
e.printStackTrace()
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
override fun getPerfStats(): MessageQueueThreadPerfStats? = messageQueueThread.getPerfStats()
|
|
55
|
+
|
|
56
|
+
override fun resetPerfStats() {
|
|
57
|
+
messageQueueThread.resetPerfStats()
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
package com.swmansion.worklets
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.BaseReactPackage
|
|
4
|
+
import com.facebook.react.ReactPackage
|
|
5
|
+
import com.facebook.react.bridge.NativeModule
|
|
6
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
7
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
8
|
+
import com.facebook.react.module.annotations.ReactModuleList
|
|
9
|
+
import com.facebook.react.module.model.ReactModuleInfo
|
|
10
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
11
|
+
|
|
12
|
+
@ReactModuleList(nativeModules = [WorkletsModule::class])
|
|
13
|
+
class WorkletsPackage : BaseReactPackage(), ReactPackage {
|
|
14
|
+
|
|
15
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
|
|
16
|
+
if (name == WorkletsModule.NAME) WorkletsModule(reactContext) else null
|
|
17
|
+
|
|
18
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
|
|
19
|
+
val moduleList: Array<Class<*>> = arrayOf(WorkletsModule::class.java)
|
|
20
|
+
|
|
21
|
+
val reactModuleInfoMap = HashMap<String, ReactModuleInfo>()
|
|
22
|
+
for (moduleClass in moduleList) {
|
|
23
|
+
val reactModule: ReactModule =
|
|
24
|
+
checkNotNull(moduleClass.getAnnotation(ReactModule::class.java))
|
|
25
|
+
|
|
26
|
+
reactModuleInfoMap[reactModule.name] =
|
|
27
|
+
ReactModuleInfo(
|
|
28
|
+
reactModule.name,
|
|
29
|
+
moduleClass.name,
|
|
30
|
+
reactModule.canOverrideExistingModule,
|
|
31
|
+
reactModule.needsEagerInit,
|
|
32
|
+
reactModule.isCxxModule,
|
|
33
|
+
true
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return ReactModuleInfoProvider { reactModuleInfoMap }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
package com.swmansion.worklets.runloop
|
|
2
|
+
|
|
3
|
+
import com.facebook.jni.HybridData
|
|
4
|
+
import com.facebook.proguard.annotations.DoNotStrip
|
|
5
|
+
|
|
6
|
+
@Suppress("KotlinJniMissingFunction")
|
|
7
|
+
class AnimationFrameCallback @DoNotStrip private constructor(
|
|
8
|
+
@field:DoNotStrip private val mHybridData: HybridData
|
|
9
|
+
) {
|
|
10
|
+
external fun onAnimationFrame(timestampMs: Double)
|
|
11
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
package com.swmansion.worklets.runloop
|
|
2
|
+
|
|
3
|
+
import android.os.SystemClock
|
|
4
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
5
|
+
import com.facebook.react.modules.core.ReactChoreographer
|
|
6
|
+
import com.facebook.react.uimanager.GuardedFrameCallback
|
|
7
|
+
import java.util.concurrent.atomic.AtomicBoolean
|
|
8
|
+
|
|
9
|
+
class AnimationFrameQueue(reactApplicationContext: ReactApplicationContext) {
|
|
10
|
+
|
|
11
|
+
private var mFirstUptime = SystemClock.uptimeMillis()
|
|
12
|
+
private var mSlowAnimationsEnabled = false
|
|
13
|
+
private var lastFrameTimeMs = 0.0
|
|
14
|
+
private var mAnimationsDragFactor = 1
|
|
15
|
+
|
|
16
|
+
/// ReactChoreographer is
|
|
17
|
+
/// [thread safe](https://github.com/facebook/react-native/blob/main/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/ReactChoreographer.kt#L21).
|
|
18
|
+
private val mReactChoreographer: ReactChoreographer = ReactChoreographer.getInstance()
|
|
19
|
+
private val mChoreographerCallback: GuardedFrameCallback =
|
|
20
|
+
object : GuardedFrameCallback(reactApplicationContext) {
|
|
21
|
+
override fun doFrameGuarded(frameTimeNanos: Long) {
|
|
22
|
+
executeQueue(frameTimeNanos)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
private val mCallbackPosted = AtomicBoolean()
|
|
26
|
+
private val mPaused = AtomicBoolean()
|
|
27
|
+
private val mFrameCallbacks = mutableListOf<AnimationFrameCallback>()
|
|
28
|
+
|
|
29
|
+
fun resume() {
|
|
30
|
+
if (mPaused.getAndSet(false)) {
|
|
31
|
+
scheduleQueueExecution()
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
fun pause() {
|
|
36
|
+
synchronized(mPaused) {
|
|
37
|
+
if (!mPaused.getAndSet(true) && mCallbackPosted.getAndSet(false)) {
|
|
38
|
+
mReactChoreographer.removeFrameCallback(
|
|
39
|
+
ReactChoreographer.CallbackType.NATIVE_ANIMATED_MODULE, mChoreographerCallback
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fun requestAnimationFrame(animationFrameCallback: AnimationFrameCallback) {
|
|
46
|
+
synchronized(mFrameCallbacks) {
|
|
47
|
+
mFrameCallbacks.add(animationFrameCallback)
|
|
48
|
+
}
|
|
49
|
+
scheduleQueueExecution()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fun enableSlowAnimations(slowAnimationsEnabled: Boolean, animationsDragFactor: Int) {
|
|
53
|
+
mSlowAnimationsEnabled = slowAnimationsEnabled
|
|
54
|
+
mAnimationsDragFactor = animationsDragFactor
|
|
55
|
+
if (slowAnimationsEnabled) {
|
|
56
|
+
mFirstUptime = SystemClock.uptimeMillis()
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private fun scheduleQueueExecution() {
|
|
61
|
+
synchronized(mPaused) {
|
|
62
|
+
if (!mPaused.get() && !mCallbackPosted.getAndSet(true)) {
|
|
63
|
+
mReactChoreographer.postFrameCallback(
|
|
64
|
+
ReactChoreographer.CallbackType.NATIVE_ANIMATED_MODULE, mChoreographerCallback
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private fun executeQueue(frameTimeNanos: Long) {
|
|
71
|
+
val currentFrameTimeMs = calculateTimestamp(frameTimeNanos)
|
|
72
|
+
if (currentFrameTimeMs <= lastFrameTimeMs) {
|
|
73
|
+
// It is possible for ChoreographerCallback to be executed twice within the same frame
|
|
74
|
+
// due to frame drops. If this occurs, the additional callback execution should be ignored.
|
|
75
|
+
mCallbackPosted.set(false)
|
|
76
|
+
scheduleQueueExecution()
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
val frameCallbacks = pullCallbacks()
|
|
81
|
+
mCallbackPosted.set(false)
|
|
82
|
+
|
|
83
|
+
lastFrameTimeMs = currentFrameTimeMs
|
|
84
|
+
for (callback in frameCallbacks) {
|
|
85
|
+
callback.onAnimationFrame(currentFrameTimeMs)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private fun pullCallbacks(): List<AnimationFrameCallback> {
|
|
90
|
+
synchronized(mFrameCallbacks) {
|
|
91
|
+
val frameCallbacks = mFrameCallbacks.toList()
|
|
92
|
+
mFrameCallbacks.clear()
|
|
93
|
+
return frameCallbacks
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private fun calculateTimestamp(frameTimeNanos: Long): Double {
|
|
98
|
+
val nanosecondsInMilliseconds = 1000000.0
|
|
99
|
+
var currentFrameTimeMs = frameTimeNanos / nanosecondsInMilliseconds
|
|
100
|
+
if (mSlowAnimationsEnabled) {
|
|
101
|
+
currentFrameTimeMs =
|
|
102
|
+
mFirstUptime + (currentFrameTimeMs - mFirstUptime) / mAnimationsDragFactor
|
|
103
|
+
}
|
|
104
|
+
return currentFrameTimeMs
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
package com.swmansion.worklets
|
|
2
|
+
|
|
3
|
+
import com.facebook.jni.HybridData
|
|
4
|
+
import com.facebook.proguard.annotations.DoNotStrip
|
|
5
|
+
import com.facebook.react.bridge.Callback
|
|
6
|
+
import com.facebook.react.bridge.LifecycleEventListener
|
|
7
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
8
|
+
import com.facebook.react.bridge.ReactMethod
|
|
9
|
+
import com.facebook.react.bridge.ReadableNativeArray
|
|
10
|
+
import com.facebook.react.bridge.ReadableNativeMap
|
|
11
|
+
import com.facebook.react.bridge.queue.MessageQueueThread
|
|
12
|
+
import com.facebook.react.common.annotations.FrameworkAPI
|
|
13
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
14
|
+
import com.facebook.react.turbomodule.core.CallInvokerHolderImpl
|
|
15
|
+
import com.facebook.soloader.SoLoader
|
|
16
|
+
import com.swmansion.worklets.runloop.AnimationFrameCallback
|
|
17
|
+
import com.swmansion.worklets.runloop.AnimationFrameQueue
|
|
18
|
+
import java.util.concurrent.atomic.AtomicBoolean
|
|
19
|
+
|
|
20
|
+
@Suppress("KotlinJniMissingFunction")
|
|
21
|
+
@ReactModule(name = WorkletsModule.NAME)
|
|
22
|
+
class WorkletsModule(reactContext: ReactApplicationContext) :
|
|
23
|
+
NativeWorkletsModuleSpec(reactContext), LifecycleEventListener {
|
|
24
|
+
|
|
25
|
+
companion object {
|
|
26
|
+
const val NAME = "WorkletsModule"
|
|
27
|
+
|
|
28
|
+
init {
|
|
29
|
+
SoLoader.loadLibrary("worklets")
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@field:DoNotStrip
|
|
34
|
+
@Suppress("unused")
|
|
35
|
+
private var mHybridData: HybridData? = null
|
|
36
|
+
|
|
37
|
+
@Suppress("unused")
|
|
38
|
+
protected fun getHybridData(): HybridData? = mHybridData
|
|
39
|
+
|
|
40
|
+
init {
|
|
41
|
+
reactContext.assertOnJSQueueThread()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private val mMessageQueueThread = WorkletsMessageQueueThread()
|
|
45
|
+
private val mAndroidUIScheduler = AndroidUIScheduler(reactContext)
|
|
46
|
+
private val mAnimationFrameQueue = AnimationFrameQueue(reactContext)
|
|
47
|
+
private val mWorkletsNetworking = WorkletsNetworking()
|
|
48
|
+
private var mSlowAnimationsEnabled = false
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Invalidating concurrently could be fatal. It shouldn't happen in a normal flow, but it doesn't
|
|
52
|
+
* cost us much to add synchronization for extra safety.
|
|
53
|
+
*/
|
|
54
|
+
private val mInvalidated = AtomicBoolean(false)
|
|
55
|
+
|
|
56
|
+
@OptIn(FrameworkAPI::class)
|
|
57
|
+
private external fun initHybrid(
|
|
58
|
+
bundleModeEnabled: Boolean,
|
|
59
|
+
jsContext: Long,
|
|
60
|
+
messageQueueThread: MessageQueueThread,
|
|
61
|
+
jsCallInvokerHolder: CallInvokerHolderImpl,
|
|
62
|
+
androidUIScheduler: AndroidUIScheduler,
|
|
63
|
+
scriptBufferWrapper: ScriptBufferWrapper?
|
|
64
|
+
): HybridData
|
|
65
|
+
|
|
66
|
+
@OptIn(FrameworkAPI::class)
|
|
67
|
+
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
68
|
+
override fun installTurboModule(bundleModeEnabled: Boolean): Boolean {
|
|
69
|
+
val context = reactApplicationContext
|
|
70
|
+
|
|
71
|
+
context.assertOnJSQueueThread()
|
|
72
|
+
|
|
73
|
+
val jsContext = checkNotNull(context.javaScriptContextHolder).get()
|
|
74
|
+
val jsCallInvokerHolder = JSCallInvokerResolver.getJSCallInvokerHolder(context)
|
|
75
|
+
|
|
76
|
+
val sourceURL = context.sourceURL
|
|
77
|
+
|
|
78
|
+
val scriptBufferWrapper: ScriptBufferWrapper? = if (bundleModeEnabled) {
|
|
79
|
+
ScriptBufferWrapper(sourceURL!!, context.assets)
|
|
80
|
+
} else null
|
|
81
|
+
|
|
82
|
+
mHybridData =
|
|
83
|
+
initHybrid(
|
|
84
|
+
bundleModeEnabled,
|
|
85
|
+
jsContext,
|
|
86
|
+
mMessageQueueThread,
|
|
87
|
+
jsCallInvokerHolder,
|
|
88
|
+
mAndroidUIScheduler,
|
|
89
|
+
scriptBufferWrapper)
|
|
90
|
+
return true
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
fun abortRequest(runtimeId: Int, requestIdAsDouble: Double) {
|
|
94
|
+
mWorkletsNetworking.jsiAbortRequest(runtimeId, requestIdAsDouble)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
fun clearCookies(callback: Callback) {
|
|
98
|
+
mWorkletsNetworking.jsiClearCookies(callback)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
fun sendRequest(
|
|
102
|
+
runtimeWrapper: WorkletRuntimeWrapper,
|
|
103
|
+
method: String,
|
|
104
|
+
url: String,
|
|
105
|
+
requestIdAsDouble: Double,
|
|
106
|
+
headers: ReadableNativeArray,
|
|
107
|
+
data: ReadableNativeMap,
|
|
108
|
+
responseType: String,
|
|
109
|
+
useIncrementalUpdates: Boolean,
|
|
110
|
+
timeoutAsDouble: Double,
|
|
111
|
+
withCredentials: Boolean
|
|
112
|
+
) {
|
|
113
|
+
mWorkletsNetworking.jsiSendRequest(
|
|
114
|
+
runtimeWrapper,
|
|
115
|
+
method,
|
|
116
|
+
url,
|
|
117
|
+
requestIdAsDouble,
|
|
118
|
+
headers,
|
|
119
|
+
data,
|
|
120
|
+
responseType,
|
|
121
|
+
useIncrementalUpdates,
|
|
122
|
+
timeoutAsDouble,
|
|
123
|
+
withCredentials)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
fun requestAnimationFrame(animationFrameCallback: AnimationFrameCallback) {
|
|
127
|
+
mAnimationFrameQueue.requestAnimationFrame(animationFrameCallback)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** @noinspection unused */
|
|
131
|
+
@DoNotStrip
|
|
132
|
+
fun isOnJSQueueThread(): Boolean = reactApplicationContext.isOnJSQueueThread
|
|
133
|
+
|
|
134
|
+
fun toggleSlowAnimations() {
|
|
135
|
+
val animationsDragFactor = 10
|
|
136
|
+
mSlowAnimationsEnabled = !mSlowAnimationsEnabled
|
|
137
|
+
mAnimationFrameQueue.enableSlowAnimations(mSlowAnimationsEnabled, animationsDragFactor)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
override fun invalidate() {
|
|
141
|
+
if (mInvalidated.getAndSet(true)) {
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
if (mHybridData != null && mHybridData!!.isValid) {
|
|
145
|
+
// We have to destroy extra runtimes when invalidate is called. If we clean
|
|
146
|
+
// it up later instead there's a chance the runtime will retain references
|
|
147
|
+
// to invalidated memory and will crash on its destruction.
|
|
148
|
+
invalidateCpp()
|
|
149
|
+
}
|
|
150
|
+
mAndroidUIScheduler.deactivate()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private external fun invalidateCpp()
|
|
154
|
+
|
|
155
|
+
override fun onHostResume() {
|
|
156
|
+
mAnimationFrameQueue.resume()
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
override fun onHostPause() {
|
|
160
|
+
mAnimationFrameQueue.pause()
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
override fun onHostDestroy() {}
|
|
164
|
+
}
|