From 2876db72f2d235a02e5670329c4f8dcb2a65a8ed Mon Sep 17 00:00:00 2001 From: Tobrun Date: Fri, 7 Jul 2017 12:28:19 +0200 Subject: Hit test Marker and MarkerViews (#9424) * [android] - hit test Marker and MarkerViews * fixup --- .../mapbox/mapboxsdk/maps/AnnotationManager.java | 201 +++++++++++++-------- .../com/mapbox/mapboxsdk/maps/IconManager.java | 4 + .../mapbox/mapboxsdk/maps/MapGestureDetector.java | 2 +- .../com/mapbox/mapboxsdk/maps/MarkerContainer.java | 1 + .../src/main/res/drawable/ic_check_box.xml | 9 + 5 files changed, 141 insertions(+), 76 deletions(-) create mode 100644 platform/android/MapboxGLAndroidSDKTestApp/src/main/res/drawable/ic_check_box.xml diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/AnnotationManager.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/AnnotationManager.java index 7e7947047e..f3573c78ba 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/AnnotationManager.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/AnnotationManager.java @@ -1,10 +1,13 @@ package com.mapbox.mapboxsdk.maps; +import android.graphics.Bitmap; import android.graphics.PointF; +import android.graphics.Rect; import android.graphics.RectF; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.util.LongSparseArray; +import android.view.View; import com.mapbox.mapboxsdk.annotations.Annotation; import com.mapbox.mapboxsdk.annotations.BaseMarkerOptions; @@ -18,7 +21,6 @@ import com.mapbox.mapboxsdk.annotations.Polyline; import com.mapbox.mapboxsdk.annotations.PolylineOptions; import java.util.ArrayList; -import java.util.Collections; import java.util.List; /** @@ -34,7 +36,6 @@ import java.util.List; */ class AnnotationManager { - private final NativeMapView nativeMapView; private final MapView mapView; private final IconManager iconManager; private final InfoWindowManager infoWindowManager = new InfoWindowManager(); @@ -52,7 +53,6 @@ class AnnotationManager { AnnotationManager(NativeMapView view, MapView mapView, LongSparseArray annotationsArray, MarkerViewManager markerViewManager, IconManager iconManager, Annotations annotations, Markers markers, Polygons polygons, Polylines polylines) { - this.nativeMapView = view; this.mapView = mapView; this.annotationsArray = annotationsArray; this.markerViewManager = markerViewManager; @@ -63,7 +63,7 @@ class AnnotationManager { this.polylines = polylines; if (view != null) { // null checking needed for unit tests - nativeMapView.addOnMapChangedListener(markerViewManager); + view.addOnMapChangedListener(markerViewManager); } } @@ -329,84 +329,135 @@ class AnnotationManager { // Click event // - boolean onTap(PointF tapPoint, float screenDensity) { - float toleranceSides = 4 * screenDensity; - float toleranceTopBottom = 10 * screenDensity; - boolean handledDefaultClick = false; - - RectF tapRect = new RectF(tapPoint.x - iconManager.getAverageIconWidth() / 2 - toleranceSides, - tapPoint.y - iconManager.getAverageIconHeight() / 2 - toleranceTopBottom, - tapPoint.x + iconManager.getAverageIconWidth() / 2 + toleranceSides, - tapPoint.y + iconManager.getAverageIconHeight() / 2 + toleranceTopBottom); - - List nearbyMarkers = getMarkersInRect(tapRect); - long newSelectedMarkerId = -1; - - // find a Marker that isn't selected yet - if (nearbyMarkers.size() > 0) { - Collections.sort(nearbyMarkers); - for (Marker nearbyMarker : nearbyMarkers) { - boolean found = false; - for (Marker selectedMarker : selectedMarkers) { - if (selectedMarker.equals(nearbyMarker)) { - found = true; - } - } - if (!found) { - newSelectedMarkerId = nearbyMarker.getId(); - break; + boolean onTap(PointF tapPoint) { + MarkerHit markerHit = getMarkerHitFromTouchArea(tapPoint); + long markerId = new MarkerHitResolver(markerViewManager, mapboxMap.getProjection()).execute(markerHit); + return markerId >= 0 && isClickHandledForMarker(markerId); + } + + private MarkerHit getMarkerHitFromTouchArea(PointF tapPoint) { + int averageIconWidthOffset = iconManager.getAverageIconWidth() / 2; + int averageIconHeightOffset = iconManager.getAverageIconHeight() / 2; + final RectF tapRect = new RectF(tapPoint.x - averageIconWidthOffset, + tapPoint.y - averageIconHeightOffset, + tapPoint.x + averageIconWidthOffset, + tapPoint.y + averageIconHeightOffset + ); + return new MarkerHit(tapRect, getMarkersInRect(tapRect)); + } + + private boolean isClickHandledForMarker(long markerId) { + boolean handledDefaultClick; + Marker marker = (Marker) getAnnotation(markerId); + if (marker instanceof MarkerView) { + handledDefaultClick = markerViewManager.onClickMarkerView((MarkerView) marker); + } else { + handledDefaultClick = onClickMarker(marker); + } + + if (!handledDefaultClick) { + setMarkerSelectionState(marker); + } + return true; + } + + private boolean onClickMarker(Marker marker) { + return onMarkerClickListener != null && onMarkerClickListener.onMarkerClick(marker); + } + + private void setMarkerSelectionState(Marker marker) { + if (!selectedMarkers.contains(marker)) { + selectMarker(marker); + } else { + deselectMarker(marker); + } + } + + private static class MarkerHitResolver { + + private final MarkerViewManager markerViewManager; + private final Projection projection; + + private View view; + private Bitmap bitmap; + private PointF markerLocation; + + private Rect hitRectView = new Rect(); + private RectF hitRectMarker = new RectF(); + private RectF highestSurfaceIntersection = new RectF(); + + private long closestMarkerId = -1; + + MarkerHitResolver(@NonNull MarkerViewManager markerViewManager, @NonNull Projection projection) { + this.markerViewManager = markerViewManager; + this.projection = projection; + } + + public long execute(MarkerHit markerHit) { + resolveForMarkers(markerHit); + return closestMarkerId; + } + + private void resolveForMarkers(MarkerHit markerHit) { + for (Marker marker : markerHit.markers) { + if (marker instanceof MarkerView) { + resolveForMarkerView(markerHit, (MarkerView) marker); + } else { + resolveForMarker(markerHit, marker); } } } - // if unselected marker found - if (newSelectedMarkerId >= 0) { - List annotations = getAnnotations(); - int count = annotations.size(); - for (int i = 0; i < count; i++) { - Annotation annotation = annotations.get(i); - if (annotation instanceof Marker) { - if (annotation.getId() == newSelectedMarkerId) { - Marker marker = (Marker) annotation; - - if (marker instanceof MarkerView) { - handledDefaultClick = markerViewManager.onClickMarkerView((MarkerView) marker); - } else { - if (onMarkerClickListener != null) { - // end developer has provided a custom click listener - handledDefaultClick = onMarkerClickListener.onMarkerClick(marker); - } - } - - if (!handledDefaultClick) { - // only select marker if user didn't handle the click event themselves - selectMarker(marker); - } - - return true; - } - } + private void resolveForMarkerView(MarkerHit markerHit, MarkerView markerView) { + view = markerViewManager.getView(markerView); + if (view != null) { + view.getHitRect(hitRectView); + hitRectMarker = new RectF(hitRectView); + hitTestMarker(markerHit, markerView, hitRectMarker); } - } else if (nearbyMarkers.size() > 0) { - // we didn't find an unselected marker, check if we can close an already open markers - for (Marker nearbyMarker : nearbyMarkers) { - for (Marker selectedMarker : selectedMarkers) { - if (nearbyMarker.equals(selectedMarker)) { - if (nearbyMarker instanceof MarkerView) { - handledDefaultClick = markerViewManager.onClickMarkerView((MarkerView) nearbyMarker); - } else if (onMarkerClickListener != null) { - handledDefaultClick = onMarkerClickListener.onMarkerClick(nearbyMarker); - } - - if (!handledDefaultClick) { - // only deselect marker if user didn't handle the click event themselves - deselectMarker(nearbyMarker); - } - return true; - } + } + + private void resolveForMarker(MarkerHit markerHit, Marker marker) { + markerLocation = projection.toScreenLocation(marker.getPosition()); + bitmap = marker.getIcon().getBitmap(); + hitRectMarker.set(0, 0, bitmap.getWidth(), bitmap.getHeight()); + hitRectMarker.offsetTo( + markerLocation.x - bitmap.getWidth() / 2, + markerLocation.y - bitmap.getHeight() / 2 + ); + hitTestMarker(markerHit, marker, hitRectMarker); + } + + private void hitTestMarker(MarkerHit markerHit, Marker marker, RectF hitRectMarker) { + if (hitRectMarker.contains(markerHit.getTapPointX(), markerHit.getTapPointY())) { + hitRectMarker.intersect(markerHit.tapRect); + if (isRectangleHighestSurfaceIntersection(hitRectMarker)) { + highestSurfaceIntersection = new RectF(hitRectMarker); + closestMarkerId = marker.getId(); } } } - return false; + + private boolean isRectangleHighestSurfaceIntersection(RectF rectF) { + return rectF.width() * rectF.height() > highestSurfaceIntersection.width() * highestSurfaceIntersection.height(); + } + } + + private static class MarkerHit { + private final RectF tapRect; + private final List markers; + + MarkerHit(RectF tapRect, List markers) { + this.tapRect = tapRect; + this.markers = markers; + } + + float getTapPointX() { + return tapRect.centerX(); + } + + float getTapPointY() { + return tapRect.centerY(); + } } } diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/IconManager.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/IconManager.java index 9f4171aee8..b5da5f59aa 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/IconManager.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/IconManager.java @@ -12,6 +12,8 @@ import com.mapbox.mapboxsdk.exceptions.IconBitmapChangedException; import java.util.ArrayList; import java.util.List; +import timber.log.Timber; + /** * Responsible for managing icons added to the Map. *

@@ -105,6 +107,8 @@ class IconManager { int iconSize = icons.size() + 1; averageIconHeight = averageIconHeight + (height - averageIconHeight) / iconSize; averageIconWidth = averageIconWidth + (width - averageIconWidth) / iconSize; + Timber.e("OnUpdateAverageSizeIcon with: %s %s", width, height); + Timber.e("OnUpdateAverageSizeIcon now: %s %s", averageIconWidth, averageIconHeight); } private void loadIcon(Icon icon) { diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapGestureDetector.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapGestureDetector.java index 17eb506b97..330e09cf32 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapGestureDetector.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapGestureDetector.java @@ -315,7 +315,7 @@ final class MapGestureDetector { @Override public boolean onSingleTapConfirmed(MotionEvent motionEvent) { PointF tapPoint = new PointF(motionEvent.getX(), motionEvent.getY()); - boolean tapHandled = annotationManager.onTap(tapPoint, uiSettings.getPixelRatio()); + boolean tapHandled = annotationManager.onTap(tapPoint); if (!tapHandled) { if (uiSettings.isDeselectMarkersOnTap()) { diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MarkerContainer.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MarkerContainer.java index 306ad59b8d..ef8054d70e 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MarkerContainer.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MarkerContainer.java @@ -261,6 +261,7 @@ class MarkerContainer implements Markers { if (icon == null) { icon = IconFactory.getInstance(mapView.getContext()).defaultMarkerView(); } + iconManager.loadIconForMarkerView(marker); marker.setIcon(icon); return marker; } diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/drawable/ic_check_box.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/drawable/ic_check_box.xml new file mode 100644 index 0000000000..cf8bfa24b5 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/drawable/ic_check_box.xml @@ -0,0 +1,9 @@ + + + -- cgit v1.2.1