Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: API updates for interactivity #43

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ enum class DotLottiePlayerState {
COMPLETED,
INITIAL,
LOADED,
ERROR,
DRAW,
ERROR
}

class DotLottieController {
Expand All @@ -40,6 +39,9 @@ class DotLottieController {
private val _height = MutableStateFlow(0u)
val height: StateFlow<UInt> = _height.asStateFlow()

private var stateMachineGestureListeners: MutableList<String> = mutableListOf()
private set

var stateMachineListeners: MutableList<StateMachineEventListener> = mutableListOf()
private set
var eventListeners = mutableListOf<DotLottieEventListener>()
Expand Down Expand Up @@ -101,6 +103,9 @@ class DotLottieController {
val activeAnimationId: String
get() = dlplayer?.activeAnimationId() ?: ""

var stateMachineIsActive: Boolean = false
get() = field

fun play() {
dlplayer?.play()
}
Expand Down Expand Up @@ -160,14 +165,25 @@ class DotLottieController {
dlplayer?.subscribe(observer!!)
}

fun startStateMachine(): Boolean {
val result = dlplayer?.startStateMachine() ?: false
fun stateMachineStart(): Boolean {
val result = dlplayer?.stateMachineStart() ?: false
if (result) {
stateMachineIsActive = true

if (dlplayer != null) {
stateMachineGestureListeners =
dlplayer!!.stateMachineFrameworkSetup().map { it.lowercase() }.toSet().toMutableList()
}

if (this.isPlaying) {
this.play()
}

dlplayer?.stateMachineSubscribe(object : StateMachineObserver {
override fun onCustomEvent(message: String) {
stateMachineListeners.forEach { it.onCustomEvent(message) }
}

override fun onStateEntered(enteringState: String) {
stateMachineListeners.forEach { it.onStateEntered(enteringState) }
}
Expand All @@ -184,54 +200,73 @@ class DotLottieController {
return result
}

fun stopStateMachine(): Boolean {
return dlplayer?.stopStateMachine() ?: false
fun stateMachineStop(): Boolean {
stateMachineIsActive = false
return dlplayer?.stateMachineStop() ?: false
}

fun loadStateMachine(stateMachineId: String): Boolean {
return dlplayer?.loadStateMachine(stateMachineId) ?: false
fun stateMachineLoad(stateMachineId: String): Boolean {
return dlplayer?.stateMachineLoad(stateMachineId) ?: false
}

fun postEvent(event: Event): Int {
val result = dlplayer?.postEvent(event) ?: 0
when (result) {
1 -> {
eventListeners.forEach { it.onError(Throwable("Error posting event: $event")) }
}

2 -> {
this.play()
}
fun stateMachineLoadData(data: String): Boolean {
return dlplayer?.stateMachineLoadData(data) ?: false
}

3 -> {
this.pause()
}
/**
* Internal function to notify the state machine of gesture input.
*/
fun stateMachinePostEvent(event: Event, force: Boolean = false): Int {
var ret: Int = 1
// Extract the event name before the parenthesis
val eventName = event.toString().split("(").firstOrNull()?.lowercase() ?: event.toString()

4 -> {
_currentState.value = DotLottiePlayerState.DRAW
}
if (force) {
ret = dlplayer?.stateMachinePostEvent(event) ?: 0
} else if (stateMachineGestureListeners.contains(eventName)) {
ret = dlplayer?.stateMachinePostEvent(event) ?: 0
}

return result
return ret
}

fun stateMachineFire(event: String) {
dlplayer?.stateMachineFireEvent(event)
}

fun stateMachineSetNumericTrigger(key: String, value: Float): Boolean {
return dlplayer?.stateMachineSetNumericTrigger(key, value) ?: false
}

fun stateMachineSetStringTrigger(key: String, value: String): Boolean {
return dlplayer?.stateMachineSetStringTrigger(key, value) ?: false
}

fun stateMachineSetBooleanTrigger(key: String, value: Boolean): Boolean {
return dlplayer?.stateMachineSetBooleanTrigger(key, value) ?: false
}

fun stateMachineGetNumericTrigger(key: String): Float? {
return dlplayer?.stateMachineGetNumericTrigger(key)
}

fun setStateMachineNumericContext(key: String, value: Float): Boolean {
return dlplayer?.setStateMachineNumericContext(key, value) ?: false
fun stateMachineGetStringTrigger(key: String): String? {
return dlplayer?.stateMachineGetStringTrigger(key)
}

fun setStateMachineStringContext(key: String, value: String): Boolean {
return dlplayer?.setStateMachineStringContext(key, value) ?: false
fun stateMachineGetBooleanTrigger(key: String): Boolean? {
return dlplayer?.stateMachineGetBooleanTrigger(key)
}

fun setStateMachineBooleanContext(key: String, value: Boolean): Boolean {
return dlplayer?.setStateMachineBooleanContext(key, value) ?: false
fun stateMachineCurrentState(): String? {
return dlplayer?.stateMachineCurrentState()
}

fun addStateMachineEventListener(listener: StateMachineEventListener) {
fun stateMachineAddEventListener(listener: StateMachineEventListener) {
stateMachineListeners.add(listener)
}

fun removeStateMachineEventListener(listener: StateMachineEventListener) {
fun stateMachineRemoveEventListener(listener: StateMachineEventListener) {
stateMachineListeners.remove(listener)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package com.lottiefiles.dotlottie.core.compose.ui

import android.graphics.Bitmap
import android.util.Log
import android.view.Choreographer
import androidx.compose.foundation.Image
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.runtime.Composable
Expand All @@ -22,11 +23,12 @@ import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.toSize
import androidx.compose.ui.input.pointer.pointerInput
import com.dotlottie.dlplayer.DotLottiePlayer
import com.dotlottie.dlplayer.Event
import com.dotlottie.dlplayer.Layout
import com.dotlottie.dlplayer.createDefaultLayout
import com.lottiefiles.dotlottie.core.compose.runtime.DotLottieController
import com.lottiefiles.dotlottie.core.compose.runtime.DotLottiePlayerState
import com.lottiefiles.dotlottie.core.util.DotLottieContent
import com.dotlottie.dlplayer.Config as DLConfig
import com.lottiefiles.dotlottie.core.util.DotLottieEventListener
Expand All @@ -36,7 +38,6 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import java.nio.ByteBuffer


@Composable
fun DotLottieAnimation(
modifier: Modifier = Modifier,
Expand Down Expand Up @@ -81,30 +82,50 @@ fun DotLottieAnimation(
var bufferBytes by remember { mutableStateOf<ByteBuffer?>(null) }
var imageBitmap by remember { mutableStateOf<ImageBitmap?>(null) }
val choreographer = remember { Choreographer.getInstance() }
val currentSate by rController.currentState.collectAsState()
val currentState by rController.currentState.collectAsState()
val _width by rController.height.collectAsState()
val _height by rController.width.collectAsState()
var layoutSize by remember { mutableStateOf<Size?>(null) }
var currentFrame by remember { mutableFloatStateOf(-1.0f) }

val frameCallback = remember {
object : Choreographer.FrameCallback {
var isActive = true

override fun doFrame(frameTimeNanos: Long) {
if (bufferBytes == null || bitmap == null || !isActive) return

val nextFrame = dlPlayer.requestFrame()
dlPlayer.setFrame(nextFrame)
dlPlayer.render()

bufferBytes?.let { bytes ->
bitmap?.let { bmp ->
bytes.rewind()
bmp.copyPixelsFromBuffer(bytes)
imageBitmap = bmp.asImageBitmap()

if (rController.stateMachineIsActive) {
if (nextFrame != currentFrame || (currentFrame == 0.0f)) {
currentFrame = nextFrame
dlPlayer.setFrame(nextFrame)
dlPlayer.render()

bufferBytes?.let { bytes ->
bitmap?.let { bmp ->
bytes.rewind()
bmp.copyPixelsFromBuffer(bytes)
imageBitmap = bmp.asImageBitmap()
}
}
}
} else {
currentFrame = nextFrame
dlPlayer.setFrame(nextFrame)
dlPlayer.render()

bufferBytes?.let { bytes ->
bitmap?.let { bmp ->
bytes.rewind()
bmp.copyPixelsFromBuffer(bytes)
imageBitmap = bmp.asImageBitmap()
}
}
}

if (dlPlayer.isPlaying()) {
if (dlPlayer.isPlaying() || rController.stateMachineIsActive ) {
choreographer.postFrameCallback(this)
}
}
Expand Down Expand Up @@ -144,7 +165,7 @@ fun DotLottieAnimation(
val startTime = System.currentTimeMillis()
val timeout = 500L // 500 milliseconds
while (isActive && System.currentTimeMillis() - startTime < timeout) {
if (System.currentTimeMillis() - startTime > 100L && !dlPlayer.isPlaying()) {
if (System.currentTimeMillis() - startTime > 100L && !rController.stateMachineIsActive && !dlPlayer.isPlaying()) {
choreographer.removeFrameCallback(frameCallback)
break
}
Expand All @@ -157,8 +178,8 @@ fun DotLottieAnimation(
}
}

LaunchedEffect(dlPlayer.isPlaying(), currentSate) {
if (dlPlayer.isPlaying() || currentSate == DotLottiePlayerState.DRAW) {
LaunchedEffect(dlPlayer.isPlaying(), currentState) {
if (dlPlayer.isPlaying() || rController.stateMachineIsActive) {
choreographer.postFrameCallback(frameCallback)
} else {
choreographer.removeFrameCallback(frameCallback)
Expand Down Expand Up @@ -232,6 +253,41 @@ fun DotLottieAnimation(
.onGloballyPositioned { layoutCoordinates ->
layoutSize = layoutCoordinates.size.toSize()
}
.pointerInput(Unit) {
awaitEachGesture {
// First touch (Down)
val down = awaitFirstDown()
// down.consume() // Consume the down event

val scaledX = down.position.x
val scaledY = down.position.y

rController.stateMachinePostEvent(Event.PointerDown(scaledX, scaledY))

// Handle move and up events
do {
val event = awaitPointerEvent()
val position = event.changes.first()
// position.consume() // Consume each event

// Handle move
if (!position.pressed) {
// Touch up detected
val upScaledX = position.position.x
val upScaledY = position.position.y

rController.stateMachinePostEvent(Event.PointerUp(upScaledX, upScaledY))
break
} else {
// Move detected
val moveX = position.position.x
val moveY = position.position.y

rController.stateMachinePostEvent(Event.PointerMove(moveX, moveY))
}
} while (position.pressed)
}
}
) {
imageBitmap?.let {
Image(
Expand Down
Loading