diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 50428fda..b25fb7d2 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -16,6 +16,7 @@ kotlinxCoroutines = "1.10.2"
leakcanaryAndroid = "2.14"
mapsecrets = "2.0.1"
mapsktx = "5.2.0"
+material3 = "1.3.2"
org-jacoco-core = "0.8.13"
screenshot = "0.0.1-alpha11"
constraintlayout = "2.2.1"
@@ -29,6 +30,7 @@ androidx-compose-activity = { module = "androidx.activity:activity-compose", ver
androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" }
androidx-compose-foundation = { module = "androidx.compose.foundation:foundation" }
androidx-compose-material = { module = "androidx.compose.material:material" }
+androidx-compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" }
androidx-compose-ui = { module = "androidx.compose.ui:ui" }
androidx-compose-ui-preview-tooling = { module = "androidx.compose.ui:ui-tooling-preview" }
androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
diff --git a/maps-app/build.gradle.kts b/maps-app/build.gradle.kts
index 3ab4a30f..28d01da6 100644
--- a/maps-app/build.gradle.kts
+++ b/maps-app/build.gradle.kts
@@ -62,6 +62,7 @@ dependencies {
implementation(libs.androidx.compose.activity)
implementation(libs.androidx.compose.foundation)
implementation(libs.androidx.compose.material)
+ implementation(libs.androidx.compose.material3)
implementation(libs.kotlin)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.androidx.compose.ui.preview.tooling)
@@ -71,7 +72,6 @@ dependencies {
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.leakcanary.android)
-
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.test.core)
androidTestImplementation(libs.androidx.test.rules)
diff --git a/maps-app/src/main/AndroidManifest.xml b/maps-app/src/main/AndroidManifest.xml
index 484beb71..5ef1eda6 100644
--- a/maps-app/src/main/AndroidManifest.xml
+++ b/maps-app/src/main/AndroidManifest.xml
@@ -39,57 +39,67 @@
+
+
+
+ android:exported="true" />
+ android:exported="true"/>
+ android:exported="true"/>
+ android:exported="true"/>
+ android:exported="true"/>
+ android:exported="true"/>
+ android:exported="true"/>
+ android:exported="true"/>
+ android:exported="true"/>
+ android:exported="true"/>
+ android:exported="true"/>
+ android:exported="true"/>
+ android:exported="true"/>
+ android:exported="true"/>
+ android:exported="true"/>
+ android:exported="true"/>
+ android:exported="true"/>
diff --git a/maps-app/src/main/java/com/google/maps/android/compose/Demo.kt b/maps-app/src/main/java/com/google/maps/android/compose/Demo.kt
new file mode 100644
index 00000000..9cf9bea5
--- /dev/null
+++ b/maps-app/src/main/java/com/google/maps/android/compose/Demo.kt
@@ -0,0 +1,315 @@
+// Copyright 2025 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.google.maps.android.compose
+
+import androidx.activity.ComponentActivity
+import androidx.annotation.StringRes
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.KeyboardArrowDown
+import androidx.compose.material.icons.filled.KeyboardArrowUp
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import com.google.maps.android.compose.markerexamples.AdvancedMarkersActivity
+import com.google.maps.android.compose.markerexamples.MarkerClusteringActivity
+import com.google.maps.android.compose.markerexamples.draggablemarkerscollectionwithpolygon.DraggableMarkersCollectionWithPolygonActivity
+import com.google.maps.android.compose.markerexamples.markerdragevents.MarkerDragEventsActivity
+import com.google.maps.android.compose.markerexamples.markerscollection.MarkersCollectionActivity
+import com.google.maps.android.compose.markerexamples.syncingdraggablemarkerwithdatamodel.SyncingDraggableMarkerWithDataModelActivity
+import com.google.maps.android.compose.markerexamples.updatingnodragmarkerwithdatamodel.UpdatingNoDragMarkerWithDataModelActivity
+import kotlin.reflect.KClass
+
+// This file defines the data structures and composable functions used to display the list of
+// demo activities on the main screen of the app. The main goal is to present the demos in a
+// clear, organized, and easy-to-navigate way.
+
+/**
+ * A sealed class representing a group of related demo activities. Using a sealed class here
+ * allows us to define a closed set of categories, which is ideal for a static list of demos.
+ * This ensures that we can handle all possible categories in a type-safe way.
+ *
+ * @param title The title of the activity group.
+ * @param activities The list of activities belonging to this group.
+ */
+sealed class ActivityGroup(
+ @StringRes val title: Int,
+ val activities: List
+) {
+ object MapTypes : ActivityGroup(
+ R.string.map_types_title,
+ listOf(
+ Activity(
+ R.string.basic_map_activity,
+ R.string.basic_map_activity_description,
+ BasicMapActivity::class
+ ),
+ Activity(
+ R.string.street_view_activity,
+ R.string.street_view_activity_description,
+ StreetViewActivity::class
+ ),
+ )
+ )
+
+ object MapFeatures : ActivityGroup(
+ R.string.map_features_title,
+ listOf(
+ Activity(
+ R.string.location_tracking_activity,
+ R.string.location_tracking_activity_description,
+ LocationTrackingActivity::class
+ ),
+ Activity(
+ R.string.scale_bar_activity,
+ R.string.scale_bar_activity_description,
+ ScaleBarActivity::class
+ ),
+ Activity(
+ R.string.custom_controls_activity,
+ R.string.custom_controls_activity_description,
+ CustomControlsActivity::class
+ ),
+ Activity(
+ R.string.accessibility_activity,
+ R.string.accessibility_activity_description,
+ AccessibilityActivity::class
+ ),
+ )
+ )
+
+ object Markers : ActivityGroup(
+ R.string.markers_title,
+ listOf(
+ Activity(
+ R.string.advanced_markers_activity,
+ R.string.advanced_markers_activity_description,
+ AdvancedMarkersActivity::class
+ ),
+ Activity(
+ R.string.marker_clustering_activity,
+ R.string.marker_clustering_activity_description,
+ MarkerClusteringActivity::class
+ ),
+ Activity(
+ R.string.marker_drag_events_activity,
+ R.string.marker_drag_events_activity_description,
+ MarkerDragEventsActivity::class
+ ),
+ Activity(
+ R.string.markers_collection_activity,
+ R.string.markers_collection_activity_description,
+ MarkersCollectionActivity::class
+ ),
+ Activity(
+ R.string.syncing_draggable_marker_with_data_model_activity,
+ R.string.syncing_draggable_marker_with_data_model_activity_description,
+ SyncingDraggableMarkerWithDataModelActivity::class
+ ),
+ Activity(
+ R.string.updating_no_drag_marker_with_data_model_activity,
+ R.string.updating_no_drag_marker_with_data_model_activity_description,
+ UpdatingNoDragMarkerWithDataModelActivity::class
+ ),
+ Activity(
+ R.string.draggable_markers_collection_with_polygon_activity,
+ R.string.draggable_markers_collection_with_polygon_activity_description,
+ DraggableMarkersCollectionWithPolygonActivity::class
+ ),
+ )
+ )
+
+ object UIIntegration : ActivityGroup(
+ R.string.ui_integration_title,
+ listOf(
+ Activity(
+ R.string.map_in_column_activity,
+ R.string.map_in_column_activity_description,
+ MapInColumnActivity::class
+ ),
+ Activity(
+ R.string.maps_in_lazy_column_activity,
+ R.string.maps_in_lazy_column_activity_description,
+ MapsInLazyColumnActivity::class
+ ),
+ Activity(
+ R.string.fragment_demo_activity,
+ R.string.fragment_demo_activity_description,
+ FragmentDemoActivity::class
+ ),
+ )
+ )
+
+ object Performance : ActivityGroup(
+ R.string.performance_title,
+ listOf(
+ Activity(
+ R.string.recomposition_activity,
+ R.string.recomposition_activity_description,
+ RecompositionActivity::class
+ ),
+ )
+ )
+}
+
+/**
+ * A data class representing a single demo activity. This class serves as a model for each
+ * item in the demo list.
+ *
+ * @param title The title of the activity.
+ * @param description A short description of what the demo showcases.
+ * @param kClass The class of the activity to be launched.
+ */
+data class Activity(
+ @StringRes val title: Int,
+ @StringRes val description: Int,
+ val kClass: KClass
+)
+
+/**
+ * The single source of truth for all the demo activity groups. This list is used to populate
+ * the main screen.
+ */
+val allActivityGroups = listOf(
+ ActivityGroup.MapTypes,
+ ActivityGroup.MapFeatures,
+ ActivityGroup.Markers,
+ ActivityGroup.UIIntegration,
+ ActivityGroup.Performance,
+)
+
+/**
+ * A composable function that displays a collapsible list of demo activity groups. This is the
+ * main UI component for the main screen.
+ *
+ * The list is built using a `LazyColumn` for performance, ensuring that only the visible items
+ * are rendered. Each group is represented by a clickable `Card` that expands or collapses to
+ * reveal the activities within it. This approach keeps the UI clean and organized, especially
+ * with a large number of demos.
+ *
+ * @param onActivityClick A lambda function to be invoked when a demo activity is clicked. This
+ * allows the navigation logic to be decoupled from the UI.
+ */
+@Composable
+fun DemoList(
+ onActivityClick: (KClass) -> Unit
+) {
+ // Tracks the currently expanded group to ensure only one is open at a time,
+ // creating a clean, accordion-style user interface.
+ var expandedGroup by remember { mutableStateOf(null) }
+
+ LazyColumn {
+ items(allActivityGroups) { group ->
+ val isExpanded = expandedGroup == group
+ Column {
+ // The card representing the group header.
+ GroupHeaderItem(group, isExpanded) {
+ expandedGroup = (if (isExpanded) null else group)
+ }
+
+ // Animate the visibility of the activities within the group.
+ AnimatedVisibility(visible = isExpanded) {
+ Column(
+ modifier = Modifier.padding(start = 16.dp, end = 16.dp)
+ ) {
+ // Create a card for each activity in the group.
+ group.activities.forEach { activity ->
+ DemoActivityItem(onActivityClick, activity)
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun DemoActivityItem(
+ onActivityClick: (KClass) -> Unit,
+ activity: Activity
+) {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 4.dp)
+ .clickable { onActivityClick(activity.kClass) }
+ ) {
+ Column(modifier = Modifier.padding(16.dp)) {
+ Text(text = stringResource(activity.title), fontWeight = FontWeight.Bold)
+ Text(text = stringResource(activity.description))
+ }
+ }
+}
+
+@Composable
+private fun GroupHeaderItem(
+ group: ActivityGroup,
+ isExpanded: Boolean,
+ onGroupClicked: () -> Unit = {}
+) {
+ Card(
+ // Highlight the card when it's expanded.
+ colors = if (isExpanded) {
+ CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer)
+ } else {
+ CardDefaults.cardColors()
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp)
+ .clickable {
+ onGroupClicked()
+ }
+ ) {
+ Row(
+ modifier = Modifier.padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = stringResource(group.title),
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.weight(1f)
+ )
+ // Show an up or down arrow to indicate the expanded/collapsed state.
+ Icon(
+ imageVector = if (isExpanded) {
+ Icons.Default.KeyboardArrowUp
+ } else {
+ Icons.Default.KeyboardArrowDown
+ },
+ contentDescription = null
+ )
+ }
+ }
+}
diff --git a/maps-app/src/main/java/com/google/maps/android/compose/MainActivity.kt b/maps-app/src/main/java/com/google/maps/android/compose/MainActivity.kt
index 3994a996..a392b8f6 100644
--- a/maps-app/src/main/java/com/google/maps/android/compose/MainActivity.kt
+++ b/maps-app/src/main/java/com/google/maps/android/compose/MainActivity.kt
@@ -21,31 +21,21 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.Button
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Surface
-import androidx.compose.material.Text
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.unit.dp
-import com.google.maps.android.compose.markerexamples.AdvancedMarkersActivity
-import com.google.maps.android.compose.markerexamples.MarkerClusteringActivity
-import com.google.maps.android.compose.markerexamples.draggablemarkerscollectionwithpolygon.DraggableMarkersCollectionWithPolygonActivity
-import com.google.maps.android.compose.markerexamples.markerdragevents.MarkerDragEventsActivity
-import com.google.maps.android.compose.markerexamples.markerscollection.MarkersCollectionActivity
-import com.google.maps.android.compose.markerexamples.syncingdraggablemarkerwithdatamodel.SyncingDraggableMarkerWithDataModelActivity
-import com.google.maps.android.compose.markerexamples.updatingnodragmarkerwithdatamodel.UpdatingNoDragMarkerWithDataModelActivity
import com.google.maps.android.compose.theme.MapsComposeSampleTheme
class MainActivity : ComponentActivity() {
+ @OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
@@ -60,161 +50,25 @@ class MainActivity : ComponentActivity() {
setContent {
MapsComposeSampleTheme {
- Surface(
- modifier = Modifier.fillMaxSize()
+ val context = LocalContext.current
+ Scaffold(
+ modifier = Modifier
+ .fillMaxSize()
.systemBarsPadding(),
- color = MaterialTheme.colors.background
- ) {
- val context = LocalContext.current
+ topBar = {
+ CenterAlignedTopAppBar(
+ title = { Text(text = getString(R.string.main_activity_title)) }
+ )
+ }
+ ) { paddingValues ->
Column(
Modifier
.fillMaxSize()
- .verticalScroll(rememberScrollState()),
+ .padding(paddingValues),
horizontalAlignment = Alignment.CenterHorizontally
) {
- Spacer(modifier = Modifier.padding(10.dp))
- Text(
- text = getString(R.string.main_activity_title),
- style = MaterialTheme.typography.h5
- )
- Spacer(modifier = Modifier.padding(10.dp))
- Button(
- onClick = {
- context.startActivity(Intent(context, BasicMapActivity::class.java))
- }) {
- Text(getString(R.string.basic_map_activity))
- }
- Spacer(modifier = Modifier.padding(5.dp))
- Button(
- onClick = {
- context.startActivity(Intent(context, AdvancedMarkersActivity::class.java))
- }) {
- Text(getString(R.string.advanced_markers))
- }
- Spacer(modifier = Modifier.padding(5.dp))
- Button(
- onClick = {
- context.startActivity(
- Intent(
- context,
- MarkerClusteringActivity::class.java
- )
- )
- }) {
- Text(getString(R.string.marker_clustering_activity))
- }
- Spacer(modifier = Modifier.padding(5.dp))
- Button(
- onClick = {
- context.startActivity(
- Intent(
- context,
- MapInColumnActivity::class.java
- )
- )
- }) {
- Text(getString(R.string.map_in_column_activity))
- }
- Spacer(modifier = Modifier.padding(5.dp))
- Button(
- onClick = {
- context.startActivity(
- Intent(
- context,
- MapsInLazyColumnActivity::class.java
- )
- )
- }) {
- Text(getString(R.string.maps_in_lazy_column_activity))
- }
- Spacer(modifier = Modifier.padding(5.dp))
- Button(
- onClick = {
- context.startActivity(
- Intent(
- context,
- LocationTrackingActivity::class.java
- )
- )
- }) {
- Text(getString(R.string.location_tracking_activity))
- }
- Spacer(modifier = Modifier.padding(5.dp))
- Button(
- onClick = {
- context.startActivity(Intent(context, ScaleBarActivity::class.java))
- }) {
- Text(getString(R.string.scale_bar_activity))
- }
- Spacer(modifier = Modifier.padding(5.dp))
- Button(
- onClick = {
- context.startActivity(Intent(context, StreetViewActivity::class.java))
- }) {
- Text(getString(R.string.street_view))
- }
- Spacer(modifier = Modifier.padding(5.dp))
- Button(
- onClick = {
- context.startActivity(Intent(context, CustomControlsActivity::class.java))
- }) {
- Text(getString(R.string.custom_location_button))
- }
- Spacer(modifier = Modifier.padding(5.dp))
- Button(
- onClick = {
- context.startActivity(Intent(context, AccessibilityActivity::class.java))
- }) {
- Text(getString(R.string.accessibility_button))
- }
- Spacer(modifier = Modifier.padding(5.dp))
- Button(
- onClick = {
- context.startActivity(Intent(context, RecompositionActivity::class.java))
- }) {
- Text(getString(R.string.recomposition_activity))
- }
- Spacer(modifier = Modifier.padding(5.dp))
- Button(
- onClick = {
- context.startActivity(Intent(context, MarkerDragEventsActivity::class.java))
- }) {
- Text(getString(R.string.marker_drag_events_activity))
- }
- Spacer(modifier = Modifier.padding(5.dp))
- Button(
- onClick = {
- context.startActivity(Intent(context, MarkersCollectionActivity::class.java))
- }) {
- Text(getString(R.string.markers_collection_activity))
- }
- Spacer(modifier = Modifier.padding(5.dp))
- Button(
- onClick = {
- context.startActivity(Intent(context, SyncingDraggableMarkerWithDataModelActivity::class.java))
- }) {
- Text(getString(R.string.syncing_draggable_marker_with_data_model))
- }
- Spacer(modifier = Modifier.padding(5.dp))
- Button(
- onClick = {
- context.startActivity(Intent(context, UpdatingNoDragMarkerWithDataModelActivity::class.java))
- }) {
- Text(getString(R.string.updating_non_draggable_marker_with_data_model))
- }
- Spacer(modifier = Modifier.padding(5.dp))
- Button(
- onClick = {
- context.startActivity(Intent(context, DraggableMarkersCollectionWithPolygonActivity::class.java))
- }) {
- Text(getString(R.string.draggable_markers_collection_with_polygon))
- }
- Spacer(modifier = Modifier.padding(5.dp))
- Button(
- onClick = {
- context.startActivity(Intent(context, FragmentDemoActivity::class.java))
- }) {
- Text(getString(R.string.fragment_demo_activity))
+ DemoList {
+ context.startActivity(Intent(context, it.java))
}
}
}
diff --git a/maps-app/src/main/res/values/strings.xml b/maps-app/src/main/res/values/strings.xml
index ea0381f6..d06ac870 100644
--- a/maps-app/src/main/res/values/strings.xml
+++ b/maps-app/src/main/res/values/strings.xml
@@ -17,21 +17,62 @@
android-maps-compose
"Maps Compose Demos \uD83D\uDDFA"
+
Basic Map
- Advanced Markers
- Map In Column
+ A simple map showing the default configuration.
+
+ Street View
+ A simple Street View.
+
+ Location Tracking
+ Tracking your location on the map.
+
+ Scale Bar
+ Displaying a scale bar on the map.
+
+ Custom Controls
+ Replacing the default location button with a custom one.
+
+ Accessibility
+ Making your map more accessible.
+
+ Advanced Markers
+ Adding advanced markers to your map.
+
Marker Clustering
+ Clustering markers on your map.
+
Marker Drag Events
+ Listening to marker drag events.
+
Markers Collection
- Location Tracking
- Scale Bar
- Syncing Draggable Marker With Model
- Updating Non-Draggable Marker With Model
- Polygon around draggable markers
- Fragment Demo Activity
- Recomposition Map
- Street View
- Custom Location Button
- Accessibility
+ Adding a collection of markers to your map.
+
+ Syncing Draggable Marker With Data Model
+ Keeping a draggable marker in sync with a data model.
+
+ Updating No-Drag Marker With Data Model
+ Updating a non-draggable marker with a data model.
+
+ Draggable Markers Collection With Polygon
+ Dragging a collection of markers that form a polygon.
+
+ Map in Column
+ Displaying a map within a column.
+
Maps in LazyColumn
+ Displaying multiple maps in a LazyColumn.
+
+ Fragment Demo
+ Using the map compose components in a fragment.
+
+ Recomposition
+ Understanding how recomposition works with maps.
+
+
+ Map Types
+ Map Features
+ Markers
+ UI Integration
+ Performance
\ No newline at end of file