Skip to content

Commit

Permalink
Replace GLSurfaceview with GLRenderer and AndroidExternalSurface
Browse files Browse the repository at this point in the history
  • Loading branch information
alexvanyo committed Sep 25, 2024
1 parent dd20270 commit 8f008ea
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 92 deletions.
3 changes: 3 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ androidxCore = "1.15.0-alpha02"
androidxCoreSplashscreen = "1.2.0-alpha02"
#androidxDataStore
androidxDataStore = "1.1.1"
#androidxGraphics
androidxGraphics = "1.0.1"
#androidxLifecycle
androidxLifecycle = "2.9.0-alpha03"
#androidxLintGradle
Expand Down Expand Up @@ -145,6 +147,7 @@ androidx-core = { group = "androidx.core", name = "core-ktx", version.ref = "and
androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "androidxCoreSplashscreen" }
androidx-dataStore = { group = "androidx.datastore", name = "datastore", version.ref = "androidxDataStore" }
androidx-dataStore-core-okio = { group = "androidx.datastore", name = "datastore-core-okio", version.ref = "androidxDataStore" }
androidx-graphics-core = { group = "androidx.graphics", name = "graphics-core", version.ref = "androidxGraphics" }
androidx-lifecycle-process = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "androidxLifecycle" }
androidx-lifecycle-runtime = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "androidxLifecycle" }
androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidxLifecycle" }
Expand Down
1 change: 1 addition & 0 deletions ui-cells/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ kotlin {
implementation(libs.androidx.compose.uiTooling)
implementation(libs.androidx.compose.uiUtil)
implementation(libs.androidx.core)
implementation(libs.androidx.graphics.core)
implementation(libs.androidx.lifecycle.runtime)
implementation(libs.androidx.poolingContainer)
implementation(libs.androidx.window)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,43 +17,37 @@
package com.alexvanyo.composelife.ui.cells

import android.app.ActivityManager
import android.opengl.EGLConfig
import android.opengl.EGLSurface
import android.opengl.GLES20
import android.opengl.GLSurfaceView
import android.opengl.Matrix
import android.view.Surface
import androidx.compose.foundation.AndroidExternalSurface
import androidx.compose.foundation.AndroidExternalSurfaceZOrder
import androidx.compose.runtime.Composable
import androidx.compose.runtime.RememberObserver
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.getSystemService
import androidx.core.view.isInvisible
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.repeatOnLifecycle
import androidx.graphics.opengl.GLRenderer
import androidx.graphics.opengl.egl.EGLManager
import androidx.graphics.opengl.egl.EGLSpec
import com.alexvanyo.composelife.model.CellWindow
import com.alexvanyo.composelife.model.GameOfLifeState
import com.alexvanyo.composelife.openglrenderer.GameOfLifeShape
import com.alexvanyo.composelife.openglrenderer.GameOfLifeShapeParameters
import com.alexvanyo.composelife.preferences.CurrentShape
import com.alexvanyo.composelife.ui.mobile.ComposeLifeTheme
import com.alexvanyo.composelife.ui.util.LocalGhostElement
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import java.nio.IntBuffer
import java.util.concurrent.Executor
import javax.microedition.khronos.egl.EGLConfig
import javax.microedition.khronos.opengles.GL10

@Composable
fun openGLSupported(): Boolean {
Expand Down Expand Up @@ -92,32 +86,41 @@ fun OpenGLNonInteractableCells(
buffer
}

val parameters = when (shape) {
is CurrentShape.RoundRectangle -> {
GameOfLifeShapeParameters.RoundRectangle(
cells = cellsBuffer,
aliveColor = aliveColor,
deadColor = deadColor,
cellWindowSize = cellWindow.size,
scaledCellPixelSize = scaledCellPixelSize,
pixelOffsetFromCenter = pixelOffsetFromCenter,
sizeFraction = shape.sizeFraction,
cornerFraction = shape.cornerFraction,
)
}
}

val lifecycleOwner = LocalLifecycleOwner.current
val coroutineScope = rememberCoroutineScope()
val parameters by rememberUpdatedState(
when (shape) {
is CurrentShape.RoundRectangle -> {
GameOfLifeShapeParameters.RoundRectangle(
cells = cellsBuffer,
aliveColor = aliveColor,
deadColor = deadColor,
cellWindowSize = cellWindow.size,
scaledCellPixelSize = scaledCellPixelSize,
pixelOffsetFromCenter = pixelOffsetFromCenter,
sizeFraction = shape.sizeFraction,
cornerFraction = shape.cornerFraction,
)
}
},
)

val isGhostElement by rememberUpdatedState(LocalGhostElement.current)
val glRenderer = rememberGLRenderer()

AndroidView(
factory = { context ->
object : GLSurfaceView(context) {
val parametersState = MutableStateFlow(parameters)
AndroidExternalSurface(
modifier = modifier,
zOrder = if (inOverlay) {
AndroidExternalSurfaceZOrder.MediaOverlay
} else {
AndroidExternalSurfaceZOrder.Behind
},
) {
onSurface { surface, width, height ->
val renderTarget = glRenderer.attach(
surface,
width,
height,
object : GLRenderer.RenderCallback {
private lateinit var gameOfLifeShape: GameOfLifeShape

val renderer = object : Renderer {
private val mvpMatrix = FloatArray(16)

init {
Expand All @@ -128,64 +131,57 @@ fun OpenGLNonInteractableCells(
Matrix.multiplyMM(mvpMatrix, 0, projectionMatrix, 0, viewMatrix, 0)
}

lateinit var gameOfLifeShape: GameOfLifeShape

override fun onSurfaceCreated(unused: GL10?, config: EGLConfig?) {
GLES20.glClearColor(0f, 0f, 0f, 0f)
gameOfLifeShape = GameOfLifeShape()
gameOfLifeShape.setScreenShapeParameters(parametersState.value)
}

override fun onSurfaceChanged(unused: GL10?, width: Int, height: Int) {
GLES20.glViewport(0, 0, width, height)
gameOfLifeShape.setSize(width, height)
override fun onSurfaceCreated(
spec: EGLSpec,
config: EGLConfig,
surface: Surface,
width: Int,
height: Int,
): EGLSurface? {
return super.onSurfaceCreated(spec, config, surface, width, height).also {
GLES20.glClearColor(0f, 0f, 0f, 0f)
GLES20.glViewport(0, 0, width, height)
gameOfLifeShape = GameOfLifeShape().apply {
setSize(width, height)
}
}
}

override fun onDrawFrame(unused: GL10?) {
override fun onDrawFrame(eglManager: EGLManager) {
gameOfLifeShape.setScreenShapeParameters(parameters)
gameOfLifeShape.draw(mvpMatrix)
}
},
)

fun setParameters(parameters: GameOfLifeShapeParameters) {
if (::gameOfLifeShape.isInitialized) {
gameOfLifeShape.setScreenShapeParameters(parameters)
}
}
}
}.apply {
setEGLContextClientVersion(2)
setRenderer(renderer)
renderMode = GLSurfaceView.RENDERMODE_WHEN_DIRTY

val openGLExecutor = Executor(::queueEvent)
val openGLDispatcher = openGLExecutor.asCoroutineDispatcher()

coroutineScope.launch {
parametersState
.onEach(renderer::setParameters)
.onEach { requestRender() }
.flowOn(openGLDispatcher)
.collect()
}
surface.onChanged { w, h ->
renderTarget.resize(w, h)
}

coroutineScope.launch {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
onResume()
try {
awaitCancellation()
} finally {
onPause()
}
}
}
surface.onDestroyed {
glRenderer.detach(renderTarget, true)
}
},
update = {
it.parametersState.value = parameters
it.setZOrderMediaOverlay(inOverlay)
// The GLSurfaceView may not handling animating in and out well with externally applied alphas.
// If we are in a ghost element, skip showing it.
it.isInvisible = isGhostElement
},
modifier = modifier,
)

snapshotFlow { parameters }
.onEach {
renderTarget.requestRender()
}
.collect()
}
}
}

@Composable
fun rememberGLRenderer(): GLRenderer =
remember {
object : RememberObserver {
val glRenderer = GLRenderer()
override fun onAbandoned() = onForgotten()
override fun onForgotten() {
glRenderer.stop(true)
}
override fun onRemembered() {
glRenderer.start()
}
}
}.glRenderer

0 comments on commit 8f008ea

Please sign in to comment.