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