Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Maps3DSamples/ApiDemos/common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,11 @@ dependencies {
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.lifecycle.runtime.ktx)

implementation(libs.androidx.ui)
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.foundation:foundation-layout")
implementation(libs.androidx.material3)
implementation(libs.androidx.material.icons.extended)

api(libs.play.services.base) // "com.google.android.gms:play-services-base:18.10.0"
api(libs.play.services.maps3d) // "com.google.android.gms:play-services-maps3d:0.2.2"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* Copyright 2026 Google LLC
*
* 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.example.maps3d.common

import android.os.Handler
import android.os.Looper
import android.util.Log
import com.google.android.gms.maps3d.GoogleMap3D
import com.google.android.gms.maps3d.model.Camera
import com.google.android.gms.maps3d.model.CameraRestriction
import com.google.android.gms.maps3d.model.Map3DMode

/**
* Robust map state resetting utility for Google Maps 3D.
*
* The Maps 3D SDK reuses its underlying native rendering engine across activity transitions
* within the same application process. As a result, properties such as [Map3DMode] (e.g. SATELLITE,
* ROADMAP, HYBRID), camera bounds/restrictions, and click listeners can persist across samples.
*
* This utility applies an immediate reset and a delayed stabilization reset (after the native
* viewport layout pass completes) to guarantee every sample starts in its expected clean state.
*/
object MapResetHelper {

private const val TAG = "MapResetHelper"

/**
* Standard delay in milliseconds to allow the native 3D engine viewport to settle
* before reapplying mode, camera position, and restrictions.
*/
const val STABILIZATION_DELAY_MS: Long = 400L

/**
* Resets the [GoogleMap3D] instance to the specified baseline state.
*
* @param map The [GoogleMap3D] instance to reset.
* @param expectedMode The target [Map3DMode] (defaults to [Map3DMode.HYBRID]).
* @param initialCamera The target initial [Camera] position, or null if unchanged.
* @param cameraRestriction The target [CameraRestriction], or null to clear all restrictions.
*/
@JvmStatic
@JvmOverloads
fun resetMapState(
map: GoogleMap3D?,
expectedMode: Int = Map3DMode.HYBRID,
initialCamera: Camera? = null,
cameraRestriction: CameraRestriction? = null,
) {
if (map == null) return
try {
// 1. Force explicit Map Rendering Mode (fixes Satellite/Roadmap bleed)
map.setMapMode(expectedMode)

// 2. Reset / apply camera restrictions (prevents bounds clamping from bleeding)
map.setCameraRestriction(cameraRestriction)

// 3. Apply target camera orientation and position
if (initialCamera != null) {
map.setCamera(initialCamera.toValidCamera())
}
} catch (e: Exception) {
Log.w(TAG, "Error applying map state reset: ${e.message}")
}
}

/**
* Schedules a delayed stabilization reset on the main thread Looper.
*
* @param mapProvider A lambda returning the current [GoogleMap3D] instance.
* @param isAlive A predicate checking if the host Activity/View is still active and not destroyed.
* @param expectedMode The target [Map3DMode] (defaults to [Map3DMode.HYBRID]).
* @param initialCamera The target initial [Camera] position.
* @param cameraRestriction The target [CameraRestriction], or null.
* @param delayMillis Delay in milliseconds (defaults to [STABILIZATION_DELAY_MS]).
* @param onStabilized Optional callback invoked after stabilization is complete.
*/
@JvmStatic
@JvmOverloads
fun scheduleStabilizationReset(
mapProvider: () -> GoogleMap3D?,
isAlive: () -> Boolean,
expectedMode: Int = Map3DMode.HYBRID,
initialCamera: Camera? = null,
cameraRestriction: CameraRestriction? = null,
delayMillis: Long = STABILIZATION_DELAY_MS,
onStabilized: ((GoogleMap3D) -> Unit)? = null,
) {
Handler(Looper.getMainLooper()).postDelayed({
if (isAlive()) {
val map = mapProvider()
if (map != null) {
resetMapState(map, expectedMode, initialCamera, cameraRestriction)
onStabilized?.invoke(map)
}
}
}, delayMillis)
}

/**
* Teardown cleanup to clear listeners and restrictions when exiting a sample.
*/
@JvmStatic
fun teardownMap(map: GoogleMap3D?) {
if (map == null) return
try {
map.setCameraChangedListener(null)
map.setOnMapSteadyListener(null)
map.setCameraRestriction(null)
// Reset mode to standard HYBRID baseline for next sample
map.setMapMode(Map3DMode.HYBRID)
} catch (e: Exception) {
Log.w(TAG, "Error during map teardown: ${e.message}")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* Copyright 2026 Google LLC
*
* 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.example.maps3d.common.showcase

/**
* Enumerates the supported development frameworks for Google Maps 3D on Android.
*
* Each framework represents a distinct pedagogical pillar with full feature parity.
*
* @property id Unique programmatic identifier.
* @property displayName Human-readable name for UI titles and headers.
* @property badge Short badge label displayed on sample cards.
* @property description Brief summary of the architecture and target audience.
* @property iconEmoji Distinctive emoji/glyph icon representing the framework.
* @property accentColorHex Hex color value for theme accents.
*/
enum class FrameworkType(
val id: String,
val displayName: String,
val badge: String,
val description: String,
val iconEmoji: String,
val accentColorHex: Long,
) {
COMPOSE(
id = "compose",
displayName = "Jetpack Compose",
badge = "Compose",
description = "Modern declarative 3D UI with GoogleMap3D composable, state flows & coroutines.",
iconEmoji = "βš›οΈ",
accentColorHex = 0xFF4285F4,
),
KOTLIN_VIEWS(
id = "kotlin",
displayName = "Kotlin Views",
badge = "Kotlin",
description = "Traditional Android XML Views with ViewModels, Kotlin DSLs & lifecycle scopes.",
iconEmoji = "πŸ’œ",
accentColorHex = 0xFF7F52FF,
),
JAVA_VIEWS(
id = "java",
displayName = "Java Views",
badge = "Java",
description = "Enterprise Android XML Views with standard Java builders and event listeners.",
iconEmoji = "β˜•",
accentColorHex = 0xFFE76F51,
);

companion object {
fun fromId(id: String?): FrameworkType =
entries.find { it.id.equals(id, ignoreCase = true) } ?: COMPOSE
}
}

/**
* Pedagogical difficulty tiers for organizing samples progressively.
*
* @property displayName User-facing tier header title.
* @property badge Emoji icon indicating the tier.
* @property order Sort order from beginner to advanced.
*/
enum class SampleTier(
val displayName: String,
val badge: String,
val order: Int,
) {
BEGINNER("Beginner", "🟒", 1),
INTERMEDIATE("Intermediate", "🟑", 2),
ADVANCED("Advanced", "πŸ”΄", 3);
}

/**
* Metadata definition for a sample across frameworks.
*
* @property id Canonical feature identifier (e.g. "path_following").
* @property title User-facing title.
* @property subtitle Short summary of what is demonstrated.
* @property tier Pedagogical difficulty classification.
* @property tags Keyword tags for search filtering.
* @property iconEmoji Distinctive visual emoji for the sample.
* @property composeActivity Full class name of Compose Activity implementation.
* @property kotlinActivity Full class name of Kotlin Views Activity implementation.
* @property javaActivity Full class name of Java Views Activity implementation.
*/
data class ShowcaseSample(
val id: String,
val title: String,
val subtitle: String,
val tier: SampleTier,
val tags: List<String>,
val iconEmoji: String,
val composeActivity: String? = null,
val kotlinActivity: String? = null,
val javaActivity: String? = null,
) {
fun getActivityClassName(framework: FrameworkType): String? = when (framework) {
FrameworkType.COMPOSE -> composeActivity
FrameworkType.KOTLIN_VIEWS -> kotlinActivity
FrameworkType.JAVA_VIEWS -> javaActivity
}

fun isAvailable(framework: FrameworkType): Boolean =
getActivityClassName(framework) != null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright 2026 Google LLC
*
* 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.example.maps3d.common.showcase

import android.content.Context
import android.content.SharedPreferences

/**
* Manages user framework preferences and persistent navigation states for the Maps 3D Showcase.
*/
class ShowcasePreferences(context: Context) {

private val prefs: SharedPreferences =
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)

var preferredFramework: FrameworkType?
get() {
val id = prefs.getString(KEY_PREFERRED_FRAMEWORK, null) ?: return null
return FrameworkType.fromId(id)
}
set(value) {
prefs.edit().apply {
if (value == null) {
remove(KEY_PREFERRED_FRAMEWORK)
} else {
putString(KEY_PREFERRED_FRAMEWORK, value.id)
}
apply()
}
}

var rememberChoice: Boolean
get() = prefs.getBoolean(KEY_REMEMBER_CHOICE, true)
set(value) {
prefs.edit().putBoolean(KEY_REMEMBER_CHOICE, value).apply()
}

fun clear() {
prefs.edit().clear().apply()
}

companion object {
private const val PREFS_NAME = "maps3d_showcase_preferences"
private const val KEY_PREFERRED_FRAMEWORK = "key_preferred_framework"
private const val KEY_REMEMBER_CHOICE = "key_remember_choice"

@Volatile
private var instance: ShowcasePreferences? = null

fun getInstance(context: Context): ShowcasePreferences =
instance ?: synchronized(this) {
instance ?: ShowcasePreferences(context.applicationContext).also { instance = it }
}
}
}
Loading
Loading