fix: support devices with cutouts for DataDrivenDatasetStylingActivity.java demo (#1979)

* fix: support devices with cutouts for DataDrivenDatasetStylingActivity.java demo

* fix: remove unnecessary 'res/' from paths in README.md file

* feat(apidemos): enhance DataDrivenBoundariesActivity UI and boundary selection

This commit enhances the DataDrivenBoundariesActivity demo with UI and
functionality improvements focusing on boundary type selection and visual
enhancements.

- Implements Material Design theming and UI elements.
- Adds boundary type selection via PopupMenu (Locality, Admin Area, Country).
- Refactors styling and implements persistent country selection.
- Handles system UI insets for improved display.

These changes improve the demo's user experience and code structure,
better showcasing data-driven boundary styling.

* feat(apidemos): configure dataset-specific zoom levels for datasets demo

This commit introduces dataset-specific zoom levels to the DataDrivenDatasetStylingActivity, enhancing the user experience when switching between datasets.

- Adds a `zoomLevel` field to the `DataSet` class to store the desired zoom level for each dataset.
- Updates the `dataSets` array to include appropriate zoom levels for Boulder, New York, and Kyoto datasets.
- Modifies the `centerMapOnLocation` method to accept a `zoomLevel` parameter, allowing it to be dynamically set.
- Updates the `switchDataSet` method to utilize the `zoomLevel` from the selected `DataSet` when centering the map, ensuring the map zooms to the optimal level for each dataset.
- Removes the previously hardcoded `ZOOM_LEVEL` constant, as the zoom level is now dataset-dependent.

These changes ensure that when a user selects a dataset, the map automatically zooms to a relevant level for that specific dataset, improving clarity and usability of the demo.

Also adds missing copyright header.
This commit is contained in:
Dale Hawkins 2025-02-14 15:51:21 -07:00 committed by GitHub
parent 45d85ac59c
commit 683b21da11
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 475 additions and 224 deletions

View File

@ -58,8 +58,9 @@ dependencies {
implementation(libs.appcompat)
implementation(libs.recyclerview)
implementation(libs.volley)
implementation(platform(libs.kotlinBom))
implementation(libs.playServicesMaps)
implementation(libs.material)
implementation(libs.activity)
// Tests
testImplementation(libs.junit)

View File

@ -92,10 +92,12 @@ limitations under the License.
android:label="@string/circle_demo_label" />
<activity
android:name=".DataDrivenBoundariesActivity"
android:theme="@style/MaterialAppTheme"
android:exported="true"
android:label="@string/data_driven_boundaries_label" />
<activity
android:name=".DataDrivenDatasetStylingActivity"
android:theme="@style/MaterialAppTheme"
android:exported="true"
android:label="@string/data_driven_styling_label" />
<activity

View File

@ -41,7 +41,6 @@ public class ApiDemoApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "onCreate called");
checkApiKey();
}

View File

@ -13,13 +13,23 @@
// limitations under the License.
package com.example.mapdemo;
import static java.lang.Math.round;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.RequiresApi;
import androidx.activity.EdgeToEdge;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.PopupMenu;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
@ -34,9 +44,12 @@ import com.google.android.gms.maps.model.FeatureType;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.MapCapabilities;
import com.google.android.gms.maps.model.PlaceFeature;
import com.google.android.material.button.MaterialButton;
import java.util.ArrayList;
import java.util.List;
import java.util.HashSet;
import java.util.Set;
/**
* This sample showcases how to use the Data-driven styling for boundaries. For more information
@ -45,7 +58,11 @@ import java.util.List;
*/
// [START maps_android_data_driven_styling_boundaries]
public class DataDrivenBoundariesActivity extends AppCompatActivity implements OnMapReadyCallback,
FeatureLayer.OnFeatureClickListener {
FeatureLayer.OnFeatureClickListener, PopupMenu.OnMenuItemClickListener {
private static final String TAG = DataDrivenBoundariesActivity.class.getName();
private static final LatLng HANA_HAWAII = new LatLng(20.7522, -155.9877); // Hana, Hawaii
private static final LatLng CENTER_US = new LatLng(39.8283, -98.5795); // Approximate geographical center of the contiguous US
private GoogleMap map;
@ -53,73 +70,146 @@ public class DataDrivenBoundariesActivity extends AppCompatActivity implements O
private FeatureLayer areaLevel1Layer = null;
private FeatureLayer countryLayer = null;
private static final String TAG = DataDrivenBoundariesActivity.class.getName();
private final FeatureLayer.StyleFactory localityStyleFactory = getLocalityStyleFactory();
private final FeatureLayer.StyleFactory countryStyleFactory = getCountryStyleFactory();
private final FeatureLayer.StyleFactory areaLevel1StyleFactory = getAreaLevel1StyleFactory();
private static final LatLng HANA_HAWAII = new LatLng(20.7522, -155.9877); // Hana, Hawaii
private static final LatLng CENTER_US = new LatLng(39.8283, -98.5795); // Approximate geographical center of the contiguous US
// Which layers are currently enabled
private boolean localityEnabled = true;
private boolean adminAreaEnabled = false;
private boolean countryEnabled = false;
private final Set<String> selectedPlaceIds = new HashSet<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.data_driven_boundaries_demo);
// [START_EXCLUDE silent]
if (getString(R.string.map_id).equals("DEMO_MAP_ID")) {
// This demo will not work if the map id is not set.
Toast.makeText(this, "Map ID is not set. See README for instructions.", Toast.LENGTH_LONG).show();
}
// [END_EXCLUDE]
SupportMapFragment mapFragment = (SupportMapFragment) getSupportFragmentManager().findFragmentById(R.id.map);
if (mapFragment != null) {
mapFragment.getMapAsync(this);
}
findViewById(R.id.button_hawaii).setOnClickListener(view -> centerMapOnLocation(HANA_HAWAII, 13.5f));
findViewById(R.id.button_hawaii).setOnClickListener(view -> centerMapOnLocation(HANA_HAWAII, 11f));
findViewById(R.id.button_us).setOnClickListener(view -> centerMapOnLocation(CENTER_US, 1f));
applyInsets(findViewById(R.id.map_container));
// [START_EXCLUDE silent]
setupBoundarySelectorButton();
// [END_EXCLUDE]
}
// [START_EXCLUDE silent]
private void setupBoundarySelectorButton() {
MaterialButton stylingTypeButton = findViewById(R.id.button_feature_type);
stylingTypeButton.setOnClickListener(v -> {
PopupMenu popupMenu = new PopupMenu(this, v);
MenuInflater inflater = popupMenu.getMenuInflater();
inflater.inflate(R.menu.boundary_types_menu, popupMenu.getMenu());
popupMenu.setOnMenuItemClickListener(this);
popupMenu.getMenu().findItem(R.id.boundary_type_locality).setChecked(localityEnabled);
popupMenu.getMenu().findItem(R.id.boundary_type_administrative_area_level_1).setChecked(adminAreaEnabled);
popupMenu.getMenu().findItem(R.id.boundary_type_country).setChecked(countryEnabled);
popupMenu.show();
});
}
// [END_EXCLUDE]
private void centerMapOnLocation(LatLng location, float zoomLevel) {
map.moveCamera(CameraUpdateFactory.newLatLngZoom(location, zoomLevel));
}
@RequiresApi(Build.VERSION_CODES.O)
@Override
public void onMapReady(GoogleMap googleMap) {
public void onMapReady(@NonNull GoogleMap googleMap) {
this.map = googleMap;
MapCapabilities capabilities = map.getMapCapabilities();
Log.d(TAG, "Data-driven Styling is available: " + capabilities.isDataDrivenStylingAvailable());
// Get the LOCALITY feature layer.
if (!capabilities.isDataDrivenStylingAvailable()) {
Toast.makeText(
this,
"Data-driven Styling is not available. See README.md for instructions.",
Toast.LENGTH_LONG
).show();
}
// Gets the LOCALITY feature layer.
localityLayer = googleMap.getFeatureLayer(
new FeatureLayerOptions.Builder()
.featureType(FeatureType.LOCALITY)
.build()
);
// Apply style factory function to LOCALITY layer.
styleLocalityLayer();
// Get the ADMINISTRATIVE_AREA_LEVEL_1 feature layer.
// Gets the ADMINISTRATIVE_AREA_LEVEL_1 feature layer.
areaLevel1Layer = googleMap.getFeatureLayer(
new FeatureLayerOptions.Builder()
.featureType(FeatureType.ADMINISTRATIVE_AREA_LEVEL_1)
.build()
);
// Apply style factory function to ADMINISTRATIVE_AREA_LEVEL_1 layer.
styleAreaLevel1Layer();
// Get the COUNTRY feature layer.
// Gets the COUNTRY feature layer.
countryLayer = googleMap.getFeatureLayer(
new FeatureLayerOptions.Builder()
.featureType(FeatureType.COUNTRY)
.build()
);
// Register the click event handler for the Country layer.
countryLayer.addOnFeatureClickListener(this);
// Apply default style to all countries on load to enable clicking.
styleCountryLayer();
centerMapOnLocation(HANA_HAWAII, 11f);
// Apply the current set of styles.
updateStyles();
}
private void styleLocalityLayer() {
// Create the style factory function.
FeatureLayer.StyleFactory styleFactory = feature -> {
/**
* Updates the styles of the locality, area level 1, and country layers based on the current
* state of the `localityEnabled`, `adminAreaEnabled`, and `countryEnabled` flags.
* <p>
* For each layer, if the corresponding flag is true, the layer's features will be styled using
* the layer specific style factory function.
*/
private void updateStyles() {
if (localityLayer != null && areaLevel1Layer != null && countryLayer != null) {
localityLayer.setFeatureStyle(localityEnabled ? localityStyleFactory : null);
areaLevel1Layer.setFeatureStyle(adminAreaEnabled ? areaLevel1StyleFactory : null);
if (countryEnabled) {
countryLayer.setFeatureStyle(countryStyleFactory);
} else {
countryLayer.setFeatureStyle(null);
}
}
}
/**
* Creates a StyleFactory for a FeatureLayer that styles Hana, HI on its Place ID.
* <p>
* This method defines a style factory that checks if a given feature is a {@link PlaceFeature}.
* and if that feature matches "ChIJ0zQtYiWsVHkRk8lRoB1RNPo" (Hana, HI) applies a specific style.
* Otherwise, it returns null, indicating no specific styling is applied.
*
* @return A {@link FeatureLayer.StyleFactory} instance that can be used to style features in a FeatureLayer.
* The factory returns a {@link FeatureStyle} for Hana, HI, and null for other features.
*/
private static FeatureLayer.StyleFactory getLocalityStyleFactory() {
int purple = 0x810FCB;
// Define a style with purple fill at 50% opacity and
// solid purple border.
int fillColor = setAlphaValueOnColor(purple, 0.5f);
int strokeColor = setAlphaValueOnColor(purple, 1f);
return feature -> {
// Check if the feature is an instance of PlaceFeature,
// which contains a place ID.
if (feature instanceof PlaceFeature placeFeature) {
@ -129,24 +219,26 @@ public class DataDrivenBoundariesActivity extends AppCompatActivity implements O
// Use FeatureStyle.Builder to configure the FeatureStyle object
// returned by the style factory function.
return new FeatureStyle.Builder()
// Define a style with purple fill at 50% opacity and
// solid purple border.
.fillColor(0x80810FCB)
.strokeColor(0xFF810FCB)
.fillColor(fillColor)
.strokeColor(strokeColor)
.build();
}
}
return null;
};
// Apply the style factory function to the feature layer.
if (localityLayer != null) {
localityLayer.setFeatureStyle(styleFactory);
}
}
private void styleAreaLevel1Layer() {
FeatureLayer.StyleFactory styleFactory = feature -> {
/**
* Creates a StyleFactory for area level 1 features (e.g., states, provinces).
* <p>
* This factory provides a semi-transparent fill color for each area level 1 feature.
* <p>
* @return A StyleFactory that can be used to style area level 1 features on a map.
*/
private static FeatureLayer.StyleFactory getAreaLevel1StyleFactory() {
int alpha = (int) (255 * 0.25);
return feature -> {
if (feature instanceof PlaceFeature placeFeature) {
// Return a hueColor in the range [-299,299]. If the value is
@ -157,64 +249,61 @@ public class DataDrivenBoundariesActivity extends AppCompatActivity implements O
}
return new FeatureStyle.Builder()
// Set the fill color for the state based on the hashed hue color.
.fillColor(Color.HSVToColor(150, new float[]{hueColor, 1f, 1f}))
.fillColor(Color.HSVToColor(alpha, new float[]{hueColor, 1f, 1f}))
.build();
}
return null;
};
// Apply the style factory function to the feature layer.
if (areaLevel1Layer != null) {
areaLevel1Layer.setFeatureStyle(styleFactory);
}
}
// Set default fill and border for all countries to ensure that they respond
// to click events.
@RequiresApi(Build.VERSION_CODES.O)
private void styleCountryLayer() {
FeatureLayer.StyleFactory styleFactory = feature -> new FeatureStyle.Builder()
// Set the fill color for the country as white with a 10% opacity.
// This requires minApi 26
.fillColor(Color.argb(0.1f, 0f, 0f, 0f))
// Set border color to solid black.
.strokeColor(Color.BLACK)
.build();
// Apply the style factory function to the country feature layer.
if (countryLayer != null) {
countryLayer.setFeatureStyle(styleFactory);
}
/**
* Creates a StyleFactory for styling country features on a FeatureLayer highlighting selected
* countries. Selection is determined via the selectedPlaceIds set.
* <p>
* *Note:* If the set of selected countries changes, this function must be called to update the
* styling.
* <p>
* @return A FeatureLayer.StyleFactory that can be used to style country features.
*/
private FeatureLayer.StyleFactory getCountryStyleFactory() {
int defaultFillColor = setAlphaValueOnColor(Color.BLACK, 0.1f);
int selectedFillColor = setAlphaValueOnColor(Color.RED, 0.33f);
return feature -> {
if (feature instanceof PlaceFeature) {
int fillColor = selectedPlaceIds.contains(((PlaceFeature) feature).getPlaceId()) ? selectedFillColor : defaultFillColor;
FeatureStyle.Builder build = new FeatureStyle.Builder();
return build.fillColor(fillColor).strokeColor(Color.BLACK).build();
}
return null;
};
}
// Define the click event handler.
/**
* Called when a feature is clicked on the map. It is only applied to the country layer.
* <p>
* Each time a country is clicked, its place ID is added to the selectedPlaceIds set or removed
* if it was already present. Each time the set is
* <p>
*/
@Override
public void onFeatureClick(FeatureClickEvent event) {
public void onFeatureClick(@NonNull FeatureClickEvent event) {
// Get the list of features affected by the click using
// getPlaceIds() defined below.
List<String> selectedPlaceIds = getPlaceIds(event.getFeatures());
if (!selectedPlaceIds.isEmpty()) {
FeatureLayer.StyleFactory styleFactory = feature -> {
// Use PlaceFeature to get the placeID of the country.
if (feature instanceof PlaceFeature) {
if (selectedPlaceIds.contains(((PlaceFeature) feature).getPlaceId())) {
return new FeatureStyle.Builder()
// Set the fill color to red.
.fillColor(Color.RED)
.build();
}
}
return null;
};
List<String> newSelectedPlaceIds = getPlaceIds(event.getFeatures());
// Apply the style factory function to the feature layer.
if (countryLayer != null) {
countryLayer.setFeatureStyle(styleFactory);
for (String placeId : newSelectedPlaceIds) {
if (selectedPlaceIds.contains(placeId)) {
selectedPlaceIds.remove(placeId);
} else {
selectedPlaceIds.add(placeId);
}
}
// Reset the feature styling
countryLayer.setFeatureStyle(countryStyleFactory);
}
// Get a List of place IDs from the FeatureClickEvent object.
// Gets a List of place IDs from the FeatureClickEvent object.
private List<String> getPlaceIds(List<Feature> features) {
List<String> placeIds = new ArrayList<>();
for (Feature feature : features) {
@ -224,5 +313,59 @@ public class DataDrivenBoundariesActivity extends AppCompatActivity implements O
}
return placeIds;
}
private static int setAlphaValueOnColor(int color, float alpha) {
return (color & 0x00ffffff) | (round(alpha * 255) << 24);
}
/**
* Applies insets to the container view to properly handle window insets.
*
* @param container the container view to apply insets to
*/
private static void applyInsets(View container) {
ViewCompat.setOnApplyWindowInsetsListener(container,
(view, insets) -> {
Insets innerPadding = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
view.setPadding(innerPadding.left, innerPadding.top, innerPadding.right, innerPadding.bottom);
return insets;
}
);
}
/**
* Handles the click events for menu items in the boundary type selection menu.
* This method is called when a user selects a boundary type (locality, administrative area, or country) from the menu.
* It toggles the checked state of the selected menu item and updates the corresponding boolean flags (localityEnabled, adminAreaEnabled, countryEnabled).
* Finally, it calls the {@link #updateStyles()} method to reflect the changes in the map's display.
*
* @param item The MenuItem that was clicked.
* @return True if the event was handled, false otherwise. In this case it always return true if one of the correct items was selected.
*/ // [START_EXCLUDE silent]
@Override
public boolean onMenuItemClick(MenuItem item) {
Log.d(TAG, "onMenuItemClick: " + item.getItemId());
int id = item.getItemId();
boolean result = false;
if (id == R.id.boundary_type_locality) {
item.setChecked(!item.isChecked());
localityEnabled = item.isChecked();
result = true;
} else if (id == R.id.boundary_type_administrative_area_level_1) {
item.setChecked(!item.isChecked());
adminAreaEnabled = item.isChecked();
result = true;
} else if (id == R.id.boundary_type_country) {
item.setChecked(!item.isChecked());
countryEnabled = item.isChecked();
result = true;
}
updateStyles();
return result;
}
// [END_EXCLUDE]
}
// [END maps_android_data_driven_styling_boundaries]

View File

@ -16,12 +16,17 @@ package com.example.mapdemo;
import android.graphics.Color;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import androidx.activity.EdgeToEdge;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.ColorUtils;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
@ -54,6 +59,7 @@ public class DataDrivenDatasetStylingActivity extends AppCompatActivity implemen
String label,
String datasetId,
LatLng location,
float zoomLevel,
DataDrivenDatasetStylingActivity.DataSet.StylingCallback callback) {
public interface StylingCallback {
void styleDatasetLayer();
@ -81,9 +87,9 @@ public class DataDrivenDatasetStylingActivity extends AppCompatActivity implemen
* Note: We have use the secrets plugin to allow us to configure the Dataset IDs in our secrets.properties file.
*/
private final DataSet[] dataSets = new DataSet[] {
new DataSet("Boulder", BuildConfig.BOULDER_DATASET_ID, new LatLng(40.0150, -105.2705), this::styleBoulderDatasetLayer),
new DataSet("New York", BuildConfig.NEW_YORK_DATASET_ID, new LatLng(40.786244, -73.962684), this::styleNYCDatasetLayer),
new DataSet("Kyoto", BuildConfig.KYOTO_DATASET_ID, new LatLng(35.005081, 135.764385), this::styleKyotoDatasetsLayer),
new DataSet("Boulder", BuildConfig.BOULDER_DATASET_ID, new LatLng(40.0150, -105.2705), 11f, this::styleBoulderDatasetLayer),
new DataSet("New York", BuildConfig.NEW_YORK_DATASET_ID, new LatLng(40.786244, -73.962684), 14f, this::styleNYCDatasetLayer),
new DataSet("Kyoto", BuildConfig.KYOTO_DATASET_ID, new LatLng(35.005081, 135.764385), 13.5f, this::styleKyotoDatasetsLayer),
};
private DataSet findDataSetByLabel(String label) {
@ -95,7 +101,6 @@ public class DataDrivenDatasetStylingActivity extends AppCompatActivity implemen
return null; // Return null if no match is found
}
private static final float ZOOM_LEVEL = 13.5f;
private static final String TAG = DataDrivenDatasetStylingActivity.class.getName();
private static FeatureLayer datasetLayer = null;
private GoogleMap map;
@ -104,8 +109,16 @@ public class DataDrivenDatasetStylingActivity extends AppCompatActivity implemen
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.data_driven_styling_demo);
// [START_EXCLUDE silent]
if (getString(R.string.map_id).equals("DEMO_MAP_ID")) {
// This demo will not work if the map id is not set.
Toast.makeText(this, "Map ID is not set. See README for instructions.", Toast.LENGTH_LONG).show();
}
// [END_EXCLUDE]
SupportMapFragment mapFragment = (SupportMapFragment) getSupportFragmentManager().findFragmentById(R.id.map);
if (mapFragment != null) {
mapFragment.getMapAsync(this);
@ -115,6 +128,18 @@ public class DataDrivenDatasetStylingActivity extends AppCompatActivity implemen
for (int buttonId : buttonIds) {
findViewById(buttonId).setOnClickListener(view -> switchDataSet(((Button) view).getText().toString()));
}
applyInsets(findViewById(R.id.map_container));
}
private static void applyInsets(View container) {
ViewCompat.setOnApplyWindowInsetsListener(container,
(view, insets) -> {
Insets innerPadding = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
view.setPadding(innerPadding.left, innerPadding.top, innerPadding.right, innerPadding.bottom);
return insets;
}
);
}
/**
@ -142,7 +167,7 @@ public class DataDrivenDatasetStylingActivity extends AppCompatActivity implemen
.build()
);
dataSet.callback.styleDatasetLayer();
centerMapOnLocation(dataSet.location());
centerMapOnLocation(dataSet.location(), dataSet.zoomLevel());
}
}
@ -152,6 +177,13 @@ public class DataDrivenDatasetStylingActivity extends AppCompatActivity implemen
MapCapabilities capabilities = map.getMapCapabilities();
Log.d(TAG, "Data-driven Styling is available: " + capabilities.isDataDrivenStylingAvailable());
if (!capabilities.isDataDrivenStylingAvailable()) {
Toast.makeText(
this,
"Data-driven Styling is not available. See README.md for instructions.",
Toast.LENGTH_LONG
).show();
}
// Switch to the default dataset which must happen before adding the feature click listener
switchDataSet("Boulder");
@ -270,8 +302,8 @@ public class DataDrivenDatasetStylingActivity extends AppCompatActivity implemen
// Create the style factory function.
FeatureLayer.StyleFactory styleFactory = feature -> {
// Set default colors to yellow and point radius to 8.
int fillColor = Color.GREEN;
int strokeColor = Color.YELLOW;
int fillColor;
int strokeColor;
float pointRadius = 8F;
float strokeWidth = 3F;
@ -323,8 +355,8 @@ public class DataDrivenDatasetStylingActivity extends AppCompatActivity implemen
}
private void centerMapOnLocation(LatLng location) {
map.moveCamera(CameraUpdateFactory.newLatLngZoom(location, ZOOM_LEVEL));
private void centerMapOnLocation(LatLng location, float zoomLevel) {
map.moveCamera(CameraUpdateFactory.newLatLngZoom(location, zoomLevel));
}
@Override

View File

@ -0,0 +1,21 @@
<!--
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M7,10l5,5 5,-5z"/>
</vector>

View File

@ -14,18 +14,38 @@
limitations under the License.
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:map="http://schemas.android.com/apk/res-auto"
android:id="@+id/map_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- Map Fragment -->
android:layout_height="match_parent"
android:fitsSystemWindows="true"
>
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/top_bar"
android:layout_width="0dp"
android:layout_height="?attr/actionBarSize"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:title="@string/data_driven_boundaries_label"
app:titleTextColor="?attr/colorOnPrimary"
style="@style/Widget.MaterialComponents.Toolbar.Primary"
/>
<fragment
android:id="@+id/map"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_width="0dp"
android:layout_height="0dp"
map:backgroundColor="#fff0b2dd"
map:mapId="@string/map_id"
class="com.google.android.gms.maps.SupportMapFragment" />
class="com.google.android.gms.maps.SupportMapFragment"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/top_bar" />
<!-- Buttons to center the map -->
<LinearLayout
@ -34,20 +54,33 @@
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:orientation="horizontal"
android:padding="16dp">
<Button
android:padding="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/top_bar"
>
<com.google.android.material.button.MaterialButton
android:id="@+id/button_hawaii"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hawaii" />
android:layout_margin="4dp"
android:text="@string/hawaii" />
<Button
<com.google.android.material.button.MaterialButton
android:id="@+id/button_us"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="US" />
android:layout_margin="4dp"
android:text="@string/united_states" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_feature_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableEnd="@drawable/baseline_arrow_drop_down_24"
android:layout_margin="4dp"
android:text="@string/boundary_type" />
</LinearLayout>
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -14,46 +14,68 @@
limitations under the License.
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:map="http://schemas.android.com/apk/res-auto"
android:id="@+id/map_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- Map Fragment -->
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/top_bar"
android:layout_width="0dp"
android:layout_height="?attr/actionBarSize"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:title="@string/data_driven_styling_label"
app:titleTextColor="?attr/colorOnPrimary"
style="@style/Widget.MaterialComponents.Toolbar.Primary"
/>
<fragment
android:id="@+id/map"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_width="0dp"
android:layout_height="0dp"
map:backgroundColor="#fff0b2dd"
map:mapId="@string/map_id"
class="com.google.android.gms.maps.SupportMapFragment" />
class="com.google.android.gms.maps.SupportMapFragment"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/top_bar" />
<!-- Buttons to center the map -->
<LinearLayout
android:id="@+id/button_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:orientation="horizontal"
android:padding="16dp">
android:padding="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/top_bar">
<Button
<com.google.android.material.button.MaterialButton
android:id="@+id/button_boulder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Boulder" />
android:layout_margin="4dp"
android:text="@string/boulder" />
<Button
<com.google.android.material.button.MaterialButton
android:id="@+id/button_ny"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="New York" />
android:layout_margin="4dp"
android:text="@string/new_york" />
<Button
<com.google.android.material.button.MaterialButton
android:id="@+id/button_kyoto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Kyoto" />
android:layout_margin="4dp"
android:text="@string/kyoto" />
</LinearLayout>
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://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.
-->
<menu xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<group android:checkableBehavior="all">
<item
android:id="@+id/boundary_type_locality"
android:title="@string/locality"
app:showAsAction="never"
android:checked="false"
android:checkable="true"
/>
<item
android:id="@+id/boundary_type_administrative_area_level_1"
android:title="@string/administrative_area_level_1"
android:titleCondensed="Admin1"
app:showAsAction="never"
android:checked="false"
android:checkable="true"
/>
<item
android:id="@+id/boundary_type_country"
android:title="@string/country"
app:showAsAction="never"
android:checked="false"
android:checkable="true"
/>
</group>
</menu>

View File

@ -301,4 +301,17 @@
<!-- Data Driven Boundaries Demo -->
<string name="data_driven_boundaries_label">Data Driven Boundaries Demo</string>
<string name="data_driven_boundaries_description">Demonstrates how to use Data Driven styling for boundaries.</string>
<!-- Data Driven Dataset Styling -->
<string name="kyoto">Kyoto</string>
<string name="new_york">New York</string>
<string name="boulder">Boulder</string>
<!-- Data Driven Boundaries Styling -->
<string name="hawaii">Hawaii</string>
<string name="united_states">US</string>
<string name="locality">Locality</string>
<string name="administrative_area_level_1">Administrative Area Level 1</string>
<string name="country">Country</string>
<string name="boundary_type">Boundary</string>
</resources>

View File

@ -23,4 +23,9 @@
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="MaterialAppTheme" parent="Theme.Material3.DynamicColors.DayNight.NoActionBar">
<!-- Customize your theme here. -->
</style>
</resources>

View File

@ -1,20 +1,21 @@
[versions]
activity = "1.10.0"
androidxJunit = "1.2.1"
appcompat = "1.7.0"
espresso = "3.6.1"
junit = "4.13.2"
material = "1.12.0"
playServicesMaps = "19.0.0"
recyclerview = "1.4.0"
volley = "1.2.1"
kotlinBom = "2.0.21"
playServicesMaps = "19.0.0"
junit = "4.13.2"
androidxJunit = "1.2.1"
espresso = "3.6.1"
[libraries]
appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerview" }
volley = { group = "com.android.volley", name = "volley", version.ref = "volley" }
kotlinBom = { group = "org.jetbrains.kotlin", name = "kotlin-bom", version.ref = "kotlinBom" }
playServicesMaps = { group = "com.google.android.gms", name = "play-services-maps", version.ref = "playServicesMaps" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
activity = { module = "androidx.activity:activity", version.ref = "activity" }
androidxJunit = { group = "androidx.test.ext", name = "junit", version.ref = "androidxJunit" }
espressoCore = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso" }
appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
espressoCore = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
playServicesMaps = { group = "com.google.android.gms", name = "play-services-maps", version.ref = "playServicesMaps" }
recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerview" }
volley = { group = "com.android.volley", name = "volley", version.ref = "volley" }

View File

@ -28,7 +28,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:title="@string/app_name"
app:title="@string/data_driven_styling_label"
app:titleTextColor="?attr/colorOnPrimary"
style="@style/Widget.MaterialComponents.Toolbar.Primary"
/>

View File

@ -10,110 +10,43 @@ polygons) based on the data associated with those features.
* **`boulder_polylines.geojson`:** This GeoJSON file contains data about trails in Boulder,
Colorado. Each feature in the file represents a trail, and the geometry is likely a polyline (a
series of connected points). The file might also include attributes like trail name, difficulty,
series of connected points). The file also includes attributes like trail name, difficulty,
and length. This data can be used to create a map where trails are displayed as lines, and the
appearance of the lines (color, thickness, pattern) can be customized based on the trail's
attributes.
* **`kyoto_polygons.geojson`:** This GeoJSON file contains data about temples in Kyoto, Japan. Each
feature in the file represents a temple, and the geometry is likely a polygon (an enclosed area).
The file might also include attributes like temple name, founding date, and architectural style.
This data can be used to create a map where temples are displayed as polygons, and the appearance
of the polygons (fill color, border color) can be customized based on the temple's attributes."
feature in the file represents a temple, and the geometry is a polygon (an enclosed area).
**Uploading Data to Google Cloud Console and Using it with Google Maps:**
To use this data with data-driven styling in Google Maps, you'll need to upload it to the Google
Cloud Console and create a dataset. Here's a general outline of the process:
1. **Go to the Google Cloud Console:** Open the Google Cloud Console in your web browser.
## 1. Create a Map ID
To create a new map ID, follow the steps in [Create a map ID](https://developers.google.com/maps/documentation/get-map-id#create-a-map-id).
Make sure to set the **Map type** to **Android**.
2. **Create a Project (If Needed):** If you don't already have a project, create one.
## 2. Create a New Map Style
Follow the instructions in [Manage map styles](https://developers.google.com/maps/documentation/android-sdk/cloud-customization/map-styles) to create a new style and [associate it with the map ID you just created](https://developers.google.com/maps/documentation/android-sdk/cloud-customization/map-styles#associate-style-with-map-id).
3. **Enable the Maps Platform:** Make sure the Maps Platform is enabled for your project.
## 3. Upload a Dataset
To include data-driven styling in your map:
4. **Go to the "Datasets" page:** In the Cloud Console, navigate to the "Datasets" page (you can
search for it).
1. Upload the dataset on the [Google Maps Platform Datasets](https://console.cloud.google.com/google/maps-apis/datasets) page.
2. Confirm that the dataset upload is successful (sometimes there can be issues due to an invalid structure).
5. **Create a Dataset:** Click "Create Dataset" and choose the appropriate type based on your data (
e.g., "Point dataset" for `new_york_points.csv`, "Polyline dataset" for
`boulder_polylines.geojson`, "Polygon dataset" for `kyoto_polygons.geojson`).
## 4. Link the Dataset to the Map Style
To enable data-driven styling:
6. **Upload Your Data:** Follow the instructions to upload your data file. You might need to specify
the data format (CSV or GeoJSON) and how to interpret the columns (e.g., which column contains
latitude and longitude).
1. Open your dataset in the [Google Maps Platform Datasets](https://console.cloud.google.com/google/maps-apis/datasets) page.
2. Click on the **Preview** of the dataset.
3. Associate the dataset with one of the previously created styles
7. **Create a Dataset ID:** Once the data is uploaded, you'll get a Dataset ID. This is a unique
identifier for your dataset.
8. **Use the Dataset ID in Your Code:** In your Android code, when you create a
`FeatureLayerOptions` object, use the `datasetId()` method to set the Dataset ID. This will link
your map layer to the dataset you uploaded.
9. **Apply Data-Driven Styling:** Use the `StyleFactory` to define how to style the features in your
layer based on their attributes. The `StyleFactory` provides access to the feature's properties,
which you can use to determine the appropriate styling.
**Important Notes:**
* **Data Preparation:** Make sure your data is properly formatted (CSV or GeoJSON) and that it
includes the necessary columns (latitude, longitude, and any attributes you want to use for
styling).
* **Dataset Types:** Choose the correct dataset type when creating the dataset in the Cloud Console.
This will ensure that the data is interpreted correctly.
* **Dataset IDs:** Keep track of your Dataset IDs, as you'll need them to link your map layers to
the correct datasets.
* **Styling:** Data-driven styling is very powerful. You can create complex and dynamic map styles
based on your data. Refer to the Google Maps documentation for more information and examples.
By following these steps, you can upload your data to the Google Cloud Console, create datasets, and
use those datasets to create beautiful and informative maps with data-driven styling in your Android
application.
## Using Dataset Styles in Android
This guide explains how to create and apply dataset styles in Android. You'll learn how to create a map style, associate it with a map ID, upload datasets, and link the datasets to the map style to enable data-driven styling.
### Sample Datasets
This directory contains sample datasets for demonstrating data-driven styling:
* **`new_york_points.csv`:** This CSV file contains data about squirrel sightings in New York City. Each row represents a sighting, and the columns likely include information like location (latitude and longitude), date, and other details about the squirrel (e.g., color, size). This data can be used to create a map where each squirrel sighting is represented by a point, and the appearance of the point (color, icon) can be customized based on the squirrel's attributes.
* **`boulder_polylines.geojson`:** This GeoJSON file contains data about trails in Boulder, Colorado. Each feature in the file represents a trail, and the geometry is likely a polyline (a series of connected points). The file might also include attributes like trail name, difficulty, and length. This data can be used to create a map where trails are displayed as lines, and the appearance of the lines (color, thickness, pattern) can be customized based on the trail's attributes.
* **`kyoto_polygons.geojson`:** This GeoJSON file contains data about temples in Kyoto, Japan. Each feature in the file represents a temple, and the geometry is likely a polygon (an enclosed area). The file might also include attributes like temple name, founding date, and architectural style. This data can be used to create a map where temples are displayed as polygons, and the appearance of the polygons (fill color, border color) can be customized based on the temple's attributes.
### Steps to Enable Data-Driven Styling
1. **Create a Map ID:**
* Follow the steps in [Create a map ID](https://developers.google.com/maps/documentation/get-map-id#create-a-map-id). Make sure to set the **Map type** to **Android**.
2. **Create a New Map Style:**
* Follow the instructions in [Manage map styles](https://developers.google.com/maps/documentation/android-sdk/cloud-customization/map-styles) to create a new style.
* [Associate the new style with the map ID you just created](https://developers.google.com/maps/documentation/android-sdk/cloud-customization/map-styles#associate-style-with-map-id).
3. **Upload a Dataset:**
* Go to the [Google Maps Platform Datasets](https://console.cloud.google.com/google/maps-apis/datasets) page in the Google Cloud Console.
* Click "Create Dataset" and choose the appropriate type based on your data (e.g., "Point dataset" for `new_york_points.csv`, "Polyline dataset" for `boulder_polylines.geojson`, "Polygon dataset" for `kyoto_polygons.geojson`).
* Upload your data file, specifying the data format (CSV or GeoJSON) and how to interpret the columns.
* Confirm that the dataset upload is successful.
4. **Link the Dataset to the Map Style:**
* On the [Datasets](https://console.cloud.google.com/google/maps-apis/datasets) page, click on the **Preview** of your dataset.
* Associate the dataset with one of the map styles you created.
**Important Notes:**
* **Data Preparation:** Ensure your data is properly formatted (CSV or GeoJSON) and includes the necessary columns (latitude, longitude, and any attributes you want to use for styling).
* **Dataset Types:** Choose the correct dataset type when creating the dataset.
* **Dataset IDs:** You'll need the Dataset ID to link your map layers to the correct datasets in your Android code.
* **Styling:** Use the `StyleFactory` in your Android code to define how to style features based on their attributes.
By following these steps, you can upload your data, create datasets, and use them to create informative maps with data-driven styling in your Android application.
## 5. Set the Dataset ID in the secrets.properties file
```
BOULDER_DATASET_ID=<BOULDER_DATASET_ID>
NEW_YORK_DATASET_ID=<NEW_YORK_DATASET_ID>
KYOTO_DATASET_ID=<KYOTO_DATASET_ID>
```
** Important: ** the map ID set in the strings.xml file must match the maps ID associated with the style.