Search Docs
Loading...
Skip to content

UI Events

In this example, we will show you how to configure the callbacks of various editor events for the mobile editor.

Configuration#

For demonstration purposes, let’s create a data class that holds the state of the editor:

data class State(
val isLoading: Boolean = false,
val isCloseConfirmationDialogVisible: Boolean = false,
val error: Throwable? = null,
)

Also, let’s add an instance of it as a mutable state:

var state by remember { mutableStateOf(State()) }

All the callback configurations are part of the EditorConfiguration class. Note that all the callbacks are within EditorScope, meaning you can access editorContext in all of them.

  • onCreate - the callback that is invoked when the editor is created. This is the main initialization block of both the editor and engine. Normally, you should load or create a scene as well as prepare asset sources in this block. We recommend that you check the availability of the scene before creating/loading a new scene since a recreated scene may already exist if the callback is invoked after a process recreation. In this example, we create a scene with a single square page, update the state to show a loading in the overlay while creating the scene and also update one of the editor settings.
onCreate = {
state = state.copy(isLoading = true)
try {
val scene = editorContext.engine.scene.create()
val page = editorContext.engine.block.create(DesignBlockType.Page)
editorContext.engine.block.setWidth(block = page, value = 1080F)
editorContext.engine.block.setHeight(block = page, value = 1080F)
editorContext.engine.block.appendChild(parent = scene, child = page)
editorContext.engine.editor.setSettingEnum(keypath = "touch/pinchAction", value = "Scale")
} finally {
state = state.copy(isLoading = false)
}
}
  • onLoaded - the callback that is invoked when the editor is loaded and ready to be used. The callback is invoked right after onCreate when launching the editor for the first time or after process recreation. The callback is not invoked after configuration changes. It is best to register callbacks, collect flows returned by the engine and apply editor settings in this callback. Note that the “load” coroutine job will survive configuration changes and will be cancelled only if the editor is closed or the process is killed when in the background. In this example, we observe the editor history and the edit mode.
onLoaded = {
coroutineScope {
launch {
editorContext.engine.editor
.onHistoryUpdated()
.collect { Toast.makeText(editorContext.activity, "History is updated!", Toast.LENGTH_SHORT).show() }
}
launch {
editorContext.engine.editor
.onStateChanged()
.map { editorContext.engine.editor.getEditMode() }
.distinctUntilChanged()
.collect {
Toast.makeText(editorContext.activity, "Edit mode is updated to $it!", Toast.LENGTH_SHORT).show()
}
}
}
}
  • onExport - the callback that is invoked when the export button is clicked. You may want to call one of the export functions in this callback. Note that the “export” coroutine job will survive configuration changes and will be cancelled only if the editor is closed or the process is killed when in the background. In this example, we export the scene to a PDF file and write the content of it to a temporary file. We also update the state to show a loading in the overlay while the export is running. Finally, we update the state to show an error dialog in the overlay if the export fails.
onExport = {
val engine = editorContext.engine
state = state.copy(isLoading = true)
runCatching {
val buffer = engine.block.export(
block = requireNotNull(engine.scene.get()),
mimeType = MimeType.PDF,
onPreExport = {
scene.getPages().forEach { page ->
block.setScopeEnabled(page, key = "layer/visibility", enabled = true)
block.setVisible(page, visible = true)
}
},
)
writeToTempFile(buffer, MimeType.PDF)
}.onSuccess { file ->
state = state.copy(isLoading = false)
// Do something with the file
}.onFailure {
state = state.copy(isLoading = false, error = it)
}
}
private suspend fun writeToTempFile(
byteBuffer: ByteBuffer,
mimeType: MimeType = MimeType.PDF,
): File = withContext(Dispatchers.IO) {
val extension = mimeType.key.split("/").last()
File
.createTempFile(UUID.randomUUID().toString(), ".$extension")
.apply {
outputStream().channel.write(byteBuffer)
}
}
  • onUpload - the callback that is invoked after an asset is added to UploadAssetSourceType. When selecting an asset to upload, a default AssetDefinition object is constructed based on the selected asset and the callback is invoked. By default, the callback leaves the asset definition unmodified and returns the same object. However, you may want to upload the selected asset to your server before adding it to the scene. Note that the “upload” coroutine job will survive configuration changes and will be cancelled only if the editor is closed or the process is killed when in the background. In this example, we upload the AssetDefinition to our server and update the instance of the definition.
onUpload = onUpload@{ assetDefinition, _ ->
val meta = assetDefinition.meta ?: return@onUpload assetDefinition
val sourceUri = meta["uri"]?.toUri()
val uploadedUri = sourceUri // todo upload the asset here and return remote uri
val newMeta = meta +
listOf(
"uri" to uploadedUri.toString(),
"thumbUri" to uploadedUri.toString(),
)
assetDefinition.copy(meta = newMeta)
}
  • onClose - the callback that is invoked after EditorEvent.OnClose event is triggered or when the system back button is clicked and editor cannot handle the event internally. Note that the “close” coroutine job will survive configuration changes and will be cancelled only if the editor is closed or the process is killed when in the background. In this example, we update the state to show a confirmation dialog in the overlay if there are undo steps available and we close the editor otherwise.
onClose = {
if (editorContext.engine.editor.canUndo()) {
state = state.copy(isCloseConfirmationDialogVisible = true)
} else {
editorContext.eventHandler.send(EditorEvent.CloseEditor())
}
}
  • onError - the callback that is invoked after the editor captures an error. Note that the “error” coroutine job will survive configuration changes and will be cancelled only if the editor is closed or the process is killed when in the background. In this example, we update the state to show an error dialog in the overlay.
onError = { error ->
state = state.copy(error = error)
}

Full Code#

Here’s the full code for configuring events:

import android.widget.Toast
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.core.net.toUri
import androidx.navigation.NavHostController
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import ly.img.editor.Editor
import ly.img.editor.core.component.EditorComponent
import ly.img.editor.core.component.remember
import ly.img.editor.core.configuration.EditorConfiguration
import ly.img.editor.core.configuration.remember
import ly.img.editor.core.event.EditorEvent
import ly.img.engine.DesignBlockType
import ly.img.engine.MimeType
import java.io.File
import java.nio.ByteBuffer
import java.util.UUID
data class State(
val isLoading: Boolean = false,
val isCloseConfirmationDialogVisible: Boolean = false,
val error: Throwable? = null,
)
// Add this composable to your NavHost
@Composable
fun CallbacksEditorSolution(navController: NavHostController) {
var state by remember { mutableStateOf(State()) }
Editor(
license = null, // pass null or empty for evaluation mode with watermark
configuration = {
EditorConfiguration.remember {
onCreate = {
state = state.copy(isLoading = true)
try {
val scene = editorContext.engine.scene.create()
val page = editorContext.engine.block.create(DesignBlockType.Page)
editorContext.engine.block.setWidth(block = page, value = 1080F)
editorContext.engine.block.setHeight(block = page, value = 1080F)
editorContext.engine.block.appendChild(parent = scene, child = page)
editorContext.engine.editor.setSettingEnum(keypath = "touch/pinchAction", value = "Scale")
} finally {
state = state.copy(isLoading = false)
}
}
onLoaded = {
coroutineScope {
launch {
editorContext.engine.editor
.onHistoryUpdated()
.collect { Toast.makeText(editorContext.activity, "History is updated!", Toast.LENGTH_SHORT).show() }
}
launch {
editorContext.engine.editor
.onStateChanged()
.map { editorContext.engine.editor.getEditMode() }
.distinctUntilChanged()
.collect {
Toast.makeText(editorContext.activity, "Edit mode is updated to $it!", Toast.LENGTH_SHORT).show()
}
}
}
}
onExport = {
val engine = editorContext.engine
state = state.copy(isLoading = true)
runCatching {
val buffer = engine.block.export(
block = requireNotNull(engine.scene.get()),
mimeType = MimeType.PDF,
onPreExport = {
scene.getPages().forEach { page ->
block.setScopeEnabled(page, key = "layer/visibility", enabled = true)
block.setVisible(page, visible = true)
}
},
)
writeToTempFile(buffer, MimeType.PDF)
}.onSuccess { file ->
state = state.copy(isLoading = false)
// Do something with the file
}.onFailure {
state = state.copy(isLoading = false, error = it)
}
}
onUpload = onUpload@{ assetDefinition, _ ->
val meta = assetDefinition.meta ?: return@onUpload assetDefinition
val sourceUri = meta["uri"]?.toUri()
val uploadedUri = sourceUri // todo upload the asset here and return remote uri
val newMeta = meta +
listOf(
"uri" to uploadedUri.toString(),
"thumbUri" to uploadedUri.toString(),
)
assetDefinition.copy(meta = newMeta)
}
onClose = {
if (editorContext.engine.editor.canUndo()) {
state = state.copy(isCloseConfirmationDialogVisible = true)
} else {
editorContext.eventHandler.send(EditorEvent.CloseEditor())
}
}
onError = { error ->
state = state.copy(error = error)
}
overlay = {
EditorComponent.remember {
// Render loading, export state, error dialog and close confirmation dialog here.
}
}
}
},
) {
// You can set result here
navController.popBackStack()
}
}
private suspend fun writeToTempFile(
byteBuffer: ByteBuffer,
mimeType: MimeType = MimeType.PDF,
): File = withContext(Dispatchers.IO) {
val extension = mimeType.key.split("/").last()
File
.createTempFile(UUID.randomUUID().toString(), ".$extension")
.apply {
outputStream().channel.write(byteBuffer)
}
}

UI Events#

In addition to the callbacks, you can also capture various editor events that may be helpful to track user actions or to do additional actions. All the editor events are available in EditorEvent class. Also, you can fire your own events and capture them too.

Let’s declare 2 custom events for showing/hiding a loading:

object ShowLoading : EditorEvent
object HideLoading : EditorEvent

Similar to our previous guide above, let’s override the onCreate but this time we fire our custom events instead of updating the state:

onCreate = {
editorContext.eventHandler.send(ShowLoading)
try {
val scene = editorContext.engine.scene.create()
val page = editorContext.engine.block.create(DesignBlockType.Page)
editorContext.engine.block.setWidth(block = page, value = 1080F)
editorContext.engine.block.setHeight(block = page, value = 1080F)
editorContext.engine.block.appendChild(parent = scene, child = page)
} finally {
editorContext.eventHandler.send(HideLoading)
}
}

Finally, let’s observe both our custom and some default EditorEvents. The onEvent callback is located in the EditorConfiguration object:

onEvent = { event ->
when (event) {
is ShowLoading, is HideLoading, is EditorEvent.SetViewMode -> {
Toast.makeText(editorContext.activity, "Observing events!", Toast.LENGTH_SHORT).show()
}
}
}

Full Code#

Here’s the full code for UI events:

import android.widget.Toast
import androidx.compose.runtime.Composable
import androidx.navigation.NavHostController
import ly.img.editor.Editor
import ly.img.editor.core.configuration.EditorConfiguration
import ly.img.editor.core.configuration.remember
import ly.img.editor.core.event.EditorEvent
import ly.img.engine.DesignBlockType
object ShowLoading : EditorEvent
object HideLoading : EditorEvent
// Add this composable to your NavHost
@Composable
fun UiEventsEditorSolution(navController: NavHostController) {
Editor(
license = null, // pass null or empty for evaluation mode with watermark
configuration = {
EditorConfiguration.remember {
onCreate = {
editorContext.eventHandler.send(ShowLoading)
try {
val scene = editorContext.engine.scene.create()
val page = editorContext.engine.block.create(DesignBlockType.Page)
editorContext.engine.block.setWidth(block = page, value = 1080F)
editorContext.engine.block.setHeight(block = page, value = 1080F)
editorContext.engine.block.appendChild(parent = scene, child = page)
} finally {
editorContext.eventHandler.send(HideLoading)
}
}
onEvent = { event ->
when (event) {
is ShowLoading, is HideLoading, is EditorEvent.SetViewMode -> {
Toast.makeText(editorContext.activity, "Observing events!", Toast.LENGTH_SHORT).show()
}
}
}
}
},
) {
// You can set result here
navController.popBackStack()
}
}