From 055074bbdab0e2db09bc6c848411ea0747a8f1c1 Mon Sep 17 00:00:00 2001 From: Ashik Abbas Date: Tue, 13 Oct 2026 14:45:02 +0530 Subject: [PATCH] feat(UIControlsScreen): add dedicated UI controls sample and snippets across Kotlin, Java, and Compose --- .../maps3d/common/Map3DQuickSettingsWidget.kt | 8 +- .../example/maps3d/common/Map3DSettings.kt | 78 ++-- .../maps3d/common/Map3DSettingsController.kt | 19 +- .../res/layout/control_panel_ui_controls.xml | 233 +++++++++++ .../layout/dialog_map3d_quick_settings.xml | 12 +- .../common/src/main/res/values/strings.xml | 16 + .../common/Map3DSettingsControllerTest.kt | 22 + .../java-app/src/main/AndroidManifest.xml | 7 + .../maps3djava/mainactivity/MainActivity.java | 2 + .../uicontrols/UiControlsActivity.java | 380 ++++++++++++++++++ .../kotlin-app/src/main/AndroidManifest.xml | 7 + .../maps3dkotlin/mainactivity/MainActivity.kt | 2 + .../uicontrols/UiControlsActivity.kt | 297 ++++++++++++++ .../app/src/main/AndroidManifest.xml | 1 + .../com/example/composedemos/MainActivity.kt | 6 + .../uicontrols/UiControlsActivity.kt | 320 +++++++++++++++ .../snippets/java/SnippetRegistry.java | 25 +- .../example/snippets/java/TrackedMap3D.java | 7 + .../java/snippets/UiControlSnippets.java | 194 +++++++++ .../snippets/kotlin/SnippetRegistry.kt | 10 + .../example/snippets/kotlin/TrackedMap3D.kt | 7 + .../kotlin/snippets/UiControlSnippets.kt | 195 +++++++++ 22 files changed, 1807 insertions(+), 41 deletions(-) create mode 100644 Maps3DSamples/ApiDemos/common/src/main/res/layout/control_panel_ui_controls.xml create mode 100644 Maps3DSamples/ApiDemos/java-app/src/main/java/com/example/maps3djava/uicontrols/UiControlsActivity.java create mode 100644 Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/uicontrols/UiControlsActivity.kt create mode 100644 Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/uicontrols/UiControlsActivity.kt create mode 100644 snippets/java-app/src/main/java/com/example/snippets/java/snippets/UiControlSnippets.java create mode 100644 snippets/kotlin-app/src/main/java/com/example/snippets/kotlin/snippets/UiControlSnippets.kt diff --git a/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/Map3DQuickSettingsWidget.kt b/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/Map3DQuickSettingsWidget.kt index 9f687325..0d6ffa07 100644 --- a/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/Map3DQuickSettingsWidget.kt +++ b/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/Map3DQuickSettingsWidget.kt @@ -293,10 +293,6 @@ fun Map3DQuickSettingsWidget( val updated = settingsController.updateUiControl(rotate = checked) onSettingsChanged(updated) }, - Triple(stringResource(id = R.string.map3d_control_compass), settings.uiControls.compass) { checked: Boolean -> - val updated = settingsController.updateUiControl(compass = checked) - onSettingsChanged(updated) - }, Triple(stringResource(id = R.string.map3d_control_pan_v), settings.uiControls.panVertical) { checked: Boolean -> val updated = settingsController.updateUiControl(panVertical = checked) onSettingsChanged(updated) @@ -305,6 +301,10 @@ fun Map3DQuickSettingsWidget( val updated = settingsController.updateUiControl(panHorizontal = checked) onSettingsChanged(updated) }, + Triple(stringResource(id = R.string.map3d_control_compass), settings.uiControls.compass) { checked: Boolean -> + val updated = settingsController.updateUiControl(compass = checked) + onSettingsChanged(updated) + }, ) controlItems.forEach { (label, isChecked, onToggle) -> diff --git a/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/Map3DSettings.kt b/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/Map3DSettings.kt index 508695d4..fbd91b1d 100644 --- a/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/Map3DSettings.kt +++ b/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/Map3DSettings.kt @@ -35,7 +35,6 @@ import com.google.android.gms.maps3d.model.Map3DMode import com.google.android.gms.maps3d.model.PanHorizontalControl import com.google.android.gms.maps3d.model.PanVerticalControl import com.google.android.gms.maps3d.model.RotateControl -import com.google.android.gms.maps3d.model.Spacer import com.google.android.gms.maps3d.model.TiltControl import com.google.android.gms.maps3d.model.ZoomControl @@ -57,22 +56,30 @@ enum class Map3DColorScheme(val value: Int) { // [START maps_3d_quick_settings_ui_controls_model] /** - * Visibility state for built-in 3D camera UI controls. + * Placement edge constant alias for [ControlPlacement] ([ControlPlacement.TRAILING] vs [ControlPlacement.LEADING]). + */ +typealias UiControlsPlacementPosition = ControlPlacement + +/** + * Visibility and placement state for built-in 3D camera UI controls. * - * @property zoom Zoom in/out (+ / -) control on trailing edge. - * @property tilt Pitch up/down control on trailing edge. - * @property rotate Heading rotation (CW / CCW) control on trailing edge. - * @property compass North orientation indicator & reset control on trailing edge. - * @property panVertical North/South pan control on leading edge. - * @property panHorizontal East/West pan control on leading edge. + * @property zoom Zoom in/out (+ / -) control. + * @property tilt Pitch up/down control. + * @property rotate Heading rotation (CW / CCW) control. + * @property compass North orientation indicator & reset control. + * @property panVertical North/South pan control. + * @property panHorizontal East/West pan control. + * @property controlsPosition Screen edge for main controls ([UiControlsPlacementPosition.TRAILING] or [UiControlsPlacementPosition.LEADING]); pan and compass controls are placed on the opposite edge. */ -data class Map3DUiControls( +data class Map3DUiControls @JvmOverloads constructor( val zoom: Boolean = true, val tilt: Boolean = true, val rotate: Boolean = true, val compass: Boolean = true, val panVertical: Boolean = true, val panHorizontal: Boolean = true, + @param:ControlPlacement @get:ControlPlacement + val controlsPosition: Int = UiControlsPlacementPosition.TRAILING, ) { val allEnabled: Boolean get() = zoom && tilt && rotate && compass && panVertical && panHorizontal @@ -91,14 +98,20 @@ data class Map3DUiControls( /** * Calculates the top margin (in dp) required to place a floating action button directly - * below the active top-anchored trailing camera controls ([zoom], [tilt], [rotate]). + * below the active top-anchored trailing camera controls. */ fun calculateFabMarginDp(): Int { - var count = 0 - if (zoom) count++ - if (tilt) count++ - if (rotate) count++ - return if (count > 0) 16 + count * 104 + 8 else 16 + var totalHeightDp = 0 + if (controlsPosition == UiControlsPlacementPosition.LEADING) { + if (panVertical) totalHeightDp += 104 + if (panHorizontal) totalHeightDp += 104 + if (compass) totalHeightDp += 56 + } else { + if (zoom) totalHeightDp += 104 + if (tilt) totalHeightDp += 104 + if (rotate) totalHeightDp += 104 + } + return if (totalHeightDp > 0) 16 + totalHeightDp + 8 else 16 } } // [END maps_3d_quick_settings_ui_controls_model] @@ -150,22 +163,29 @@ data class Map3DSettings( // Apply built-in UI controls configuration try { googleMap3D.getUiControlSettings()?.let { settings -> - val trailingControls = mutableListOf() - if (uiControls.zoom) trailingControls.add(ZoomControl(null)) - if (uiControls.tilt) trailingControls.add(TiltControl(null)) - if (uiControls.rotate) trailingControls.add(RotateControl(null)) - if (uiControls.compass) { - if (trailingControls.isNotEmpty()) { - trailingControls.add(Spacer()) - } - trailingControls.add(CompassControl()) + val mainControls = mutableListOf() + if (uiControls.zoom) mainControls.add(ZoomControl(null)) + if (uiControls.tilt) mainControls.add(TiltControl(null)) + if (uiControls.rotate) mainControls.add(RotateControl(null)) + + val panControls = mutableListOf() + if (uiControls.panVertical) panControls.add(PanVerticalControl(null)) + if (uiControls.panHorizontal) panControls.add(PanHorizontalControl(null)) + if (uiControls.compass) panControls.add(CompassControl()) + + val mainPosition = if (uiControls.controlsPosition == UiControlsPlacementPosition.LEADING) { + ControlPlacement.LEADING + } else { + ControlPlacement.TRAILING + } + val panPosition = if (mainPosition == ControlPlacement.TRAILING) { + ControlPlacement.LEADING + } else { + ControlPlacement.TRAILING } - settings.setUiControlsPlacement(trailingControls, ControlPlacement.TRAILING) - val leadingControls = mutableListOf() - if (uiControls.panVertical) leadingControls.add(PanVerticalControl(null)) - if (uiControls.panHorizontal) leadingControls.add(PanHorizontalControl(null)) - settings.setUiControlsPlacement(leadingControls, ControlPlacement.LEADING) + settings.setUiControlsPlacement(mainControls, mainPosition) + settings.setUiControlsPlacement(panControls, panPosition) } } catch (e: Throwable) { Log.e("Map3DSettings", "Could not apply UI controls placement to GoogleMap3D", e) diff --git a/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/Map3DSettingsController.kt b/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/Map3DSettingsController.kt index 545b1e75..184e06cb 100644 --- a/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/Map3DSettingsController.kt +++ b/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/Map3DSettingsController.kt @@ -16,6 +16,7 @@ package com.example.maps3d.common +import com.google.android.gms.maps3d.UiControlSettings.ControlPlacement import com.google.android.gms.maps3d.model.Map3DMode import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -145,8 +146,22 @@ class Map3DSettingsController( } /** - * Updates a single UI control's visibility. + * Updates the screen placement edge ([UiControlsPlacementPosition.TRAILING] or [UiControlsPlacementPosition.LEADING]) + * for the main camera controls, placing pan controls on the opposite edge. */ + fun setControlsPosition(@ControlPlacement position: Int): Map3DSettings { + val sanitized = if (position == UiControlsPlacementPosition.LEADING) { + UiControlsPlacementPosition.LEADING + } else { + UiControlsPlacementPosition.TRAILING + } + return setUiControls(_settings.value.uiControls.copy(controlsPosition = sanitized)) + } + + /** + * Updates individual UI control visibility or placement flags. + */ + @JvmOverloads fun updateUiControl( zoom: Boolean = _settings.value.uiControls.zoom, tilt: Boolean = _settings.value.uiControls.tilt, @@ -154,6 +169,7 @@ class Map3DSettingsController( compass: Boolean = _settings.value.uiControls.compass, panVertical: Boolean = _settings.value.uiControls.panVertical, panHorizontal: Boolean = _settings.value.uiControls.panHorizontal, + @ControlPlacement controlsPosition: Int = _settings.value.uiControls.controlsPosition, ): Map3DSettings { val updatedControls = _settings.value.uiControls.copy( zoom = zoom, @@ -162,6 +178,7 @@ class Map3DSettingsController( compass = compass, panVertical = panVertical, panHorizontal = panHorizontal, + controlsPosition = controlsPosition, ) return setUiControls(updatedControls) } diff --git a/Maps3DSamples/ApiDemos/common/src/main/res/layout/control_panel_ui_controls.xml b/Maps3DSamples/ApiDemos/common/src/main/res/layout/control_panel_ui_controls.xml new file mode 100644 index 00000000..f9fc49b8 --- /dev/null +++ b/Maps3DSamples/ApiDemos/common/src/main/res/layout/control_panel_ui_controls.xml @@ -0,0 +1,233 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Maps3DSamples/ApiDemos/common/src/main/res/layout/dialog_map3d_quick_settings.xml b/Maps3DSamples/ApiDemos/common/src/main/res/layout/dialog_map3d_quick_settings.xml index f9a73153..fdc69df5 100644 --- a/Maps3DSamples/ApiDemos/common/src/main/res/layout/dialog_map3d_quick_settings.xml +++ b/Maps3DSamples/ApiDemos/common/src/main/res/layout/dialog_map3d_quick_settings.xml @@ -171,25 +171,25 @@ android:text="@string/map3d_control_rotate" /> + android:text="@string/map3d_control_pan_v" /> + android:text="@string/map3d_control_pan_h" /> + android:text="@string/map3d_control_compass" /> diff --git a/Maps3DSamples/ApiDemos/common/src/main/res/values/strings.xml b/Maps3DSamples/ApiDemos/common/src/main/res/values/strings.xml index 365c248b..fd92ffb7 100644 --- a/Maps3DSamples/ApiDemos/common/src/main/res/values/strings.xml +++ b/Maps3DSamples/ApiDemos/common/src/main/res/values/strings.xml @@ -43,6 +43,7 @@ Data Visualization Cloud Map Styling Roadmap Mode + UI Controls Field Of View 3D Screen Projection @@ -286,4 +287,19 @@ Done Reset to Defaults Open 3D map quick settings + + + 3D Map UI Controls + Visible Controls + Main Controls Placement + Trailing (Right) + Leading (Left) + Show All + Hide All + Zoom + Tilt + Rotate + Compass + Pan Vertical + Pan Horizontal diff --git a/Maps3DSamples/ApiDemos/common/src/test/java/com/example/maps3d/common/Map3DSettingsControllerTest.kt b/Maps3DSamples/ApiDemos/common/src/test/java/com/example/maps3d/common/Map3DSettingsControllerTest.kt index ff0d2d6c..c2b61d07 100644 --- a/Maps3DSamples/ApiDemos/common/src/test/java/com/example/maps3d/common/Map3DSettingsControllerTest.kt +++ b/Maps3DSamples/ApiDemos/common/src/test/java/com/example/maps3d/common/Map3DSettingsControllerTest.kt @@ -271,4 +271,26 @@ class Map3DSettingsControllerTest { assertEquals(128, Map3DUiControls(zoom = true, tilt = false, rotate = false).calculateFabMarginDp()) assertEquals(16, Map3DUiControls(zoom = false, tilt = false, rotate = false).calculateFabMarginDp()) } + + @Test + fun setControlsPosition_leading_updatesPlacementAndAdjustsFabMargin() { + assertEquals(UiControlsPlacementPosition.TRAILING, controller.getSettings().uiControls.controlsPosition) + + val updated = controller.setControlsPosition(UiControlsPlacementPosition.LEADING) + + assertEquals(UiControlsPlacementPosition.LEADING, updated.uiControls.controlsPosition) + assertEquals(288, updated.uiControls.calculateFabMarginDp()) + assertEquals( + 232, + updated.uiControls.copy(compass = false).calculateFabMarginDp(), + ) + assertEquals( + 184, + updated.uiControls.copy(panHorizontal = false).calculateFabMarginDp(), + ) + assertEquals( + 16, + updated.uiControls.copy(panVertical = false, panHorizontal = false, compass = false).calculateFabMarginDp(), + ) + } } diff --git a/Maps3DSamples/ApiDemos/java-app/src/main/AndroidManifest.xml b/Maps3DSamples/ApiDemos/java-app/src/main/AndroidManifest.xml index 7385455c..a6676ead 100644 --- a/Maps3DSamples/ApiDemos/java-app/src/main/AndroidManifest.xml +++ b/Maps3DSamples/ApiDemos/java-app/src/main/AndroidManifest.xml @@ -207,6 +207,13 @@ android:exported="true" /> + + diff --git a/Maps3DSamples/ApiDemos/java-app/src/main/java/com/example/maps3djava/mainactivity/MainActivity.java b/Maps3DSamples/ApiDemos/java-app/src/main/java/com/example/maps3djava/mainactivity/MainActivity.java index 2c211868..dc0d414a 100644 --- a/Maps3DSamples/ApiDemos/java-app/src/main/java/com/example/maps3djava/mainactivity/MainActivity.java +++ b/Maps3DSamples/ApiDemos/java-app/src/main/java/com/example/maps3djava/mainactivity/MainActivity.java @@ -52,6 +52,7 @@ import com.example.maps3djava.roadmapmode.RoadmapModeActivity; import com.example.maps3djava.fieldofview.FieldOfViewActivity; import com.example.maps3djava.projection3d.Projection3DActivity; +import com.example.maps3djava.uicontrols.UiControlsActivity; import com.google.android.material.appbar.MaterialToolbar; import java.util.LinkedHashMap; @@ -81,6 +82,7 @@ public class MainActivity extends AppCompatActivity { put(R.string.feature_title_data_visualization, DataVisualizationActivity.class); put(R.string.feature_title_cloud_styling, CloudStylingActivity.class); put(R.string.feature_title_roadmap_mode, RoadmapModeActivity.class); + put(R.string.feature_title_ui_controls, UiControlsActivity.class); put(R.string.feature_title_field_of_view, FieldOfViewActivity.class); put(R.string.feature_title_projection_3d, Projection3DActivity.class); }}; diff --git a/Maps3DSamples/ApiDemos/java-app/src/main/java/com/example/maps3djava/uicontrols/UiControlsActivity.java b/Maps3DSamples/ApiDemos/java-app/src/main/java/com/example/maps3djava/uicontrols/UiControlsActivity.java new file mode 100644 index 00000000..bf26f283 --- /dev/null +++ b/Maps3DSamples/ApiDemos/java-app/src/main/java/com/example/maps3djava/uicontrols/UiControlsActivity.java @@ -0,0 +1,380 @@ +/* + * 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.maps3djava.uicontrols; + +import static com.example.maps3d.common.UtilitiesKt.toValidCamera; + +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.transition.TransitionManager; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.RadioGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.cardview.widget.CardView; +import com.example.maps3d.common.Map3DSettings; +import com.example.maps3d.common.Map3DSettingsController; +import com.example.maps3d.common.Map3DUiControls; +import com.example.maps3dcommon.R; +import com.example.maps3djava.sampleactivity.SampleBaseActivity; +import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps3d.GoogleMap3D; +import com.google.android.gms.maps3d.UiControlSettings.ControlPlacement; +import com.google.android.gms.maps3d.model.Camera; +import com.google.android.gms.maps3d.model.LatLngAltitude; +import com.google.android.gms.maps3d.model.Map3DMode; +import com.google.android.material.appbar.MaterialToolbar; +import com.google.android.material.button.MaterialButton; +import com.google.android.material.chip.Chip; + +/** + * ================================================================================================= + * Dedicated 3D Map UI Controls & Placement Demo (Java Views) + * ================================================================================================= + * + * Demonstrates configuring built-in 3D map UI control buttons (Zoom, Tilt, Rotate, Compass, + * PanVertical, PanHorizontal, Spacer) and toggling their screen placement between + * {@link ControlPlacement#TRAILING} and {@link ControlPlacement#LEADING}. + */ +public class UiControlsActivity extends SampleBaseActivity { + + /** Focal landmark centered on the San Francisco Financial District. */ + public static final LatLng SF_LOCATION = new LatLng(37.7915, -122.4010); + + // --- UI Elements --- + + private CardView controlsCard; + private View cardHeader; + private View cardContent; + private MaterialButton btnCollapse; + private MaterialButton btnShowAll; + private MaterialButton btnHideAll; + private Chip chipZoom; + private Chip chipTilt; + private Chip chipRotate; + private Chip chipCompass; + private Chip chipPanVertical; + private Chip chipPanHorizontal; + private RadioGroup rgPlacement; + + // --- State Variables --- + + private boolean isCollapsed = false; + private boolean isSyncingUi = false; + + // --- Handlers & Runnables --- + + private final Handler fadeHandler = new Handler(Looper.getMainLooper()); + private final Runnable fadeOutRunnable = () -> { + if (controlsCard != null && !isCollapsed) { + controlsCard.animate() + .alpha(0.85f) + .setDuration(400) + .start(); + } + }; + + @NonNull + @Override + public String getTAG() { + return "UiControlsActivity"; + } + + @Override + @Map3DMode + protected int getDefaultMapMode() { + return Map3DMode.ROADMAP; + } + + @NonNull + @Override + public Camera getInitialCamera() { + return toValidCamera(new Camera( + new LatLngAltitude(SF_LOCATION.latitude, SF_LOCATION.longitude, 250.0), + 45.0, + 65.0, + 0.0, + 800.0 + )); + } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Hide base pill scroll view as this activity manages its own overlay card + View baseScrollView = findViewById(R.id.control_scroll_view); + if (baseScrollView != null) { + baseScrollView.setVisibility(View.GONE); + } + + ViewGroup container = findViewById(R.id.map_container); + if (container != null) { + getLayoutInflater().inflate(R.layout.control_panel_ui_controls, container, true); + } + + MaterialToolbar topBar = findViewById(R.id.top_bar); + if (topBar != null) { + topBar.setTitle(R.string.feature_title_ui_controls); + topBar.setNavigationOnClickListener(v -> finish()); + } + + initViews(); + } + + private void initViews() { + controlsCard = findViewById(R.id.control_panel); + cardHeader = findViewById(R.id.card_header); + cardContent = findViewById(R.id.card_content); + btnCollapse = findViewById(R.id.btn_collapse); + btnShowAll = findViewById(R.id.btn_show_all); + btnHideAll = findViewById(R.id.btn_hide_all); + chipZoom = findViewById(R.id.chip_zoom); + chipTilt = findViewById(R.id.chip_tilt); + chipRotate = findViewById(R.id.chip_rotate); + chipCompass = findViewById(R.id.chip_compass); + chipPanVertical = findViewById(R.id.chip_pan_vertical); + chipPanHorizontal = findViewById(R.id.chip_pan_horizontal); + rgPlacement = findViewById(R.id.rg_controls_placement); + + if (btnCollapse != null) { + btnCollapse.setOnClickListener(v -> { + if (isCollapsed) { + expandControls(); + } else { + collapseControls(); + } + }); + } + + if (cardHeader != null) { + cardHeader.setOnClickListener(v -> { + if (isCollapsed) { + expandControls(); + } else { + collapseControls(); + } + }); + } + + if (btnShowAll != null) { + btnShowAll.setOnClickListener(v -> { + Map3DSettings updated = Map3DSettingsController.getShared().setAllUiControls(true); + if (googleMap3D != null) { + updated.applyTo(googleMap3D); + } + syncControlPanelUi(updated); + }); + } + + if (btnHideAll != null) { + btnHideAll.setOnClickListener(v -> { + Map3DSettings updated = Map3DSettingsController.getShared().setAllUiControls(false); + if (googleMap3D != null) { + updated.applyTo(googleMap3D); + } + syncControlPanelUi(updated); + }); + } + + if (chipZoom != null) { + chipZoom.setOnCheckedChangeListener((buttonView, isChecked) -> { + if (isSyncingUi) return; + Map3DUiControls c = Map3DSettingsController.getShared().getSettings().getUiControls(); + Map3DSettings updated = Map3DSettingsController.getShared().updateUiControl( + isChecked, c.getTilt(), c.getRotate(), c.getCompass(), c.getPanVertical(), c.getPanHorizontal() + ); + if (googleMap3D != null) { + updated.applyTo(googleMap3D); + } + }); + } + + if (chipTilt != null) { + chipTilt.setOnCheckedChangeListener((buttonView, isChecked) -> { + if (isSyncingUi) return; + Map3DUiControls c = Map3DSettingsController.getShared().getSettings().getUiControls(); + Map3DSettings updated = Map3DSettingsController.getShared().updateUiControl( + c.getZoom(), isChecked, c.getRotate(), c.getCompass(), c.getPanVertical(), c.getPanHorizontal() + ); + if (googleMap3D != null) { + updated.applyTo(googleMap3D); + } + }); + } + + if (chipRotate != null) { + chipRotate.setOnCheckedChangeListener((buttonView, isChecked) -> { + if (isSyncingUi) return; + Map3DUiControls c = Map3DSettingsController.getShared().getSettings().getUiControls(); + Map3DSettings updated = Map3DSettingsController.getShared().updateUiControl( + c.getZoom(), c.getTilt(), isChecked, c.getCompass(), c.getPanVertical(), c.getPanHorizontal() + ); + if (googleMap3D != null) { + updated.applyTo(googleMap3D); + } + }); + } + + if (chipCompass != null) { + chipCompass.setOnCheckedChangeListener((buttonView, isChecked) -> { + if (isSyncingUi) return; + Map3DUiControls c = Map3DSettingsController.getShared().getSettings().getUiControls(); + Map3DSettings updated = Map3DSettingsController.getShared().updateUiControl( + c.getZoom(), c.getTilt(), c.getRotate(), isChecked, c.getPanVertical(), c.getPanHorizontal() + ); + if (googleMap3D != null) { + updated.applyTo(googleMap3D); + } + }); + } + + if (chipPanVertical != null) { + chipPanVertical.setOnCheckedChangeListener((buttonView, isChecked) -> { + if (isSyncingUi) return; + Map3DUiControls c = Map3DSettingsController.getShared().getSettings().getUiControls(); + Map3DSettings updated = Map3DSettingsController.getShared().updateUiControl( + c.getZoom(), c.getTilt(), c.getRotate(), c.getCompass(), isChecked, c.getPanHorizontal() + ); + if (googleMap3D != null) { + updated.applyTo(googleMap3D); + } + }); + } + + if (chipPanHorizontal != null) { + chipPanHorizontal.setOnCheckedChangeListener((buttonView, isChecked) -> { + if (isSyncingUi) return; + Map3DUiControls c = Map3DSettingsController.getShared().getSettings().getUiControls(); + Map3DSettings updated = Map3DSettingsController.getShared().updateUiControl( + c.getZoom(), c.getTilt(), c.getRotate(), c.getCompass(), c.getPanVertical(), isChecked + ); + if (googleMap3D != null) { + updated.applyTo(googleMap3D); + } + }); + } + + if (rgPlacement != null) { + rgPlacement.setOnCheckedChangeListener((group, checkedId) -> { + if (isSyncingUi) return; + int position; + if (checkedId == R.id.rb_placement_leading) { + position = ControlPlacement.LEADING; + } else if (checkedId == R.id.rb_placement_trailing) { + position = ControlPlacement.TRAILING; + } else { + return; + } + Map3DSettings updated = Map3DSettingsController.getShared().setControlsPosition(position); + if (googleMap3D != null) { + updated.applyTo(googleMap3D); + } + }); + } + + syncControlPanelUi(Map3DSettingsController.getShared().getSettings()); + fadeHandler.postDelayed(fadeOutRunnable, 3000L); + } + + @Override + protected void onQuickSettingsChanged(@NonNull Map3DSettings settings) { + syncControlPanelUi(settings); + } + + private void syncControlPanelUi(@NonNull Map3DSettings settings) { + isSyncingUi = true; + Map3DUiControls controls = settings.getUiControls(); + if (chipZoom != null) chipZoom.setChecked(controls.getZoom()); + if (chipTilt != null) chipTilt.setChecked(controls.getTilt()); + if (chipRotate != null) chipRotate.setChecked(controls.getRotate()); + if (chipCompass != null) chipCompass.setChecked(controls.getCompass()); + if (chipPanVertical != null) chipPanVertical.setChecked(controls.getPanVertical()); + if (chipPanHorizontal != null) chipPanHorizontal.setChecked(controls.getPanHorizontal()); + + if (rgPlacement != null) { + int targetId = (controls.getControlsPosition() == ControlPlacement.LEADING) + ? R.id.rb_placement_leading + : R.id.rb_placement_trailing; + if (rgPlacement.getCheckedRadioButtonId() != targetId) { + rgPlacement.check(targetId); + } + } + isSyncingUi = false; + } + + // --- UI Collapse / Expand Mechanics --- + + private void collapseControls() { + if (controlsCard == null || cardContent == null) { + return; + } + isCollapsed = true; + fadeHandler.removeCallbacks(fadeOutRunnable); + if (btnCollapse != null) { + btnCollapse.setIconResource(R.drawable.expand_less_24px); + btnCollapse.setContentDescription(getString(R.string.expand_controls)); + } + TransitionManager.beginDelayedTransition(controlsCard); + cardContent.setVisibility(View.GONE); + } + + private void expandControls() { + if (controlsCard == null || cardContent == null) { + return; + } + isCollapsed = false; + if (btnCollapse != null) { + btnCollapse.setIconResource(R.drawable.expand_more_24px); + btnCollapse.setContentDescription(getString(R.string.collapse_controls)); + } + TransitionManager.beginDelayedTransition(controlsCard); + cardContent.setVisibility(View.VISIBLE); + fadeHandler.removeCallbacks(fadeOutRunnable); + fadeHandler.postDelayed(fadeOutRunnable, 3000L); + } + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_DOWN || ev.getAction() == MotionEvent.ACTION_MOVE) { + if (controlsCard != null && !isCollapsed) { + controlsCard.animate() + .alpha(1.0f) + .setDuration(150) + .start(); + fadeHandler.removeCallbacks(fadeOutRunnable); + fadeHandler.postDelayed(fadeOutRunnable, 3000L); + } + } + return super.dispatchTouchEvent(ev); + } + + @Override + protected void onMapReady(@NonNull GoogleMap3D googleMap3D) { + super.onMapReady(googleMap3D); + googleMap3D.setCamera(getInitialCamera()); + } + + @Override + protected void onDestroy() { + fadeHandler.removeCallbacks(fadeOutRunnable); + super.onDestroy(); + } +} diff --git a/Maps3DSamples/ApiDemos/kotlin-app/src/main/AndroidManifest.xml b/Maps3DSamples/ApiDemos/kotlin-app/src/main/AndroidManifest.xml index 8f56448d..131a23e2 100644 --- a/Maps3DSamples/ApiDemos/kotlin-app/src/main/AndroidManifest.xml +++ b/Maps3DSamples/ApiDemos/kotlin-app/src/main/AndroidManifest.xml @@ -210,6 +210,13 @@ android:exported="true" /> + + diff --git a/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/mainactivity/MainActivity.kt b/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/mainactivity/MainActivity.kt index f84d8f41..e83a37a0 100644 --- a/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/mainactivity/MainActivity.kt +++ b/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/mainactivity/MainActivity.kt @@ -68,6 +68,7 @@ import com.example.maps3dkotlin.cloudstyling.CloudStylingActivity import com.example.maps3dkotlin.roadmapmode.RoadmapModeActivity import com.example.maps3dkotlin.fieldofview.FieldOfViewActivity import com.example.maps3dkotlin.projection3d.Projection3DActivity +import com.example.maps3dkotlin.uicontrols.UiControlsActivity import com.example.maps3dkotlin.theme.Maps3DSamplesTheme import kotlinx.coroutines.launch @@ -114,6 +115,7 @@ class MainActivity : ComponentActivity() { Sample(R.string.feature_title_data_visualization, DataVisualizationActivity::class.java), Sample(R.string.feature_title_cloud_styling, CloudStylingActivity::class.java), Sample(R.string.feature_title_roadmap_mode, RoadmapModeActivity::class.java), + Sample(R.string.feature_title_ui_controls, UiControlsActivity::class.java), Sample(R.string.feature_title_field_of_view, FieldOfViewActivity::class.java), Sample(R.string.feature_title_projection_3d, Projection3DActivity::class.java), ) diff --git a/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/uicontrols/UiControlsActivity.kt b/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/uicontrols/UiControlsActivity.kt new file mode 100644 index 00000000..075b6b6f --- /dev/null +++ b/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/uicontrols/UiControlsActivity.kt @@ -0,0 +1,297 @@ +/* + * 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.maps3dkotlin.uicontrols + +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.transition.TransitionManager +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.widget.RadioGroup +import androidx.cardview.widget.CardView +import com.example.maps3d.common.Map3DSettings +import com.example.maps3d.common.Map3DSettingsController +import com.example.maps3d.common.UiControlsPlacementPosition +import com.example.maps3d.common.toValidCamera +import com.example.maps3dcommon.R +import com.example.maps3dkotlin.sampleactivity.SampleBaseActivity +import com.google.android.gms.maps.model.LatLng +import com.google.android.gms.maps3d.GoogleMap3D +import com.google.android.gms.maps3d.model.Camera +import com.google.android.gms.maps3d.model.Map3DMode +import com.google.android.gms.maps3d.model.camera +import com.google.android.gms.maps3d.model.latLngAltitude +import com.google.android.material.appbar.MaterialToolbar +import com.google.android.material.button.MaterialButton +import com.google.android.material.chip.Chip + +/** + * ================================================================================================= + * Dedicated 3D Map UI Controls & Placement Demo (Kotlin Views) + * ================================================================================================= + * + * Demonstrates configuring built-in 3D map UI control buttons (Zoom, Tilt, Rotate, Compass, + * PanVertical, PanHorizontal, Spacer) and toggling their screen placement between + * [UiControlsPlacementPosition.TRAILING] and [UiControlsPlacementPosition.LEADING]. + */ +class UiControlsActivity : SampleBaseActivity() { + + override val TAG = "UiControlsActivity" + + @Map3DMode + override val defaultMapMode: Int = Map3DMode.ROADMAP + + override val initialCamera: Camera + get() = camera { + center = latLngAltitude { + latitude = SF_LOCATION.latitude + longitude = SF_LOCATION.longitude + altitude = 250.0 + } + heading = 45.0 + tilt = 65.0 + roll = 0.0 + range = 800.0 + }.toValidCamera() + + // --- UI Elements --- + + private var controlsCard: CardView? = null + private var cardHeader: View? = null + private var cardContent: View? = null + private var btnCollapse: MaterialButton? = null + private var btnShowAll: MaterialButton? = null + private var btnHideAll: MaterialButton? = null + private var chipZoom: Chip? = null + private var chipTilt: Chip? = null + private var chipRotate: Chip? = null + private var chipCompass: Chip? = null + private var chipPanVertical: Chip? = null + private var chipPanHorizontal: Chip? = null + private var rgPlacement: RadioGroup? = null + + // --- State Variables --- + + private var isCollapsed = false + private var isSyncingUi = false + + // --- Handlers & Runnables --- + + private val fadeHandler = Handler(Looper.getMainLooper()) + private val fadeOutRunnable = Runnable { + if (!isCollapsed) { + controlsCard?.animate() + ?.alpha(0.85f) + ?.setDuration(400) + ?.start() + } + } + + // --- Lifecycle & Initialization --- + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // Hide base pill scroll view as this activity manages its own overlay card + findViewById(R.id.control_scroll_view)?.visibility = View.GONE + + // Inflate UI controls panel overlay into map container managed by SampleBaseActivity + findViewById(R.id.map_container)?.let { container -> + layoutInflater.inflate(R.layout.control_panel_ui_controls, container, true) + } + + findViewById(R.id.top_bar)?.apply { + setTitle(R.string.feature_title_ui_controls) + setNavigationOnClickListener { finish() } + } + + initViews() + } + + private fun initViews() { + controlsCard = findViewById(R.id.control_panel) + cardHeader = findViewById(R.id.card_header) + cardContent = findViewById(R.id.card_content) + btnCollapse = findViewById(R.id.btn_collapse) + btnShowAll = findViewById(R.id.btn_show_all) + btnHideAll = findViewById(R.id.btn_hide_all) + chipZoom = findViewById(R.id.chip_zoom) + chipTilt = findViewById(R.id.chip_tilt) + chipRotate = findViewById(R.id.chip_rotate) + chipCompass = findViewById(R.id.chip_compass) + chipPanVertical = findViewById(R.id.chip_pan_vertical) + chipPanHorizontal = findViewById(R.id.chip_pan_horizontal) + rgPlacement = findViewById(R.id.rg_controls_placement) + + btnCollapse?.setOnClickListener { + if (isCollapsed) expandControls() else collapseControls() + } + + cardHeader?.setOnClickListener { + if (isCollapsed) expandControls() else collapseControls() + } + + btnShowAll?.setOnClickListener { + val updated = Map3DSettingsController.shared.setAllUiControls(true) + googleMap3D?.let { updated.applyTo(it) } + syncControlPanelUi(updated) + } + + btnHideAll?.setOnClickListener { + val updated = Map3DSettingsController.shared.setAllUiControls(false) + googleMap3D?.let { updated.applyTo(it) } + syncControlPanelUi(updated) + } + + chipZoom?.setOnCheckedChangeListener { _, isChecked -> + if (isSyncingUi) return@setOnCheckedChangeListener + val updated = Map3DSettingsController.shared.updateUiControl(zoom = isChecked) + googleMap3D?.let { updated.applyTo(it) } + } + + chipTilt?.setOnCheckedChangeListener { _, isChecked -> + if (isSyncingUi) return@setOnCheckedChangeListener + val updated = Map3DSettingsController.shared.updateUiControl(tilt = isChecked) + googleMap3D?.let { updated.applyTo(it) } + } + + chipRotate?.setOnCheckedChangeListener { _, isChecked -> + if (isSyncingUi) return@setOnCheckedChangeListener + val updated = Map3DSettingsController.shared.updateUiControl(rotate = isChecked) + googleMap3D?.let { updated.applyTo(it) } + } + + chipCompass?.setOnCheckedChangeListener { _, isChecked -> + if (isSyncingUi) return@setOnCheckedChangeListener + val updated = Map3DSettingsController.shared.updateUiControl(compass = isChecked) + googleMap3D?.let { updated.applyTo(it) } + } + + chipPanVertical?.setOnCheckedChangeListener { _, isChecked -> + if (isSyncingUi) return@setOnCheckedChangeListener + val updated = Map3DSettingsController.shared.updateUiControl(panVertical = isChecked) + googleMap3D?.let { updated.applyTo(it) } + } + + chipPanHorizontal?.setOnCheckedChangeListener { _, isChecked -> + if (isSyncingUi) return@setOnCheckedChangeListener + val updated = Map3DSettingsController.shared.updateUiControl(panHorizontal = isChecked) + googleMap3D?.let { updated.applyTo(it) } + } + + rgPlacement?.setOnCheckedChangeListener { _, checkedId -> + if (isSyncingUi) return@setOnCheckedChangeListener + val position = when (checkedId) { + R.id.rb_placement_leading -> UiControlsPlacementPosition.LEADING + R.id.rb_placement_trailing -> UiControlsPlacementPosition.TRAILING + else -> return@setOnCheckedChangeListener + } + val updated = Map3DSettingsController.shared.setControlsPosition(position) + googleMap3D?.let { updated.applyTo(it) } + } + + syncControlPanelUi(Map3DSettingsController.shared.settings.value) + fadeHandler.postDelayed(fadeOutRunnable, 3000L) + } + + override fun onQuickSettingsChanged(settings: Map3DSettings) { + syncControlPanelUi(settings) + } + + private fun syncControlPanelUi(settings: Map3DSettings) { + isSyncingUi = true + val controls = settings.uiControls + chipZoom?.isChecked = controls.zoom + chipTilt?.isChecked = controls.tilt + chipRotate?.isChecked = controls.rotate + chipCompass?.isChecked = controls.compass + chipPanVertical?.isChecked = controls.panVertical + chipPanHorizontal?.isChecked = controls.panHorizontal + + val targetPlacementId = if (controls.controlsPosition == UiControlsPlacementPosition.LEADING) { + R.id.rb_placement_leading + } else { + R.id.rb_placement_trailing + } + val group = rgPlacement + if (group != null && group.checkedRadioButtonId != targetPlacementId) { + group.check(targetPlacementId) + } + isSyncingUi = false + } + + // --- UI Collapse / Expand Mechanics --- + + private fun collapseControls() { + val card = controlsCard ?: return + val content = cardContent ?: return + isCollapsed = true + fadeHandler.removeCallbacks(fadeOutRunnable) + btnCollapse?.setIconResource(R.drawable.expand_less_24px) + btnCollapse?.contentDescription = getString(R.string.expand_controls) + + TransitionManager.beginDelayedTransition(card) + content.visibility = View.GONE + } + + private fun expandControls() { + val card = controlsCard ?: return + val content = cardContent ?: return + isCollapsed = false + btnCollapse?.setIconResource(R.drawable.expand_more_24px) + btnCollapse?.contentDescription = getString(R.string.collapse_controls) + + TransitionManager.beginDelayedTransition(card) + content.visibility = View.VISIBLE + + fadeHandler.removeCallbacks(fadeOutRunnable) + fadeHandler.postDelayed(fadeOutRunnable, 3000L) + } + + override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { + if (ev?.action == MotionEvent.ACTION_DOWN || ev?.action == MotionEvent.ACTION_MOVE) { + if (!isCollapsed) { + controlsCard?.animate() + ?.alpha(1.0f) + ?.setDuration(150) + ?.start() + fadeHandler.removeCallbacks(fadeOutRunnable) + fadeHandler.postDelayed(fadeOutRunnable, 3000L) + } + } + return super.dispatchTouchEvent(ev) + } + + // --- 3D Map Setup --- + + override fun onMapReady(googleMap3D: GoogleMap3D) { + super.onMapReady(googleMap3D) + googleMap3D.setCamera(initialCamera) + } + + override fun onDestroy() { + fadeHandler.removeCallbacks(fadeOutRunnable) + super.onDestroy() + } + + companion object { + /** Focal landmark centered on the San Francisco Financial District. */ + val SF_LOCATION = LatLng(37.7915, -122.4010) + } +} diff --git a/Maps3DSamples/ComposeDemos/app/src/main/AndroidManifest.xml b/Maps3DSamples/ComposeDemos/app/src/main/AndroidManifest.xml index 1fdee10d..fdf0641f 100644 --- a/Maps3DSamples/ComposeDemos/app/src/main/AndroidManifest.xml +++ b/Maps3DSamples/ComposeDemos/app/src/main/AndroidManifest.xml @@ -63,6 +63,7 @@ + diff --git a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/MainActivity.kt b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/MainActivity.kt index 37c9820e..143798d1 100644 --- a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/MainActivity.kt +++ b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/MainActivity.kt @@ -58,6 +58,7 @@ import com.example.composedemos.polylines.PolylinesActivity import com.example.composedemos.popovers.PopoversActivity import com.example.composedemos.roadmapmode.RoadmapModeActivity import com.example.composedemos.routes.RoutesActivity +import com.example.composedemos.uicontrols.UiControlsActivity import com.example.maps3d.common.Map3DSessionTheme class MainActivity : ComponentActivity() { @@ -203,6 +204,11 @@ fun CatalogScreen() { context.startActivity(Intent(context, RoadmapModeActivity::class.java)) } } + item { + SampleItem("UI Controls") { + context.startActivity(Intent(context, UiControlsActivity::class.java)) + } + } item { SampleItem("Field Of View") { context.startActivity(Intent(context, FieldOfViewActivity::class.java)) diff --git a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/uicontrols/UiControlsActivity.kt b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/uicontrols/UiControlsActivity.kt new file mode 100644 index 00000000..afb60fcd --- /dev/null +++ b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/uicontrols/UiControlsActivity.kt @@ -0,0 +1,320 @@ +/* + * 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.composedemos.uicontrols + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.FilterChip +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +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.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.example.maps3d.common.Map3DQuickSettingsWidget +import com.example.maps3d.common.Map3DSessionTheme +import com.example.maps3d.common.Map3DSettingsController +import com.example.maps3d.common.UiControlsPlacementPosition +import com.example.maps3d.common.rememberMap3DSessionSettings +import com.example.maps3dcommon.R +import com.google.android.gms.maps.model.LatLng +import com.google.android.gms.maps3d.GoogleMap3D +import com.google.android.gms.maps3d.model.Map3DMode +import com.google.android.gms.maps3d.model.camera +import com.google.android.gms.maps3d.model.latLngAltitude +import com.google.maps.android.compose3d.GoogleMap3D + +class UiControlsActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + Map3DSessionTheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background, + ) { + UiControlsScreen() + } + } + } + } + + companion object { + // San Francisco Financial District + val SF_LOCATION = LatLng(37.7915, -122.4010) + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun UiControlsScreen() { + var googleMap3D by remember { mutableStateOf(null) } + var isCollapsed by remember { mutableStateOf(false) } + val settings by rememberMap3DSessionSettings(defaultMapMode = Map3DMode.ROADMAP) + val uiControls = settings.uiControls + + val initialLocation = UiControlsActivity.SF_LOCATION + val currentCameraState by remember { + mutableStateOf( + camera { + center = latLngAltitude { + latitude = initialLocation.latitude + longitude = initialLocation.longitude + altitude = 250.0 + } + heading = 45.0 + tilt = 65.0 + range = 800.0 + }, + ) + } + + Box(modifier = Modifier.fillMaxSize()) { + GoogleMap3D( + camera = currentCameraState, + mapMode = settings.mapMode, + modifier = Modifier.fillMaxSize(), + onMapReady = { map -> + googleMap3D = map + }, + ) + + Card( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .padding(12.dp), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), + elevation = CardDefaults.cardElevation(defaultElevation = 8.dp), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { isCollapsed = !isCollapsed }, + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(R.string.ui_controls_panel_title), + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Bold, + ) + + IconButton(onClick = { isCollapsed = !isCollapsed }) { + Icon( + painter = painterResource( + id = if (isCollapsed) { + R.drawable.expand_less_24px + } else { + R.drawable.expand_more_24px + }, + ), + contentDescription = stringResource( + id = if (isCollapsed) { + R.string.expand_controls + } else { + R.string.collapse_controls + }, + ), + ) + } + } + + AnimatedVisibility(visible = !isCollapsed) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 4.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(R.string.ui_controls_visibility_label), + style = MaterialTheme.typography.labelMedium, + ) + + Row { + TextButton( + onClick = { + Map3DSettingsController.shared.setAllUiControls(true) + }, + ) { + Text(text = stringResource(R.string.ui_controls_show_all)) + } + + TextButton( + onClick = { + Map3DSettingsController.shared.setAllUiControls(false) + }, + ) { + Text(text = stringResource(R.string.ui_controls_hide_all)) + } + } + } + + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + FilterChip( + selected = uiControls.zoom, + onClick = { + Map3DSettingsController.shared.updateUiControl(zoom = !uiControls.zoom) + }, + label = { Text(stringResource(R.string.ui_control_chip_zoom)) }, + ) + FilterChip( + selected = uiControls.tilt, + onClick = { + Map3DSettingsController.shared.updateUiControl(tilt = !uiControls.tilt) + }, + label = { Text(stringResource(R.string.ui_control_chip_tilt)) }, + ) + FilterChip( + selected = uiControls.rotate, + onClick = { + Map3DSettingsController.shared.updateUiControl(rotate = !uiControls.rotate) + }, + label = { Text(stringResource(R.string.ui_control_chip_rotate)) }, + ) + FilterChip( + selected = uiControls.panVertical, + onClick = { + Map3DSettingsController.shared.updateUiControl(panVertical = !uiControls.panVertical) + }, + label = { Text(stringResource(R.string.ui_control_chip_pan_vertical)) }, + ) + FilterChip( + selected = uiControls.panHorizontal, + onClick = { + Map3DSettingsController.shared.updateUiControl(panHorizontal = !uiControls.panHorizontal) + }, + label = { Text(stringResource(R.string.ui_control_chip_pan_horizontal)) }, + ) + FilterChip( + selected = uiControls.compass, + onClick = { + Map3DSettingsController.shared.updateUiControl(compass = !uiControls.compass) + }, + label = { Text(stringResource(R.string.ui_control_chip_compass)) }, + ) + } + + Spacer(modifier = Modifier.height(6.dp)) + + Text( + text = stringResource(R.string.ui_controls_placement_label), + style = MaterialTheme.typography.labelMedium, + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + val placements = listOf( + UiControlsPlacementPosition.TRAILING to stringResource(R.string.ui_controls_placement_trailing), + UiControlsPlacementPosition.LEADING to stringResource(R.string.ui_controls_placement_leading), + ) + + placements.forEach { (positionValue, positionLabel) -> + Row( + modifier = Modifier + .weight(1f) + .selectable( + selected = (uiControls.controlsPosition == positionValue), + onClick = { + Map3DSettingsController.shared.setControlsPosition(positionValue) + }, + role = Role.RadioButton, + ) + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + RadioButton( + selected = (uiControls.controlsPosition == positionValue), + onClick = null, + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = positionLabel, + style = MaterialTheme.typography.bodyMedium, + ) + } + } + } + } + } + } + } + + Map3DQuickSettingsWidget( + modifier = Modifier + .align(Alignment.TopEnd) + .statusBarsPadding() + .padding(top = 8.dp, end = 8.dp), + sampleDefaultMapMode = Map3DMode.ROADMAP, + googleMap3D = googleMap3D, + ) + } +} diff --git a/snippets/java-app/src/main/java/com/example/snippets/java/SnippetRegistry.java b/snippets/java-app/src/main/java/com/example/snippets/java/SnippetRegistry.java index ef38592e..71641e22 100644 --- a/snippets/java-app/src/main/java/com/example/snippets/java/SnippetRegistry.java +++ b/snippets/java-app/src/main/java/com/example/snippets/java/SnippetRegistry.java @@ -19,8 +19,18 @@ import android.content.Context; import com.example.snippets.java.annotations.SnippetGroup; import com.example.snippets.java.annotations.SnippetItem; -import com.example.snippets.java.snippets.*; +import com.example.snippets.java.snippets.CameraControlSnippets; +import com.example.snippets.java.snippets.MapInitSnippets; +import com.example.snippets.java.snippets.MarkerSnippets; +import com.example.snippets.java.snippets.ModelSnippets; +import com.example.snippets.java.snippets.PlaceSnippets; +import com.example.snippets.java.snippets.PolygonSnippets; +import com.example.snippets.java.snippets.PolylineSnippets; +import com.example.snippets.java.snippets.PopoverSnippets; +import com.example.snippets.java.snippets.UiControlSnippets; import com.google.android.gms.maps3d.Popover; +import com.google.android.gms.maps3d.UiControlSettings; +import com.google.android.gms.maps3d.UiControlSettings.ControlPlacement; import com.google.android.gms.maps3d.model.Marker; import com.google.android.gms.maps3d.model.Model; import com.google.android.gms.maps3d.model.Polygon; @@ -28,6 +38,7 @@ import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; @@ -45,6 +56,17 @@ public static void clearTrackedItems() { else if (item instanceof Polygon) ((Polygon) item).remove(); else if (item instanceof Model) ((Model) item).remove(); else if (item instanceof Popover) ((Popover) item).remove(); + else if (item instanceof UiControlSettings) { + UiControlSettings uiSettings = (UiControlSettings) item; + uiSettings.setUiControlsPlacement( + Collections.emptyList(), ControlPlacement.TRAILING); + uiSettings.setUiControlsPlacement( + Collections.emptyList(), ControlPlacement.LEADING); + uiSettings.setUiControlsPlacement( + Collections.emptyList(), ControlPlacement.TOP); + uiSettings.setUiControlsPlacement( + Collections.emptyList(), ControlPlacement.BOTTOM); + } } catch (Exception e) { /* ignore */ } @@ -56,6 +78,7 @@ public static void clearTrackedItems() { Arrays.asList( MapInitSnippets.class, CameraControlSnippets.class, + UiControlSnippets.class, MarkerSnippets.class, PolygonSnippets.class, PolylineSnippets.class, diff --git a/snippets/java-app/src/main/java/com/example/snippets/java/TrackedMap3D.java b/snippets/java-app/src/main/java/com/example/snippets/java/TrackedMap3D.java index 3a685dd3..bc69cf13 100644 --- a/snippets/java-app/src/main/java/com/example/snippets/java/TrackedMap3D.java +++ b/snippets/java-app/src/main/java/com/example/snippets/java/TrackedMap3D.java @@ -22,6 +22,7 @@ import com.google.android.gms.maps3d.OnMapReadyListener; import com.google.android.gms.maps3d.OnMapSteadyListener; import com.google.android.gms.maps3d.Popover; +import com.google.android.gms.maps3d.UiControlSettings; import com.google.android.gms.maps3d.model.Camera; import com.google.android.gms.maps3d.model.CameraRestriction; import com.google.android.gms.maps3d.model.FlyAroundOptions; @@ -122,4 +123,10 @@ public void setCameraRestriction(CameraRestriction restriction) { public CameraRestriction getCameraRestriction() { return delegate.getCameraRestriction(); } + + public UiControlSettings getUiControlSettings() { + UiControlSettings settings = delegate.getUiControlSettings(); + if (settings != null) items.add(settings); + return settings; + } } diff --git a/snippets/java-app/src/main/java/com/example/snippets/java/snippets/UiControlSnippets.java b/snippets/java-app/src/main/java/com/example/snippets/java/snippets/UiControlSnippets.java new file mode 100644 index 00000000..6656efaf --- /dev/null +++ b/snippets/java-app/src/main/java/com/example/snippets/java/snippets/UiControlSnippets.java @@ -0,0 +1,194 @@ +/* + * 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.snippets.java.snippets; + +import android.os.Handler; +import android.os.Looper; +import com.example.snippets.java.TrackedMap3D; +import com.example.snippets.java.annotations.SnippetGroup; +import com.example.snippets.java.annotations.SnippetItem; +import com.google.android.gms.maps3d.UiControlSettings; +import com.google.android.gms.maps3d.UiControlSettings.ControlOrientation; +import com.google.android.gms.maps3d.UiControlSettings.ControlPlacement; +import com.google.android.gms.maps3d.model.Camera; +import com.google.android.gms.maps3d.model.CompassControl; +import com.google.android.gms.maps3d.model.FlyToOptions; +import com.google.android.gms.maps3d.model.LatLngAltitude; +import com.google.android.gms.maps3d.model.Map3DControl; +import com.google.android.gms.maps3d.model.PanHorizontalControl; +import com.google.android.gms.maps3d.model.PanVerticalControl; +import com.google.android.gms.maps3d.model.RotateControl; +import com.google.android.gms.maps3d.model.Spacer; +import com.google.android.gms.maps3d.model.TiltControl; +import com.google.android.gms.maps3d.model.ZoomControl; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +@SnippetGroup( + title = "UI Controls", + description = + "Snippets demonstrating built-in 3D map camera UI controls and screen placement.") +public class UiControlSnippets { + + private final TrackedMap3D map; + + public UiControlSnippets(TrackedMap3D map) { + this.map = map; + } + + /** + * Configures the built-in 3D camera UI controls on the trailing and leading edges of the map. + */ + @SuppressWarnings("unused") + @SnippetItem( + title = "1. Configure UI Controls Placement", + description = + "Configures Zoom, Tilt, and Rotate controls on the trailing edge and Pan and Compass controls on the leading edge.") + public void configureUiControlsPlacement() { + focusSampleScene(); + // [START maps_android_3d_ui_controls_placement_java] + UiControlSettings uiSettings = map.getUiControlSettings(); + if (uiSettings != null) { + List trailingControls = + Arrays.asList( + new ZoomControl(ControlOrientation.VERTICAL), + new TiltControl(ControlOrientation.VERTICAL), + new RotateControl(ControlOrientation.VERTICAL)); + uiSettings.setUiControlsPlacement(trailingControls, ControlPlacement.TRAILING); + + List leadingControls = + Arrays.asList( + new PanVerticalControl(ControlOrientation.VERTICAL), + new PanHorizontalControl(ControlOrientation.VERTICAL), + new CompassControl()); + uiSettings.setUiControlsPlacement(leadingControls, ControlPlacement.LEADING); + } + // [END maps_android_3d_ui_controls_placement_java] + } + + /** + * Swaps the main camera controls to the leading edge and secondary pan/compass controls to the + * trailing edge. + */ + @SuppressWarnings("unused") + @SnippetItem( + title = "2. Swap Leading & Trailing Edges", + description = + "Moves main camera controls (Zoom, Tilt, Rotate) to the leading edge and Pan/Compass controls to the trailing edge.") + public void swapUiControlsEdges() { + focusSampleScene(); + // [START maps_android_3d_ui_controls_swap_edges_java] + UiControlSettings uiSettings = map.getUiControlSettings(); + if (uiSettings != null) { + List leadingControls = + Arrays.asList( + new ZoomControl(null), new TiltControl(null), new RotateControl(null)); + uiSettings.setUiControlsPlacement(leadingControls, ControlPlacement.LEADING); + + List trailingControls = + Arrays.asList( + new PanVerticalControl(null), + new PanHorizontalControl(null), + new CompassControl()); + uiSettings.setUiControlsPlacement(trailingControls, ControlPlacement.TRAILING); + } + // [END maps_android_3d_ui_controls_swap_edges_java] + } + + /** Configures horizontal UI controls with spacers along the top and bottom screen edges. */ + @SuppressWarnings("unused") + @SnippetItem( + title = "3. Horizontal & Top/Bottom Controls", + description = + "Configures horizontal control orientation with Spacers along the top and bottom edges of the map.") + public void configureHorizontalTopBottomControls() { + focusSampleScene(); + // [START maps_android_3d_ui_controls_orientation_java] + UiControlSettings uiSettings = map.getUiControlSettings(); + if (uiSettings != null) { + List topControls = + Arrays.asList( + new CompassControl(), + new Spacer(), + new RotateControl(ControlOrientation.HORIZONTAL), + new TiltControl(ControlOrientation.HORIZONTAL)); + uiSettings.setUiControlsPlacement(topControls, ControlPlacement.TOP); + + List bottomControls = + Arrays.asList( + new ZoomControl(ControlOrientation.HORIZONTAL), + new Spacer(), + new PanHorizontalControl(ControlOrientation.HORIZONTAL), + new PanVerticalControl(ControlOrientation.HORIZONTAL)); + uiSettings.setUiControlsPlacement(bottomControls, ControlPlacement.BOTTOM); + } + // [END maps_android_3d_ui_controls_orientation_java] + } + + /** + * Hides all built-in 3D camera UI controls by passing an empty list to each placement edge. + */ + @SuppressWarnings("unused") + @SnippetItem( + title = "4. Hide UI Controls", + description = + "Displays UI controls briefly and then hides all controls across every screen edge using an empty list.") + public void hideUiControls() { + focusSampleScene(); + // [START_EXCLUDE] + UiControlSettings initialSettings = map.getUiControlSettings(); + if (initialSettings != null) { + initialSettings.setUiControlsPlacement( + Arrays.asList( + new ZoomControl(null), new TiltControl(null), new RotateControl(null)), + ControlPlacement.TRAILING); + initialSettings.setUiControlsPlacement( + Arrays.asList( + new PanVerticalControl(null), + new PanHorizontalControl(null), + new CompassControl()), + ControlPlacement.LEADING); + } + // [END_EXCLUDE] + + new Handler(Looper.getMainLooper()) + .postDelayed( + () -> { + // [START maps_android_3d_ui_controls_hide_java] + UiControlSettings uiSettings = map.getUiControlSettings(); + if (uiSettings != null) { + uiSettings.setUiControlsPlacement( + Collections.emptyList(), ControlPlacement.TRAILING); + uiSettings.setUiControlsPlacement( + Collections.emptyList(), ControlPlacement.LEADING); + uiSettings.setUiControlsPlacement( + Collections.emptyList(), ControlPlacement.TOP); + uiSettings.setUiControlsPlacement( + Collections.emptyList(), ControlPlacement.BOTTOM); + } + // [END maps_android_3d_ui_controls_hide_java] + }, + 2000); + } + + private void focusSampleScene() { + Camera targetCamera = + new Camera(new LatLngAltitude(37.8199, -122.4783, 0.0), 330.0, 55.0, 0.0, 1500.0); + map.flyCameraTo(new FlyToOptions(targetCamera, 1500L)); + } +} diff --git a/snippets/kotlin-app/src/main/java/com/example/snippets/kotlin/SnippetRegistry.kt b/snippets/kotlin-app/src/main/java/com/example/snippets/kotlin/SnippetRegistry.kt index f4cfecde..321e5467 100644 --- a/snippets/kotlin-app/src/main/java/com/example/snippets/kotlin/SnippetRegistry.kt +++ b/snippets/kotlin-app/src/main/java/com/example/snippets/kotlin/SnippetRegistry.kt @@ -27,8 +27,11 @@ import com.example.snippets.kotlin.snippets.PlaceSnippets import com.example.snippets.kotlin.snippets.PolygonSnippets import com.example.snippets.kotlin.snippets.PolylineSnippets import com.example.snippets.kotlin.snippets.PopoverSnippets +import com.example.snippets.kotlin.snippets.UiControlSnippets import com.google.android.gms.maps3d.GoogleMap3D import com.google.android.gms.maps3d.Popover +import com.google.android.gms.maps3d.UiControlSettings +import com.google.android.gms.maps3d.UiControlSettings.ControlPlacement import com.google.android.gms.maps3d.model.Marker import com.google.android.gms.maps3d.model.Model import com.google.android.gms.maps3d.model.Polygon @@ -60,6 +63,12 @@ object SnippetRegistry { is Polygon -> item.remove() is Model -> item.remove() is Popover -> item.remove() + is UiControlSettings -> { + item.setUiControlsPlacement(emptyList(), ControlPlacement.TRAILING) + item.setUiControlsPlacement(emptyList(), ControlPlacement.LEADING) + item.setUiControlsPlacement(emptyList(), ControlPlacement.TOP) + item.setUiControlsPlacement(emptyList(), ControlPlacement.BOTTOM) + } } } catch (e: Exception) { // Ignore failures to avoid crashing if already cleanup or invalid @@ -71,6 +80,7 @@ object SnippetRegistry { private val snippetClasses = listOf( MapInitSnippets::class.java, CameraControlSnippets::class.java, + UiControlSnippets::class.java, MarkerSnippets::class.java, PolygonSnippets::class.java, PolylineSnippets::class.java, diff --git a/snippets/kotlin-app/src/main/java/com/example/snippets/kotlin/TrackedMap3D.kt b/snippets/kotlin-app/src/main/java/com/example/snippets/kotlin/TrackedMap3D.kt index 683d246a..7e2b171c 100644 --- a/snippets/kotlin-app/src/main/java/com/example/snippets/kotlin/TrackedMap3D.kt +++ b/snippets/kotlin-app/src/main/java/com/example/snippets/kotlin/TrackedMap3D.kt @@ -6,6 +6,7 @@ import com.google.android.gms.maps3d.OnMap3DClickListener import com.google.android.gms.maps3d.OnMapReadyListener import com.google.android.gms.maps3d.OnMapSteadyListener import com.google.android.gms.maps3d.Popover +import com.google.android.gms.maps3d.UiControlSettings import com.google.android.gms.maps3d.model.Camera import com.google.android.gms.maps3d.model.CameraRestriction import com.google.android.gms.maps3d.model.FlyAroundOptions @@ -68,4 +69,10 @@ class TrackedMap3D(val delegate: GoogleMap3D, private val items: MutableList + val trailingControls = listOf( + ZoomControl(ControlOrientation.VERTICAL), + TiltControl(ControlOrientation.VERTICAL), + RotateControl(ControlOrientation.VERTICAL), + ) + uiSettings.setUiControlsPlacement(trailingControls, ControlPlacement.TRAILING) + + val leadingControls = listOf( + PanVerticalControl(ControlOrientation.VERTICAL), + PanHorizontalControl(ControlOrientation.VERTICAL), + CompassControl(), + ) + uiSettings.setUiControlsPlacement(leadingControls, ControlPlacement.LEADING) + } + // [END maps_android_3d_ui_controls_placement_kt] + } + + /** + * Swaps the main camera controls to the leading edge and secondary pan/compass controls to the trailing edge. + */ + @Suppress("unused") + @SnippetItem( + title = "2. Swap Leading & Trailing Edges", + description = "Moves main camera controls (Zoom, Tilt, Rotate) to the leading edge and Pan/Compass controls to the trailing edge.", + ) + fun swapUiControlsEdges() { + focusSampleScene() + // [START maps_android_3d_ui_controls_swap_edges_kt] + map.getUiControlSettings()?.let { uiSettings -> + val leadingControls = listOf( + ZoomControl(null), + TiltControl(null), + RotateControl(null), + ) + uiSettings.setUiControlsPlacement(leadingControls, ControlPlacement.LEADING) + + val trailingControls = listOf( + PanVerticalControl(null), + PanHorizontalControl(null), + CompassControl(), + ) + uiSettings.setUiControlsPlacement(trailingControls, ControlPlacement.TRAILING) + } + // [END maps_android_3d_ui_controls_swap_edges_kt] + } + + /** + * Configures horizontal UI controls with spacers along the top and bottom screen edges. + */ + @Suppress("unused") + @SnippetItem( + title = "3. Horizontal & Top/Bottom Controls", + description = "Configures horizontal control orientation with Spacers along the top and bottom edges of the map.", + ) + fun configureHorizontalTopBottomControls() { + focusSampleScene() + // [START maps_android_3d_ui_controls_orientation_kt] + map.getUiControlSettings()?.let { uiSettings -> + val topControls = listOf( + CompassControl(), + Spacer(), + RotateControl(ControlOrientation.HORIZONTAL), + TiltControl(ControlOrientation.HORIZONTAL), + ) + uiSettings.setUiControlsPlacement(topControls, ControlPlacement.TOP) + + val bottomControls = listOf( + ZoomControl(ControlOrientation.HORIZONTAL), + Spacer(), + PanHorizontalControl(ControlOrientation.HORIZONTAL), + PanVerticalControl(ControlOrientation.HORIZONTAL), + ) + uiSettings.setUiControlsPlacement(bottomControls, ControlPlacement.BOTTOM) + } + // [END maps_android_3d_ui_controls_orientation_kt] + } + + /** + * Hides all built-in 3D camera UI controls by passing an empty list to each placement edge. + */ + @Suppress("unused") + @SnippetItem( + title = "4. Hide UI Controls", + description = "Displays UI controls briefly and then hides all controls across every screen edge using an empty list.", + ) + fun hideUiControls() { + focusSampleScene() + // [START_EXCLUDE] + map.getUiControlSettings()?.let { uiSettings -> + uiSettings.setUiControlsPlacement( + listOf(ZoomControl(null), TiltControl(null), RotateControl(null)), + ControlPlacement.TRAILING, + ) + uiSettings.setUiControlsPlacement( + listOf(PanVerticalControl(null), PanHorizontalControl(null), CompassControl()), + ControlPlacement.LEADING, + ) + } + // [END_EXCLUDE] + + lifecycleScope.launch { + // [START_EXCLUDE] + delay(2.seconds) + // [END_EXCLUDE] + // [START maps_android_3d_ui_controls_hide_kt] + map.getUiControlSettings()?.let { uiSettings -> + uiSettings.setUiControlsPlacement(emptyList(), ControlPlacement.TRAILING) + uiSettings.setUiControlsPlacement(emptyList(), ControlPlacement.LEADING) + uiSettings.setUiControlsPlacement(emptyList(), ControlPlacement.TOP) + uiSettings.setUiControlsPlacement(emptyList(), ControlPlacement.BOTTOM) + } + // [END maps_android_3d_ui_controls_hide_kt] + } + } + + private fun focusSampleScene() { + map.flyCameraTo( + flyToOptions { + endCamera = camera { + center = latLngAltitude { + latitude = 37.8199 + longitude = -122.4783 + altitude = 0.0 + } + heading = 330.0 + tilt = 55.0 + range = 1500.0 + } + durationInMillis = 1500 + }, + ) + } +}