In this example, we will show you how to configure the callbacks of various editor events for the mobile editor. The example is based on the Design Editor
, however, it is exactly the same for all the other solutions.
Note that the bodies of all callbacks except onUpload
are copied from the Design Editor
default implementations.
Configuration
All the callback configurations are part of the EngineConfiguration
class. Note that all the callbacks receive EditorEventHandler
parameter that can be used to send UI events.
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. Callback does not have a default implementation, as default scenes are solution-specific, however,EditorDefaults.onCreate
contains the default logic. By default, it loads a scene and adds all default and demo asset sources. Note that the “create” 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.
onCreate = { // Note that lambda is copied from EditorDefaults.onCreate coroutineScope { // In case of process recovery, engine automatically recovers the scene that is why we need to check if (editorContext.engine.scene.get() == null) { editorContext.engine.scene.load(EngineConfiguration.defaultDesignSceneUri) } launch { val baseUri = Uri.parse("https://cdn.img.ly/assets/v3") editorContext.engine.addDefaultAssetSources(baseUri = baseUri) val defaultTypeface = TypefaceProvider().provideTypeface(editorContext.engine, "Roboto") requireNotNull(defaultTypeface) editorContext.engine.asset.addSource(TextAssetSource(editorContext.engine, defaultTypeface)) } launch { editorContext.engine.addDemoAssetSources( sceneMode = editorContext.engine.scene.getMode(), withUploadAssetSources = true, baseUri = Uri.parse("https://cdn.img.ly/assets/demo/v2"), ) } coroutineContext[Job]?.invokeOnCompletion { editorContext.eventHandler.send(HideLoading) } }},
onExport
- the callback that is invoked when the export button is tapped. You may want to call one of the export functions in this callback. The default implementations callBlockApi.export
orBlockApi.exportVideo
, write the content into a temporary file and open a system dialog for sharing the exported file. 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
onExport = { EditorDefaults.run { editorContext.eventHandler.send(ShowLoading) val blob = editorContext.engine.block.export( block = requireNotNull(editorContext.engine.scene.get()), mimeType = MimeType.PDF, ) { scene.getPages().forEach { block.setScopeEnabled(it, key = "layer/visibility", enabled = true) block.setVisible(it, visible = true) } } val tempFile = writeToTempFile(blob) editorContext.eventHandler.send(HideLoading) editorContext.eventHandler.send(ShareFileEvent(tempFile, MimeType.PDF.key)) }},
onUpload
- the callback that is invoked after an asset is added toUploadAssetSourceType
. When selecting an asset to upload, a defaultAssetDefinition
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. This example demonstrates how you can access the URI of the new asset, use it to upload the file to your server, and then replace the URI with the URI of your server. 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.
onUpload = { assetDefinition, _ -> val meta = assetDefinition.meta ?: return@remember assetDefinition val sourceUri = Uri.parse(meta["uri"]) 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 a tap on the navigation icon of the toolbar or on the system back button. The callback receives a boolean parameter that indicates whether editor has unsaved changes. Default implementation sendsShowCloseConfirmationDialogEvent
event in case that parameter istrue
and closes the editor if it isfalse
. 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.
onClose = { hasUnsavedChanges -> if (hasUnsavedChanges) { editorContext.eventHandler.send(ShowCloseConfirmationDialogEvent) } else { editorContext.eventHandler.send(EditorEvent.CloseEditor()) }},
onError
- the callback that is invoked after the editor captures an error. Default implementation sendsShowErrorDialogEvent
event which displays a popup dialog with action button that closes the editor. 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.
onError = { error -> editorContext.eventHandler.send(ShowErrorDialogEvent(error))},
Full Code
Here’s the full code for configuring events:
import android.net.Uriimport androidx.compose.runtime.Composableimport androidx.navigation.NavHostControllerimport kotlinx.coroutines.Jobimport kotlinx.coroutines.coroutineScopeimport kotlinx.coroutines.launchimport ly.img.editor.DesignEditorimport ly.img.editor.EditorDefaultsimport ly.img.editor.EngineConfigurationimport ly.img.editor.HideLoadingimport ly.img.editor.ShareFileEventimport ly.img.editor.ShowCloseConfirmationDialogEventimport ly.img.editor.ShowErrorDialogEventimport ly.img.editor.ShowLoadingimport ly.img.editor.core.event.EditorEventimport ly.img.editor.core.library.data.TextAssetSourceimport ly.img.editor.core.library.data.TypefaceProviderimport ly.img.engine.MimeTypeimport ly.img.engine.addDefaultAssetSourcesimport ly.img.engine.addDemoAssetSources
// Add this composable to your NavHost@Composablefun CallbacksEditorSolution(navController: NavHostController) { val engineConfiguration = EngineConfiguration.remember( license = "<your license here>", onCreate = { // Note that lambda is copied from EditorDefaults.onCreate coroutineScope { // In case of process recovery, engine automatically recovers the scene that is why we need to check if (editorContext.engine.scene.get() == null) { editorContext.engine.scene.load(EngineConfiguration.defaultDesignSceneUri) } launch { val baseUri = Uri.parse("https://cdn.img.ly/assets/v3") editorContext.engine.addDefaultAssetSources(baseUri = baseUri) val defaultTypeface = TypefaceProvider().provideTypeface(editorContext.engine, "Roboto") requireNotNull(defaultTypeface) editorContext.engine.asset.addSource(TextAssetSource(editorContext.engine, defaultTypeface)) } launch { editorContext.engine.addDemoAssetSources( sceneMode = editorContext.engine.scene.getMode(), withUploadAssetSources = true, baseUri = Uri.parse("https://cdn.img.ly/assets/demo/v2"), ) } coroutineContext[Job]?.invokeOnCompletion { editorContext.eventHandler.send(HideLoading) } } }, onExport = { EditorDefaults.run { editorContext.eventHandler.send(ShowLoading) val blob = editorContext.engine.block.export( block = requireNotNull(editorContext.engine.scene.get()), mimeType = MimeType.PDF, ) { scene.getPages().forEach { block.setScopeEnabled(it, key = "layer/visibility", enabled = true) block.setVisible(it, visible = true) } } val tempFile = writeToTempFile(blob) editorContext.eventHandler.send(HideLoading) editorContext.eventHandler.send(ShareFileEvent(tempFile, MimeType.PDF.key)) } }, onUpload = { assetDefinition, _ -> val meta = assetDefinition.meta ?: return@remember assetDefinition val sourceUri = Uri.parse(meta["uri"]) 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 = { hasUnsavedChanges -> if (hasUnsavedChanges) { editorContext.eventHandler.send(ShowCloseConfirmationDialogEvent) } else { editorContext.eventHandler.send(EditorEvent.CloseEditor()) } }, onError = { error -> editorContext.eventHandler.send(ShowErrorDialogEvent(error)) }, ) DesignEditor(engineConfiguration = engineConfiguration) { // You can set result here navController.popBackStack() }}
Updating the UI
When working with the callbacks, you may want to make UI updates before, during, or after the callback execution. This is when UI events come to help. All the callbacks receive an extra parameter EditorEventHandler, which can be used to send editor events. By default, there are existing ui events, which can be found in Events.kt file (i.e. ShowLoading, HideLoading etc).
onCreate = { EditorDefaults.onCreate( engine = editorContext.engine, sceneUri = EngineConfiguration.defaultDesignSceneUri, eventHandler = editorContext.eventHandler, ) editorContext.eventHandler.send(OnCreateCustomEvent) },
You may want to declare your own custom event. Simply inherit from class EditorEvent.
data object OnCreateCustomEvent : EditorEvent
After declaring the event, you can send the UI event using send function. Note that EditorEventHandler has another function called sendCloseEditorEvent, which can be used to forcefully close the mobile editor.
editorContext.eventHandler.send(OnCreateCustomEvent)
Once the event is sent, it can be captured in EditorConfiguration.onEvent. The lambda contains three parameters: activity, state and of course, the captured event. In this example, the editor state is of default type EditorUiState (initially provided in initialState), however, you can have your own state class that wraps the EditorUiState or does not contain EditorUiState at all. The only requirement is that it should be Parcelable. The lambda should return the updated state, which, if changed, will trigger overlay composable to be recomposed and the overlaying ui components will be updated.
onEvent = { state, event -> when (event) { OnCreateCustomEvent -> { Toast.makeText(editorContext.activity, "Editor is created!", Toast.LENGTH_SHORT).show() state } ShowLoading -> { state.copy(showLoading = true) } else -> { // handle other default events EditorDefaults.onEvent(editorContext.activity, state, event) } } },
To handle your brand new custom event, simply check the type of the event and handle it per your needs.
OnCreateCustomEvent -> { Toast.makeText(editorContext.activity, "Editor is created!", Toast.LENGTH_SHORT).show() state }
Besides, you can override the behavior of existing events too. Simply extend your when block and override the behavior.
ShowLoading -> { state.copy(showLoading = true) }
If you want to leave the behavior of remaining default events unchanged, simply return the result of EditorDefaults.onEvent in the else block.
else -> { // handle other default events EditorDefaults.onEvent(editorContext.activity, state, event) }
Full Code
Here’s the full code for making UI updates before, during, or after the callback execution
import android.widget.Toastimport androidx.compose.runtime.Composableimport androidx.navigation.NavHostControllerimport ly.img.editor.DesignEditorimport ly.img.editor.EditorConfigurationimport ly.img.editor.EditorDefaultsimport ly.img.editor.EditorUiStateimport ly.img.editor.EngineConfigurationimport ly.img.editor.ShowLoadingimport ly.img.editor.core.event.EditorEvent
data object OnCreateCustomEvent : EditorEvent
// Add this composable to your NavHost@Composablefun UiEventsEditorSolution(navController: NavHostController) { val engineConfiguration = EngineConfiguration.remember( license = "<your license here>", onCreate = { EditorDefaults.onCreate( engine = editorContext.engine, sceneUri = EngineConfiguration.defaultDesignSceneUri, eventHandler = editorContext.eventHandler, ) editorContext.eventHandler.send(OnCreateCustomEvent) }, ) val editorConfiguration = EditorConfiguration.remember( initialState = EditorUiState(), onEvent = { state, event -> when (event) { OnCreateCustomEvent -> { Toast.makeText(editorContext.activity, "Editor is created!", Toast.LENGTH_SHORT).show() state } ShowLoading -> { state.copy(showLoading = true) } else -> { // handle other default events EditorDefaults.onEvent(editorContext.activity, state, event) } } }, ) DesignEditor( engineConfiguration = engineConfiguration, editorConfiguration = editorConfiguration, ) { // You can set result here navController.popBackStack() }}