-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathRiveReactNativeView.kt
More file actions
400 lines (344 loc) · 12.7 KB
/
Copy pathRiveReactNativeView.kt
File metadata and controls
400 lines (344 loc) · 12.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
package com.rive
import android.annotation.SuppressLint
import android.graphics.SurfaceTexture
import android.util.Log
import android.view.Choreographer
import android.view.MotionEvent
import android.view.TextureView
import android.widget.FrameLayout
import app.rive.Artboard
import app.rive.Fit
import app.rive.RiveFile
import app.rive.ViewModelInstance
import app.rive.ViewModelSource
import app.rive.core.ArtboardHandle
import app.rive.core.CommandQueue
import app.rive.core.RiveSurface
import app.rive.core.StateMachineHandle
import com.facebook.react.uimanager.ThemedReactContext
import com.margelo.nitro.rive.RiveErrorLogger
import com.margelo.nitro.rive.RiveLog
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.time.Duration
import kotlin.time.Duration.Companion.nanoseconds
sealed class BindData {
data object None : BindData()
data object Auto : BindData()
data class Instance(val instance: ViewModelInstance) : BindData()
data class ByName(val name: String) : BindData()
}
data class ViewConfiguration(
val artboardName: String?,
val stateMachineName: String?,
val autoPlay: Boolean,
val riveFile: RiveFile,
val riveWorker: CommandQueue,
val alignment: app.rive.Alignment,
val fit: app.rive.Fit,
val layoutScaleFactor: Float?,
val bindData: BindData
)
@SuppressLint("ViewConstructor")
class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) {
companion object {
private const val TAG = "RiveReactNativeView"
}
var onError: ((String) -> Unit)? = null
private val errorListener: (String) -> Unit = { msg ->
onError?.invoke(msg)
}
private val viewReadyDeferred = CompletableDeferred<Boolean>()
private var boundInstance: ViewModelInstance? = null
private var riveWorker: CommandQueue? = null
private var activeFit: Fit = Fit.Contain()
private var riveFile: RiveFile? = null
private var artboard: Artboard? = null
private var artboardHandle: ArtboardHandle? = null
private var stateMachineHandle: StateMachineHandle? = null
private var riveSurface: RiveSurface? = null
private var surfaceTexture: SurfaceTexture? = null
private var surfaceWidth = 0
private var surfaceHeight = 0
private var renderLoopRunning = false
private var disposed = false
private var lastFrameTimeNs = 0L
private var frameCount = 0L
@Volatile
private var paused = false
private val textureView = TextureView(context).apply {
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
surfaceTextureListener = object : TextureView.SurfaceTextureListener {
override fun onSurfaceTextureAvailable(st: SurfaceTexture, w: Int, h: Int) {
Log.d(TAG, "onSurfaceTextureAvailable: ${w}x$h worker=${this@RiveReactNativeView.riveWorker != null}")
this@RiveReactNativeView.surfaceTexture = st
this@RiveReactNativeView.surfaceWidth = w
this@RiveReactNativeView.surfaceHeight = h
this@RiveReactNativeView.riveWorker?.let { worker ->
this@RiveReactNativeView.riveSurface = worker.createRiveSurface(st)
Log.d(TAG, "onSurfaceTextureAvailable: surface created")
resizeArtboardIfLayout()
}
}
override fun onSurfaceTextureDestroyed(st: SurfaceTexture): Boolean {
this@RiveReactNativeView.riveSurface = null
return false
}
override fun onSurfaceTextureSizeChanged(st: SurfaceTexture, w: Int, h: Int) {
this@RiveReactNativeView.surfaceWidth = w
this@RiveReactNativeView.surfaceHeight = h
resizeArtboardIfLayout()
}
override fun onSurfaceTextureUpdated(st: SurfaceTexture) {}
}
}
init {
addView(textureView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
}
private val renderCallback = object : Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
if (!renderLoopRunning || disposed) return
val deltaTime = if (lastFrameTimeNs == 0L) {
Duration.ZERO
} else {
(frameTimeNanos - lastFrameTimeNs).nanoseconds
}
lastFrameTimeNs = frameTimeNanos
val worker = riveWorker
val art = artboardHandle
val sm = stateMachineHandle
val rs = riveSurface
if (worker != null && art != null && sm != null && rs != null) {
try {
if (!paused) {
worker.advanceStateMachine(sm, deltaTime)
}
worker.draw(art, sm, rs, activeFit)
frameCount++
val isFirstFrame = frameCount == 1L
if (isFirstFrame) {
viewReadyDeferred.complete(true)
}
} catch (e: Exception) {
Log.e(TAG, "Render loop error", e)
}
}
if (!disposed) {
Choreographer.getInstance().postFrameCallback(this)
}
}
}
private fun startRenderLoop() {
if (renderLoopRunning) return
renderLoopRunning = true
lastFrameTimeNs = 0L
Choreographer.getInstance().postFrameCallback(renderCallback)
}
private fun stopRenderLoop() {
renderLoopRunning = false
Choreographer.getInstance().removeFrameCallback(renderCallback)
}
suspend fun awaitViewReady(): Boolean {
return viewReadyDeferred.await()
}
fun configure(config: ViewConfiguration, dataBindingChanged: Boolean, reload: Boolean = false, initialUpdate: Boolean = false) {
riveWorker = config.riveWorker
activeFit = config.fit
Log.d(
TAG,
"configure: reload=$reload initialUpdate=$initialUpdate fit=$activeFit surfaceTexture=${surfaceTexture != null} surfaceW=$surfaceWidth surfaceH=$surfaceHeight"
)
if (reload) {
RiveErrorLogger.resetReportedErrors()
RiveErrorLogger.addListener(errorListener)
artboard?.close()
val newArtboard = if (config.artboardName != null) {
Artboard.fromFile(config.riveFile, config.artboardName)
} else {
Artboard.fromFile(config.riveFile)
}
artboard = newArtboard
artboardHandle = newArtboard.artboardHandle
riveFile = config.riveFile
stateMachineHandle = if (config.stateMachineName != null) {
config.riveWorker.createStateMachineByName(newArtboard.artboardHandle, config.stateMachineName)
} else {
config.riveWorker.createDefaultStateMachine(newArtboard.artboardHandle)
}
if (surfaceTexture != null && riveSurface == null) {
riveSurface = config.riveWorker.createRiveSurface(surfaceTexture!!)
}
Log.d(TAG, "configure: artboard=${artboardHandle != null} sm=${stateMachineHandle != null} surface=${riveSurface != null}")
paused = !config.autoPlay
startRenderLoop()
}
resizeArtboardIfLayout()
if (dataBindingChanged || initialUpdate) {
applyDataBinding(config.bindData, config.riveFile)
}
}
private fun resizeArtboardIfLayout() {
val fit = activeFit
if (fit is Fit.Layout) {
val rs = riveSurface ?: return
val art = artboard ?: return
art.resizeArtboard(rs, fit.scaleFactor)
}
}
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean = true
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
handlePointerEvent(event)
return true
}
private fun handlePointerEvent(event: MotionEvent) {
val worker = riveWorker ?: run {
Log.w(TAG, "touch: no worker")
return
}
val smHandle = stateMachineHandle ?: run {
Log.w(TAG, "touch: no smHandle")
return
}
val w = surfaceWidth.toFloat()
val h = surfaceHeight.toFloat()
if (w <= 0 || h <= 0) {
Log.w(TAG, "touch: invalid surface ${w}x$h")
return
}
val fit = activeFit
try {
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
worker.pointerDown(smHandle, fit, w, h, event.getPointerId(event.actionIndex), event.x, event.y)
}
MotionEvent.ACTION_MOVE -> {
worker.pointerMove(smHandle, fit, w, h, event.getPointerId(0), event.x, event.y)
}
MotionEvent.ACTION_UP -> {
val id = event.getPointerId(event.actionIndex)
worker.pointerUp(smHandle, fit, w, h, id, event.x, event.y)
worker.pointerExit(smHandle, fit, w, h, id, event.x, event.y)
}
MotionEvent.ACTION_CANCEL -> {
val id = event.getPointerId(event.actionIndex)
worker.pointerUp(smHandle, fit, w, h, id, -1f, -1f)
worker.pointerExit(smHandle, fit, w, h, id, -1f, -1f)
}
}
} catch (e: Exception) {
Log.e(TAG, "Pointer event failed", e)
}
}
fun bindViewModelInstance(vmi: ViewModelInstance) {
boundInstance = vmi
}
fun getViewModelInstance(): ViewModelInstance? {
return boundInstance
}
private fun applyDataBinding(bindData: BindData, riveFile: RiveFile) {
when (bindData) {
is BindData.None -> {
boundInstance = null
}
is BindData.Auto -> {
CoroutineScope(Dispatchers.Default).launch {
try {
val vmNames = riveFile.getViewModelNames()
if (vmNames.isEmpty()) return@launch
withContext(Dispatchers.Main) {
val art = artboard ?: return@withContext
val source = ViewModelSource.DefaultForArtboard(art).defaultInstance()
val instance = ViewModelInstance.fromFile(riveFile, source)
// A handle of 1L is the C++ null sentinel — the artboard has ViewModels but
// none is set as the default, so binding would fire "instance 0x1 not found".
if (instance.instanceHandle.handle == 1L) {
Log.d(TAG, "Auto-binding skipped: no default ViewModel for artboard")
return@withContext
}
boundInstance = instance
bindInstanceToStateMachine(instance)
}
} catch (e: Exception) {
Log.d(TAG, "Auto-binding skipped: ${e.message}")
}
}
}
is BindData.Instance -> {
boundInstance = bindData.instance
bindInstanceToStateMachine(bindData.instance)
}
is BindData.ByName -> {
try {
val art = artboard ?: return
val source = ViewModelSource.DefaultForArtboard(art).namedInstance(bindData.name)
val instance = ViewModelInstance.fromFile(riveFile, source)
boundInstance = instance
bindInstanceToStateMachine(instance)
} catch (e: Exception) {
Log.e(TAG, "Failed to create named instance", e)
}
}
}
}
private fun bindInstanceToStateMachine(instance: ViewModelInstance) {
val worker = riveWorker
val smHandle = stateMachineHandle
if (worker != null && smHandle != null) {
worker.bindViewModelInstance(smHandle, instance.instanceHandle)
} else {
Log.w(TAG, "Cannot bind VMI: worker or state machine handle not available")
}
}
fun play() {
paused = false
}
fun pause() {
paused = true
}
// Deprecated: the experimental Rive runtime has no reset primitive.
fun reset() {
RiveLog.e(TAG, "reset() is deprecated and not supported on the experimental backend")
}
fun playIfNeeded() {
paused = false
}
fun setNumberInputValue(name: String, value: Double, path: String?) {
throw UnsupportedOperationException("SMI inputs not supported in experimental API")
}
fun getNumberInputValue(name: String, path: String?): Double {
throw UnsupportedOperationException("SMI inputs not supported in experimental API")
}
fun setBooleanInputValue(name: String, value: Boolean, path: String?) {
throw UnsupportedOperationException("SMI inputs not supported in experimental API")
}
fun getBooleanInputValue(name: String, path: String?): Boolean {
throw UnsupportedOperationException("SMI inputs not supported in experimental API")
}
fun triggerInput(name: String, path: String?) {
throw UnsupportedOperationException("SMI inputs not supported in experimental API")
}
fun setTextRunValue(name: String, value: String, path: String?) {
throw UnsupportedOperationException("Text runs not supported in experimental API")
}
fun getTextRunValue(name: String, path: String?): String {
throw UnsupportedOperationException("Text runs not supported in experimental API")
}
fun dispose() {
disposed = true
RiveErrorLogger.removeListener(errorListener)
stopRenderLoop()
// Null handles to prevent any further draw calls.
// Don't close artboard/stateMachine/surface here — the command queue
// may still have a pending draw command that references them.
// Let them be cleaned up by GC instead.
boundInstance = null
artboard = null
artboardHandle = null
stateMachineHandle = null
riveSurface = null
}
}