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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.example.maps3d.common

import android.content.Context
import android.util.AttributeSet
import androidx.appcompat.app.AlertDialog
import com.example.maps3dcommon.R
import com.google.android.gms.maps3d.GoogleMap3D
import com.google.android.material.floatingactionbutton.FloatingActionButton

// [START maps_3d_quick_settings_button]
/**
* A self-contained settings button overlay for 3D map activities.
*
* Can be positioned anywhere in a layout (e.g., top-end or bottom-end of the map).
* Clicking the button opens a modal [Map3DQuickSettingsDialog] allowing the user
* to switch map modes (Satellite, Hybrid, Roadmap) and color schemes (System, Light, Dark).
*
* When attached to a [GoogleMap3D] instance via [attachMap], changes are automatically
* applied in real time to the 3D map.
*/
class Map3DQuickSettingsButton @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = com.google.android.material.R.attr.floatingActionButtonStyle,
) : FloatingActionButton(context, attrs, defStyleAttr) {

fun interface OnSettingsChangedListener {
fun onSettingsChanged(settings: Map3DSettings)
}

private val controller = Map3DSettingsController()
private var attachedMap: GoogleMap3D? = null
private var mapProvider: (() -> GoogleMap3D?)? = null
private var activeDialog: AlertDialog? = null
private var onSettingsChangedListener: OnSettingsChangedListener? = null

init {
setImageResource(R.drawable.settings_24px)
contentDescription = context.getString(R.string.map3d_settings_button_description)
size = SIZE_MINI

controller.addListener { updatedSettings ->
val map = attachedMap ?: mapProvider?.invoke()
map?.let { updatedSettings.applyTo(it) }
onSettingsChangedListener?.onSettingsChanged(updatedSettings)
}

setOnClickListener {
showDialog()
}
}

/**
* Attaches a [GoogleMap3D] instance.
*
* Settings changes will automatically be applied to this map instance.
*/
fun attachMap(googleMap3D: GoogleMap3D?) {
this.attachedMap = googleMap3D
if (googleMap3D != null) {
controller.getSettings().applyTo(googleMap3D)
}
}

/**
* Sets a provider lambda that resolves the active [GoogleMap3D] instance on demand.
*/
fun setMapProvider(provider: () -> GoogleMap3D?) {
this.mapProvider = provider
}

/** Returns the current [Map3DSettings] snapshot. */
fun getSettings(): Map3DSettings = controller.getSettings()

/** Programmatically sets new [Map3DSettings]. */
fun setSettings(settings: Map3DSettings) {
controller.setSettings(settings)
}

/** Sets a callback invoked whenever the user updates settings in the dialog. */
fun setOnSettingsChangedListener(listener: OnSettingsChangedListener) {
this.onSettingsChangedListener = listener
}

/** Displays the [Map3DQuickSettingsDialog] modal. */
fun showDialog() {
val map = attachedMap ?: mapProvider?.invoke()
activeDialog?.dismiss()
activeDialog = Map3DQuickSettingsDialog.show(
context = context,
controller = controller,
googleMap3D = map,
onDismiss = { activeDialog = null },
)
}
}
// [END maps_3d_quick_settings_button]
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.example.maps3d.common

import android.content.Context
import android.view.LayoutInflater
import android.widget.RadioGroup
import androidx.appcompat.app.AlertDialog
import com.example.maps3dcommon.R
import com.google.android.gms.maps3d.GoogleMap3D
import com.google.android.gms.maps3d.model.Map3DMode
import com.google.android.material.checkbox.MaterialCheckBox
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.materialswitch.MaterialSwitch

// [START maps_3d_quick_settings_dialog]
/**
* Modal dialog for inspecting and adjusting 3D map settings.
*
* Provides single-choice radio controls for Map Mode (Satellite, Hybrid, Roadmap)
* and Color Scheme (Follow System, Light, Dark).
* Directly applies changes to [GoogleMap3D] and app theme for instant live feedback.
*/
object Map3DQuickSettingsDialog {

/**
* Builds and displays the modal [AlertDialog].
*
* @param context Host Android UI [Context].
* @param controller The [Map3DSettingsController] managing settings state.
* @param googleMap3D Optional [GoogleMap3D] instance to apply settings immediately.
* @param onDismiss Optional callback invoked when the dialog is closed.
* @return The displayed [AlertDialog] instance.
*/
fun show(
context: Context,
controller: Map3DSettingsController,
googleMap3D: GoogleMap3D? = null,
onDismiss: (() -> Unit)? = null,
): AlertDialog {
val view = LayoutInflater.from(context).inflate(
R.layout.dialog_map3d_quick_settings,
null,
false,
)

val rgMapMode = view.findViewById<RadioGroup>(R.id.rg_map_mode)
val rgColorScheme = view.findViewById<RadioGroup>(R.id.rg_color_scheme)
val currentSettings = controller.getSettings()

// Set initial Map Mode selection
val initialModeId = when (currentSettings.mapMode) {
Map3DMode.SATELLITE -> R.id.rb_mode_satellite
Map3DMode.ROADMAP -> R.id.rb_mode_roadmap
else -> R.id.rb_mode_hybrid
}
rgMapMode.check(initialModeId)

// Set initial Color Scheme selection
val initialSchemeId = when (currentSettings.colorScheme) {
Map3DColorScheme.LIGHT -> R.id.rb_scheme_light
Map3DColorScheme.DARK -> R.id.rb_scheme_dark
else -> R.id.rb_scheme_system
}
rgColorScheme.check(initialSchemeId)

// Listen for Map Mode changes
rgMapMode.setOnCheckedChangeListener { _, checkedId ->
val newMode = when (checkedId) {
R.id.rb_mode_satellite -> Map3DMode.SATELLITE
R.id.rb_mode_roadmap -> Map3DMode.ROADMAP
else -> Map3DMode.HYBRID
}
val updated = controller.setMapMode(newMode)
googleMap3D?.let { updated.applyTo(it) }
}

// Listen for Color Scheme changes
rgColorScheme.setOnCheckedChangeListener { _, checkedId ->
val newScheme = when (checkedId) {
R.id.rb_scheme_light -> Map3DColorScheme.LIGHT
R.id.rb_scheme_dark -> Map3DColorScheme.DARK
else -> Map3DColorScheme.FOLLOW_SYSTEM
}
val updated = controller.setColorScheme(newScheme)
googleMap3D?.let { updated.applyTo(it) }
}

// Camera UI Controls Checkboxes & Master Switch
val switchAll = view.findViewById<MaterialSwitch>(R.id.switch_toggle_all_controls)
val cbZoom = view.findViewById<MaterialCheckBox>(R.id.cb_control_zoom)
val cbTilt = view.findViewById<MaterialCheckBox>(R.id.cb_control_tilt)
val cbRotate = view.findViewById<MaterialCheckBox>(R.id.cb_control_rotate)
val cbCompass = view.findViewById<MaterialCheckBox>(R.id.cb_control_compass)
val cbPanV = view.findViewById<MaterialCheckBox>(R.id.cb_control_pan_v)
val cbPanH = view.findViewById<MaterialCheckBox>(R.id.cb_control_pan_h)

var isUpdatingUi = false
fun syncCheckboxes(ui: Map3DUiControls) {
isUpdatingUi = true
switchAll.isChecked = ui.allEnabled
cbZoom.isChecked = ui.zoom
cbTilt.isChecked = ui.tilt
cbRotate.isChecked = ui.rotate
cbCompass.isChecked = ui.compass
cbPanV.isChecked = ui.panVertical
cbPanH.isChecked = ui.panHorizontal
isUpdatingUi = false
}
syncCheckboxes(currentSettings.uiControls)

// Master switch toggle
switchAll.setOnCheckedChangeListener { _, isChecked ->
if (isUpdatingUi) return@setOnCheckedChangeListener
val updated = controller.setAllUiControls(isChecked)
syncCheckboxes(updated.uiControls)
googleMap3D?.let { updated.applyTo(it) }
}

// Helper for individual checkboxes
fun onIndividualControlToggled() {
if (isUpdatingUi) return
val updated = controller.updateUiControl(
zoom = cbZoom.isChecked,
tilt = cbTilt.isChecked,
rotate = cbRotate.isChecked,
compass = cbCompass.isChecked,
panVertical = cbPanV.isChecked,
panHorizontal = cbPanH.isChecked,
)
isUpdatingUi = true
switchAll.isChecked = updated.uiControls.allEnabled
isUpdatingUi = false
googleMap3D?.let { updated.applyTo(it) }
}

cbZoom.setOnCheckedChangeListener { _, _ -> onIndividualControlToggled() }
cbTilt.setOnCheckedChangeListener { _, _ -> onIndividualControlToggled() }
cbRotate.setOnCheckedChangeListener { _, _ -> onIndividualControlToggled() }
cbCompass.setOnCheckedChangeListener { _, _ -> onIndividualControlToggled() }
cbPanV.setOnCheckedChangeListener { _, _ -> onIndividualControlToggled() }
cbPanH.setOnCheckedChangeListener { _, _ -> onIndividualControlToggled() }

val dialog = MaterialAlertDialogBuilder(context)
.setTitle(R.string.map3d_settings_title)
.setView(view)
.setPositiveButton(R.string.map3d_settings_done) { d, _ ->
d.dismiss()
}
.setOnDismissListener {
onDismiss?.invoke()
}
.create()

dialog.show()
return dialog
}
}
// [END maps_3d_quick_settings_dialog]
Loading