Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tabletop AR: Add ArSurfaceView #590

Merged
merged 10 commits into from
Sep 27, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ package com.arcgismaps.toolkit.ar

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Face
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import com.arcgismaps.geometry.SpatialReference
import com.arcgismaps.mapping.ArcGISScene
import com.arcgismaps.mapping.TimeExtent
Expand All @@ -50,21 +50,22 @@ import com.arcgismaps.mapping.view.SceneViewInteractionOptions
import com.arcgismaps.mapping.view.SelectionProperties
import com.arcgismaps.mapping.view.SingleTapConfirmedEvent
import com.arcgismaps.mapping.view.SpaceEffect
import com.arcgismaps.mapping.view.TransformationMatrixCameraController
import com.arcgismaps.mapping.view.TwoPointerTapEvent
import com.arcgismaps.mapping.view.UpEvent
import com.arcgismaps.mapping.view.ViewLabelProperties
import com.arcgismaps.toolkit.ar.internal.ArCameraFeed
import com.arcgismaps.toolkit.ar.internal.ArSessionWrapper
import com.arcgismaps.toolkit.geoviewcompose.SceneView
import com.arcgismaps.toolkit.geoviewcompose.SceneViewDefaults
import java.time.Instant

/**
* A scene view that provides an augmented reality table top experience.
* Displays a [SceneView] in a tabletop AR environment.
*
* @since 200.6.0
*/
@Composable
public fun TableTopSceneView(
fun TableTopSceneView(
arcGISScene: ArcGISScene,
modifier: Modifier = Modifier,
onViewpointChangedForCenterAndScale: ((Viewpoint) -> Unit)? = null,
Expand Down Expand Up @@ -101,18 +102,23 @@ public fun TableTopSceneView(
onDrawStatusChanged: ((DrawStatus) -> Unit)? = null,
content: (@Composable TableTopSceneViewScope.() -> Unit)? = null
) {
Box(modifier = Modifier.fillMaxSize()) {
Icon(
imageVector = Icons.Default.Face,
contentDescription = null,
modifier = Modifier.fillMaxSize(),
tint = Color.Red
)
val lifecycleOwner = LocalLifecycleOwner.current

val cameraController = remember { TransformationMatrixCameraController() }
val context = LocalContext.current
val arSessionWrapper = remember { ArSessionWrapper(context.applicationContext) }
DisposableEffect(Unit) {
lifecycleOwner.lifecycle.addObserver(arSessionWrapper)
onDispose {
lifecycleOwner.lifecycle.removeObserver(arSessionWrapper)
arSessionWrapper.onDestroy(lifecycleOwner)
}
}

Box(modifier = modifier) {
ArCameraFeed(arSessionWrapper = arSessionWrapper, onFrame = {}, onTap = {})
SceneView(
arcGISScene = arcGISScene,
modifier = modifier,
modifier = Modifier.fillMaxSize(),
onViewpointChangedForCenterAndScale = onViewpointChangedForCenterAndScale,
onViewpointChangedForBoundingGeometry = onViewpointChangedForBoundingGeometry,
graphicsOverlays = graphicsOverlays,
Expand All @@ -123,7 +129,6 @@ public fun TableTopSceneView(
isAttributionBarVisible = isAttributionBarVisible,
onAttributionTextChanged = onAttributionTextChanged,
onAttributionBarLayoutChanged = onAttributionBarLayoutChanged,
cameraController = cameraController,
analysisOverlays = analysisOverlays,
imageOverlays = imageOverlays,
atmosphereEffect = AtmosphereEffect.None,
Expand All @@ -135,7 +140,6 @@ public fun TableTopSceneView(
ambientLightColor = ambientLightColor,
onNavigationChanged = onNavigationChanged,
onSpatialReferenceChanged = {
// scene view is ready?
onSpatialReferenceChanged?.invoke(it)
},
onLayerViewStateChanged = onLayerViewStateChanged,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ public final class TableTopSceneViewProxy internal constructor(internal val scen

public constructor() : this(SceneViewProxy())

init {
sceneViewProxy.setManualRenderingEnabled(true)
}

/**
* True if continuous panning across the international date line is enabled in the TableTopSceneView, false otherwise.
* A null value represents that it is currently undetermined.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
*
* Copyright 2024 Esri
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.arcgismaps.toolkit.ar.internal

import android.content.Context
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.google.ar.core.Session

/**
* Provides an ARCore [Session] and manages the session's lifecycle.
*
* @since 200.6.0
*/
internal class ArSessionWrapper(applicationContext: Context) : DefaultLifecycleObserver {
val session: Session = Session(applicationContext)

override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
session.close()
}

override fun onPause(owner: LifecycleOwner) {
super.onPause(owner)
session.pause()
}

override fun onResume(owner: LifecycleOwner) {
super.onResume(owner)
session.resume()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
*
* Copyright 2024 Esri
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package com.arcgismaps.toolkit.ar.internal

import android.content.Context
import android.opengl.GLSurfaceView
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInteropFilter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.arcgismaps.toolkit.ar.internal.render.CameraFeedRenderer
import com.arcgismaps.toolkit.ar.internal.render.SurfaceDrawHandler
import com.google.ar.core.Frame
import com.google.ar.core.HitResult
import com.google.ar.core.Session

/**
* Renders the AR camera feed using a [GLSurfaceView].
*
* @param arSessionWrapper an [ArSessionWrapper] that provides an ARCore [Session].
* @param onFrame a callback that is invoked every frame.
* @param onTap a callback that is invoked when the user taps the screen and a hit is detected.
* @since 200.6.0
*/
@OptIn(ExperimentalComposeUiApi::class)
@Composable
internal fun ArCameraFeed(
arSessionWrapper: ArSessionWrapper,
onFrame: (Frame) -> Unit,
onTap: (hit: HitResult?) -> Unit
) {
val lifecycleOwner = LocalLifecycleOwner.current
val context = LocalContext.current
val surfaceViewWrapper = remember { GLSurfaceViewWrapper(context) }

val cameraFeedRenderer = remember {
CameraFeedRenderer(
context,
arSessionWrapper.session,
context.assets,
onFrame,
onTap
).apply {
this.surfaceDrawHandler = SurfaceDrawHandler(surfaceViewWrapper.glSurfaceView, this)
}
}

DisposableEffect(Unit) {
lifecycleOwner.lifecycle.addObserver(surfaceViewWrapper)
lifecycleOwner.lifecycle.addObserver(cameraFeedRenderer)

onDispose {
lifecycleOwner.lifecycle.removeObserver(surfaceViewWrapper)
lifecycleOwner.lifecycle.removeObserver(cameraFeedRenderer)
cameraFeedRenderer.onDestroy(lifecycleOwner)
}
}

AndroidView(factory = { surfaceViewWrapper.glSurfaceView }, modifier = Modifier
.fillMaxSize()
.pointerInteropFilter {
cameraFeedRenderer.onClick(it)
true
})
}

/**
* Provides a [GLSurfaceView] and handles its lifecycle.
*
* @since 200.6.0
*/
internal class GLSurfaceViewWrapper(context: Context) : DefaultLifecycleObserver {
val glSurfaceView = GLSurfaceView(context)

override fun onPause(owner: LifecycleOwner) {
super.onPause(owner)
glSurfaceView.onPause()
}

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