diff --git a/ApiDemos/java/app/build.gradle.kts b/ApiDemos/java/app/build.gradle.kts index 2230fae8..ccb62802 100644 --- a/ApiDemos/java/app/build.gradle.kts +++ b/ApiDemos/java/app/build.gradle.kts @@ -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) diff --git a/ApiDemos/java/app/src/main/AndroidManifest.xml b/ApiDemos/java/app/src/main/AndroidManifest.xml index 1f982433..229771d1 100644 --- a/ApiDemos/java/app/src/main/AndroidManifest.xml +++ b/ApiDemos/java/app/src/main/AndroidManifest.xml @@ -92,10 +92,12 @@ limitations under the License. android:label="@string/circle_demo_label" /> 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. + *

+ * 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. + *

+ * 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). + *

+ * This factory provides a semi-transparent fill color for each area level 1 feature. + *

+ * @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. + *

+ * *Note:* If the set of selected countries changes, this function must be called to update the + * styling. + *

+ * @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. + *

+ * 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 + *

+ */ @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 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 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 getPlaceIds(List features) { List 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] \ No newline at end of file diff --git a/ApiDemos/java/app/src/main/java/com/example/mapdemo/DataDrivenDatasetStylingActivity.java b/ApiDemos/java/app/src/main/java/com/example/mapdemo/DataDrivenDatasetStylingActivity.java index f5f0f2c4..89c57194 100644 --- a/ApiDemos/java/app/src/main/java/com/example/mapdemo/DataDrivenDatasetStylingActivity.java +++ b/ApiDemos/java/app/src/main/java/com/example/mapdemo/DataDrivenDatasetStylingActivity.java @@ -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 diff --git a/ApiDemos/java/app/src/main/res/drawable/baseline_arrow_drop_down_24.xml b/ApiDemos/java/app/src/main/res/drawable/baseline_arrow_drop_down_24.xml new file mode 100644 index 00000000..e830b0ff --- /dev/null +++ b/ApiDemos/java/app/src/main/res/drawable/baseline_arrow_drop_down_24.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/ApiDemos/java/app/src/main/res/layout/data_driven_boundaries_demo.xml b/ApiDemos/java/app/src/main/res/layout/data_driven_boundaries_demo.xml index 04780ce9..74797f61 100644 --- a/ApiDemos/java/app/src/main/res/layout/data_driven_boundaries_demo.xml +++ b/ApiDemos/java/app/src/main/res/layout/data_driven_boundaries_demo.xml @@ -14,18 +14,38 @@ limitations under the License. --> - - + android:layout_height="match_parent" + android:fitsSystemWindows="true" + > + + + + 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" /> - -