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 theoverlaywhile 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 afteronCreatewhen 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 theoverlaywhile the export is running. Finally, we update the state to show an error dialog in theoverlayif 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 toUploadAssetSourceType. When selecting an asset to upload, a defaultAssetDefinitionobject 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 theAssetDefinitionto 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 afterEditorEvent.OnCloseevent 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 theoverlayif 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 theoverlay.
onError = { error -> state = state.copy(error = error)}Full Code#
Here’s the full code for configuring events:
import android.widget.Toastimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.setValueimport androidx.core.net.toUriimport androidx.navigation.NavHostControllerimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.coroutineScopeimport kotlinx.coroutines.flow.distinctUntilChangedimport kotlinx.coroutines.flow.mapimport kotlinx.coroutines.launchimport kotlinx.coroutines.withContextimport ly.img.editor.Editorimport ly.img.editor.core.component.EditorComponentimport ly.img.editor.core.component.rememberimport ly.img.editor.core.configuration.EditorConfigurationimport ly.img.editor.core.configuration.rememberimport ly.img.editor.core.event.EditorEventimport ly.img.engine.DesignBlockTypeimport ly.img.engine.MimeTypeimport java.io.Fileimport java.nio.ByteBufferimport java.util.UUID
data class State( val isLoading: Boolean = false, val isCloseConfirmationDialogVisible: Boolean = false, val error: Throwable? = null,)
// Add this composable to your NavHost@Composablefun 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 : EditorEventSimilar 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.Toastimport androidx.compose.runtime.Composableimport androidx.navigation.NavHostControllerimport ly.img.editor.Editorimport ly.img.editor.core.configuration.EditorConfigurationimport ly.img.editor.core.configuration.rememberimport ly.img.editor.core.event.EditorEventimport ly.img.engine.DesignBlockType
object ShowLoading : EditorEvent
object HideLoading : EditorEvent
// Add this composable to your NavHost@Composablefun 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() }}