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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* 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 com.google.android.gms.maps3d.model.Map3DMode

/**
* Pure Kotlin state machine controller for Maps Localization.
*
* Manages transitions between [MapLocalePreset] configurations and map rendering modes
* ([Map3DMode.HYBRID] vs [Map3DMode.ROADMAP]).
*/
class LocalizationController(
initialPreset: MapLocalePreset = MapLocalePreset.ENGLISH_US,
) {
private var state = LocalizationState(
selectedPreset = initialPreset,
)

/** Returns the current immutable [LocalizationState]. */
fun getState(): LocalizationState = state

/**
* Selects a new [MapLocalePreset] and updates language and region.
*/
fun selectPreset(preset: MapLocalePreset): LocalizationState {
state = state.copy(
selectedPreset = preset,
languageCode = preset.language,
regionCode = preset.region,
)
return state
}

/**
* Updates the 3D map rendering mode (`Map3DMode.HYBRID` or `Map3DMode.ROADMAP`).
*/
fun setMapMode(@Map3DMode mapMode: Int): LocalizationState {
state = state.copy(mapMode = mapMode)
return state
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* 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 com.google.android.gms.maps3d.model.Camera
import com.google.android.gms.maps3d.model.LocaleOptions
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 java.util.Locale

private const val SYSTEM_DEFAULT_CODE = "default"

val DEFAULT_LOCALIZATION_CAMERA: Camera = camera {
center = latLngAltitude {
latitude = 37.8080
longitude = -122.4177
altitude = 0.0
}
tilt = 45.0
heading = 340.0
range = 18500.0
roll = 0.0
}

/**
* Curated language and region presets for the Maps Localization sample.
*/
enum class MapLocalePreset(
val displayName: String,
val nativeLabel: String,
val language: String,
val region: String,
) {
ENGLISH_US(
displayName = "English (US)",
nativeLabel = "en-US",
language = "en",
region = "US",
),
CHINESE_CHINA(
displayName = "Chinese (China)",
nativeLabel = "中文 (zh-CN)",
language = "zh",
region = "CN",
),
FRENCH_FRANCE(
displayName = "French (France)",
nativeLabel = "Français (fr-FR)",
language = "fr",
region = "FR",
),
JAPANESE_JAPAN(
displayName = "Japanese (Japan)",
nativeLabel = "日本語 (ja-JP)",
language = "ja",
region = "JP",
),
SPANISH_SPAIN(
displayName = "Spanish (Spain)",
nativeLabel = "Español (es-ES)",
language = "es",
region = "ES",
),
ARABIC_SAUDI_ARABIA(
displayName = "Arabic (Saudi Arabia)",
nativeLabel = "العربية (ar-SA)",
language = "ar",
region = "SA",
),
GERMAN_GERMANY(
displayName = "German (Germany)",
nativeLabel = "Deutsch (de-DE)",
language = "de",
region = "DE",
),
SYSTEM_DEFAULT(
displayName = "System Default",
nativeLabel = SYSTEM_DEFAULT_CODE,
language = Locale.getDefault().language,
region = Locale.getDefault().country,
)
Comment on lines +91 to +96

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In MapLocalePreset.SYSTEM_DEFAULT, language and region are evaluated once at class initialization time as constructor arguments (Locale.getDefault().language / country). If a user changes their device system language in Android Settings while the app process is cached in memory, this enum constant will retain the stale initial locale.

Consider making language and region dynamic custom property accessors on MapLocalePreset so that selecting SYSTEM_DEFAULT always queries Locale.getDefault() dynamically:

// [START maps_android_3d_localization_preset_dynamic_locale]
enum class MapLocalePreset(
    val displayName: String,
    val nativeLabel: String,
    private val rawLanguage: String? = null,
    private val rawRegion: String? = null,
) {
    ENGLISH_US("English (US)", "en-US", "en", "US"),
    CHINESE_CHINA("Chinese (China)", "中文 (zh-CN)", "zh", "CN"),
    FRENCH_FRANCE("French (France)", "Français (fr-FR)", "fr", "FR"),
    JAPANESE_JAPAN("Japanese (Japan)", "日本語 (ja-JP)", "ja", "JP"),
    SPANISH_SPAIN("Spanish (Spain)", "Español (es-ES)", "es", "ES"),
    ARABIC_SAUDI_ARABIA("Arabic (Saudi Arabia)", "العربية (ar-SA)", "ar", "SA"),
    GERMAN_GERMANY("German (Germany)", "Deutsch (de-DE)", "de", "DE"),
    SYSTEM_DEFAULT("System Default", SYSTEM_DEFAULT_CODE);

    val language: String
        get() = rawLanguage ?: Locale.getDefault().language

    val region: String
        get() = rawRegion ?: Locale.getDefault().country
}
// [END maps_android_3d_localization_preset_dynamic_locale]

}

/**
* Immutable UI state for the Maps Localization sample across Kotlin Views, Java Views, and Compose.
*
* @property selectedPreset Currently active [MapLocalePreset].
* @property languageCode Active ISO-639 language code (`"en"`, `"zh"`, `"fr"`, etc.).
* @property regionCode Active ISO-3166-1 alpha-2 region code (`"US"`, `"CN"`, `"FR"`, etc.).
* @property mapMode Current [Map3DMode] (`HYBRID` or `ROADMAP` to display localized labels).
* @property camera Target [Camera] position.
*/
data class LocalizationState(
val selectedPreset: MapLocalePreset = MapLocalePreset.ENGLISH_US,
val languageCode: String = selectedPreset.language,
val regionCode: String = selectedPreset.region,
@param:Map3DMode val mapMode: Int = Map3DMode.HYBRID,
val camera: Camera = DEFAULT_LOCALIZATION_CAMERA,
) {
/** Formatted locale badge text (e.g. `"zh-CN"` or `"default (en_US)"`). */
val localeBadgeText: String
get() = if (selectedPreset == MapLocalePreset.SYSTEM_DEFAULT) {
"default (${Locale.getDefault()})"
} else {
"$languageCode-$regionCode"
}

/**
* Builds a [LocaleOptions] instance matching the current [languageCode] and [regionCode]
* for runtime locale updates via [com.google.android.gms.maps3d.GoogleMap3D.setLocale].
*/
fun toLocaleOptions(): LocaleOptions = LocaleOptions().apply {
language = languageCode
region = regionCode
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* 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 androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import com.google.android.gms.maps3d.model.Map3DMode
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow

/**
* Shared Architecture ViewModel for Maps Localization across Kotlin Views, Java Views, and
* Jetpack Compose.
*
* Exposes immutable [LocalizationState] via [StateFlow] (for Kotlin/Compose) and [LiveData]
* (for Java Views via [asLiveData]).
*/
class MapsLocalizationViewModel(
initialPreset: MapLocalePreset = MapLocalePreset.ENGLISH_US,
) : ViewModel() {

private val controller = LocalizationController(initialPreset)

private val _uiState = MutableStateFlow(controller.getState())
val uiState: StateFlow<LocalizationState> = _uiState.asStateFlow()

val localizationStateData: LiveData<LocalizationState> = _uiState.asLiveData()

val currentState: LocalizationState
get() = _uiState.value

/** Selects a [MapLocalePreset] and updates the language and region. */
fun selectPreset(preset: MapLocalePreset) {
_uiState.value = controller.selectPreset(preset)
}

/** Switches between [Map3DMode.HYBRID] and [Map3DMode.ROADMAP] to inspect localized labels. */
fun setMapMode(@Map3DMode mapMode: Int) {
_uiState.value = controller.setMapMode(mapMode)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<androidx.cardview.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/control_panel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="12dp"
app:cardCornerRadius="16dp"
app:cardElevation="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">

<!-- Card Header with title and collapse/expand button -->
<LinearLayout
android:id="@+id/card_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingTop="8dp"
android:paddingEnd="8dp"
android:paddingBottom="4dp">

<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/localization_controls_title"
android:textStyle="bold"
style="@style/TextAppearance.Material3.TitleSmall" />

<com.google.android.material.button.MaterialButton
android:id="@+id/btn_collapse"
style="?attr/materialIconButtonStyle"
android:layout_width="36dp"
android:layout_height="36dp"
android:contentDescription="@string/collapse_controls"
android:padding="0dp"
app:icon="@drawable/expand_more_24px"
app:iconSize="20dp"
app:iconTint="?attr/colorOnSurfaceVariant" />
</LinearLayout>

<!-- Collapsible Content Section -->
<LinearLayout
android:id="@+id/card_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingBottom="10dp">

<!-- Compact Language Selection Dropdown Button -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_change_language"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="40dp"
android:ellipsize="end"
android:insetTop="0dp"
android:insetBottom="0dp"
android:minHeight="40dp"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:singleLine="true"
android:text="@string/change_language_button"
android:textAlignment="viewStart"
android:textSize="13sp"
app:cornerRadius="8dp"
app:icon="@drawable/arrow_drop_down_24px"
app:iconGravity="end" />

<!-- Location displayed below the language change button -->
<TextView
android:id="@+id/tv_location"
style="@style/TextAppearance.Material3.BodySmall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:paddingVertical="2dp"
android:text="@string/localization_location"
android:textColor="?android:attr/textColorSecondary" />

<!-- Map Mode Selection (Hybrid vs Roadmap for inspecting localized labels) -->
<RadioGroup
android:id="@+id/rg_localization_map_mode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:gravity="center_vertical"
android:orientation="horizontal">

<RadioButton
android:id="@+id/rb_mode_hybrid"
android:layout_width="0dp"
android:layout_height="36dp"
android:layout_weight="1"
android:checked="true"
android:text="@string/hybrid" />

<RadioButton
android:id="@+id/rb_mode_roadmap"
android:layout_width="0dp"
android:layout_height="36dp"
android:layout_weight="1"
android:text="@string/roadmap" />
</RadioGroup>
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
Loading
Loading