New Compose Multiplatform components arrived on Composables UICheck it out →

AndroidEmbeddedExternalSurface

Provides a dedicated drawing [Surface] embedded directly in the UI hierarchy. Unlike [AndroidExternalSurface], [AndroidEmbeddedExternalSurface] positions its surface as a regular element inside the composable hierarchy. This means that graphics composition is handled like any other UI widget, using the GPU. This can lead to increased power and memory bandwidth usage compared to [AndroidExternalSurface]. It is therefore recommended to use [AndroidExternalSurface] over [AndroidEmbeddedExternalSurface] whenever possible.

[AndroidEmbeddedExternalSurface] can however be useful when interactions with other widgets is necessary, for instance if the surface needs to be "sandwiched" between two other widgets, or if it must participate in visual effects driven by a Modifier.graphicsLayer{}.

The [Surface] provided can be used to present content that's external to Compose, such as a video stream (from a camera or a media player), OpenGL, Vulkan... The provided [Surface] can be rendered into using a thread different from the main thread.

The drawing surface is opaque by default, which can be controlled with the [isOpaque] parameter.

To start rendering, the caller must first acquire the [Surface] when it's created. This is achieved by providing the [onInit] lambda, which allows the caller to register an appropriate [AndroidExternalSurfaceScope.onSurface] callback. The [onInit] lambda can also be used to initialize/cache resources needed once a surface is available.

After acquiring a surface, the caller can start rendering into it. Rendering into a surface can be done from any thread.

It is recommended to register the [SurfaceScope.onChanged] and [SurfaceScope.onDestroyed] callbacks to properly handle the lifecycle of the surface and react to dimension changes. You must ensure that the rendering thread stops interacting with the surface when the [SurfaceScope.onDestroyed] callback is invoked.

If a [surfaceSize] is specified (set to non-[IntSize.Zero]), the surface will use the specified size instead of the layout size of this composable. The surface will be stretched at render time to fit the layout size. This can be used for instance to render at a lower resolution for performance reasons.

Last updated:

Installation

dependencies {
   implementation("androidx.compose.foundation:foundation:1.7.0-beta03")
}

Overloads

@Composable
fun AndroidEmbeddedExternalSurface(
    modifier: Modifier = Modifier,
    isOpaque: Boolean = true,
    surfaceSize: IntSize = IntSize.Zero,
    transform: Matrix? = null,
    onInit: AndroidExternalSurfaceScope.() -> Unit
)

Parameters

namedescription
modifierModifier to be applied to the [AndroidExternalSurface]
isOpaqueWhether the managed surface should be opaque or transparent. If transparent and [isMediaOverlay] is false, the surface will be positioned above the parent window.
surfaceSizeSets the surface size independently of the layout size of this [AndroidExternalSurface]. If set to [IntSize.Zero], the surface size will be equal to the [AndroidExternalSurface] layout size.
transformSets the transform to apply to the [Surface]. Some transforms might prevent the content from drawing all the pixels contained within this Composable's bounds. In such situations, make sure to set [isOpaque] to false.
onInitLambda invoked on first composition. This lambda can be used to declare a [AndroidExternalSurfaceScope.onSurface] callback that will be invoked when a surface is available.

Code Example

AndroidEmbeddedExternalSurfaceColors

@Composable
@Sampled
fun AndroidEmbeddedExternalSurfaceColors() {
    AndroidEmbeddedExternalSurface(
        modifier = Modifier.fillMaxWidth().height(400.dp)
    ) {
        // Resources can be initialized/cached here

        // A surface is available, we can start rendering
        onSurface { surface, width, height ->
            var w = width
            var h = height

            // Initial draw to avoid a black frame
            surface.lockCanvas(Rect(0, 0, w, h)).apply {
                drawColor(Color.Yellow.toArgb())
                surface.unlockCanvasAndPost(this)
            }

            // React to surface dimension changes
            surface.onChanged { newWidth, newHeight ->
                w = newWidth
                h = newHeight
            }

            // Cleanup if needed
            surface.onDestroyed {
            }

            // Render loop, automatically cancelled on surface destruction
            while (true) {
                withFrameNanos { time ->
                    surface.lockCanvas(Rect(0, 0, w, h)).apply {
                        val timeMs = time / 1_000_000L
                        val t = 0.5f + 0.5f * sin(timeMs / 1_000.0f)
                        drawColor(lerp(Color.Yellow, Color.Red, t).toArgb())
                        surface.unlockCanvasAndPost(this)
                    }
                }
            }
        }
    }
}