Jetpack Compose periodically update and draw in another thread as SurfaceView
Asked Answered
N

1

5

How can i implement counterpart of SurfaceView used with another thread to draw and update in a specific interval in Jetpack Compose?

And with coroutines i use it like this

abstract class CoroutineSurfaceView : SurfaceView, SurfaceHolder.Callback,
    DefaultLifecycleObserver {

    // Handle works in thread that exception is caught that are
    private val handler = CoroutineExceptionHandler { coroutineContext, throwable ->
    }

    internal lateinit var canvas: Canvas

    var framePerSecond = 60

    private var renderTime = 100L / framePerSecond

    private val coroutineScope = CoroutineScope(handler + SupervisorJob() + Dispatchers.Default)

    private lateinit var job: Job

    private lateinit var surfaceHolder: SurfaceHolder

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
        init(context)
    }

    constructor(context: Context) : super(context) {
        init(context)
    }

    open fun init(context: Context) {
        surfaceHolder = this.holder
        surfaceHolder.addCallback(this)
        setZOrderOnTop(true)
    }

    override fun surfaceCreated(holder: SurfaceHolder) {

    }

    override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {

    }

    override fun surfaceDestroyed(holder: SurfaceHolder) {

    }

    private fun startCoroutineRendering() {

        job = coroutineScope.launch {

            while (isActive) {

                if (!surfaceHolder.surface.isValid) {
                    continue
                }

                canvas = surfaceHolder.lockCanvas()
                update()
                render(canvas)
                holder.unlockCanvasAndPost(canvas)

                delay(renderTime)
            }
        }
    }

    internal abstract fun update()

    internal abstract fun render(canvas: Canvas)

    override fun onResume(owner: LifecycleOwner) {
        super.onResume(owner)
        startCoroutineRendering()
    }

    override fun onPause(owner: LifecycleOwner) {
        super.onPause(owner)
        coroutineScope.launch(Dispatchers.Main.immediate) {
            job.cancelAndJoin()
        }
    }
}
Nationalize answered 30/7, 2021 at 12:57 Comment(0)
A
7
@Composable
fun EachFrameUpdatingCanvas(modifier: Modifier, onDraw: DrawScope.(Long) -> Unit) {
    var frameTime by remember { mutableStateOf(0L) }

    val lifecycleOwner = LocalLifecycleOwner.current
    LaunchedEffect(Unit) {
        lifecycleOwner.whenStarted {
            while (true) {
                // this will be called for each frame
                // by updating `remember` value we initiating EachFrameUpdatingCanvas redraw
                frameTime = withFrameMillis { it }
            }
        }
    }
    Canvas(modifier = modifier) {
        // you had to use frameTime somewhere in EachFrameUpdatingCanvas
        // otherwise it won't be redrawn. But you don't have to pass it to `onDraw` if you don't want
        onDraw(frameTime)
    }
}

Use it like this:

EachFrameUpdatingCanvas(Modifier.fillMaxSize()) { frameTime ->
    drawCircle(
        Color.Black,
        radius = size.minDimension / 2.0f * (frameTime % 100) / 100f,
    )
}

Drawing in compose is done on the background, but if you have some heavy calculations during drawing(inside DrawScope), you probably have to wrap your CoroutineSurfaceView with AndroidView

Amalbergas answered 31/7, 2021 at 11:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.