Skip to main content

Animation around

Contents

The Mobile Maps SDK examples require that you first complete the initial project setup.

This example provides a reference of how to animate the camera around a location in a Jetpack Compose environment. It displays an animated view that spins around a specific location, similar to the Animation Around code example for Java and Kotlin.

import android.animation.ValueAnimator
import android.content.Context
import android.os.Bundle
import android.view.animation.LinearInterpolator
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import com.trimblemaps.account.LicensedFeature
import com.trimblemaps.account.TrimbleMapsAccountManager
import com.trimblemaps.account.models.TrimbleMapsAccount
import com.trimblemaps.mapsdk.TrimbleMaps
import com.trimblemaps.mapsdk.camera.CameraPosition
import com.trimblemaps.mapsdk.camera.CameraUpdateFactory
import com.trimblemaps.mapsdk.geometry.LatLng
import com.trimblemaps.mapsdk.maps.MapView
import com.trimblemaps.mapsdk.maps.OnMapReadyCallback
import com.trimblemaps.mapsdk.maps.Style
import com.trimblemaps.mapsdk.maps.TrimbleMapsMap
import kotlin.math.roundToLong

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            // IMPORTANT: Use your real API key
            AnimateAroundScreen(apiKey = "YOUR-API-KEY")
        }
    }
}

@Composable
fun AnimateAroundScreen(apiKey: String) {
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current

    // Gate rendering the MapView until account initialization completes
    var isInitialized by remember { mutableStateOf(false) }
    var trimbleMap by remember { mutableStateOf<TrimbleMapsMap?>(null) }
    var shouldAnimate by remember { mutableStateOf(false) }

    val numberOfSpins = 8f // How many times should it spin
    val spinDuration = 16 // How long, in seconds, should a spin take

    // Initialize the Trimble account/session once.
    LaunchedEffect(apiKey) {
        // Build account with your API key and the Maps SDK licensed feature
        val account = TrimbleMapsAccount.builder()
            .apiKey(apiKey)
            .addLicensedFeature(LicensedFeature.MAPS_SDK)
            .build()

        // Initialize and block until ready
        TrimbleMapsAccountManager.initialize(account)
        TrimbleMapsAccountManager.awaitInitialization()

        // Obtain the Trimble Maps instance BEFORE creating/using MapView
        TrimbleMaps.getInstance(context)

        isInitialized = true
    }

    // Remember the animator
    val animator = remember {
        ValueAnimator.ofFloat(0f, numberOfSpins * 360).apply {
            duration = (numberOfSpins * spinDuration * 1000).roundToLong()
            interpolator = LinearInterpolator()
            startDelay = 1000
        }
    }

    // Setup animator update listener when map is ready
    val updatedMap = rememberUpdatedState(trimbleMap)
    LaunchedEffect(trimbleMap, shouldAnimate) {
        if (updatedMap.value != null && shouldAnimate) {
            if (animator.isStarted) {
                animator.resume()
            } else {
                animator.removeAllUpdateListeners()
                animator.addUpdateListener { valueAnimator ->
                    // Get the bearing from the animator and apply it to the camera position
                    val bearing = valueAnimator.animatedValue as Float
                    updatedMap.value?.let { map ->
                        val currentTarget = map.cameraPosition?.target
                        if (currentTarget != null) {
                            map.moveCamera(
                                CameraUpdateFactory.newCameraPosition(
                                    CameraPosition.Builder()
                                        .target(currentTarget)
                                        .bearing(bearing.toDouble())
                                        .build()
                                )
                            )
                        }
                    }
                }
                animator.start()
            }
        } else if (!shouldAnimate) {
            animator.pause()
        }
    }

    // Cleanup animator on dispose
    DisposableEffect(Unit) {
        onDispose {
            animator.cancel()
            animator.removeAllUpdateListeners()
        }
    }

    if (isInitialized) {
        val mapView = rememberMapViewWithLifecycle(context, lifecycleOwner.lifecycle)

        Box(modifier = Modifier.fillMaxSize()) {
            AndroidView(
                modifier = Modifier.fillMaxSize(),
                factory = { mapView },
                update = { view ->
                    // Request a TrimbleMapsMap asynchronously
                    view.getMapAsync(OnMapReadyCallback { map ->
                        trimbleMap = map

                        // Set the style
                        map.setStyle(Style.TrimbleMobileStyle.MOBILE_DAY)

                        // Set initial camera position
                        val position = CameraPosition.Builder()
                            .target(LatLng(39.90073499962372, -75.16745401827387))
                            .zoom(16.0)
                            .build()
                        map.cameraPosition = position
                    })
                }
            )

            // Toggle animation button
            Button(
                onClick = { shouldAnimate = !shouldAnimate },
                modifier = Modifier
                    .align(Alignment.BottomCenter)
                    .padding(16.dp)
            ) {
                Text(if (shouldAnimate) "Stop Animation" else "Start Animation")
            }
        }
    }
}

/**
 * Handle the lifecycle events from the map view, onDestroy etc.
 */
@Composable
private fun rememberMapViewWithLifecycle(
    context: Context,
    lifecycle: Lifecycle
): MapView {
    // Create once per composition
    val mapView = remember {
        MapView(context)
    }

    // Hook the MapView into the lifecycle
    DisposableEffect(lifecycle, mapView) {
        val observer = LifecycleEventObserver { _, event ->
            when (event) {
                Lifecycle.Event.ON_CREATE -> { /* no-op (Trimble handled) */ }
                Lifecycle.Event.ON_START -> mapView.onStart()
                Lifecycle.Event.ON_RESUME -> mapView.onResume()
                Lifecycle.Event.ON_PAUSE -> mapView.onPause()
                Lifecycle.Event.ON_STOP -> mapView.onStop()
                Lifecycle.Event.ON_DESTROY -> mapView.onDestroy()
                else -> Unit
            }
        }
        lifecycle.addObserver(observer)

        onDispose {
            lifecycle.removeObserver(observer)
            try {
                mapView.onDestroy()
            } catch (_: Throwable) {
            }
        }
    }

    return mapView
}
Last updated November 26, 2025.
Contents