Skip to content
This repository has been archived by the owner on Aug 8, 2023. It is now read-only.

Commit

Permalink
Hit test Marker and MarkerViews (#9424)
Browse files Browse the repository at this point in the history
* [android] - hit test Marker and MarkerViews

* fixup
  • Loading branch information
tobrun committed Jul 20, 2017
1 parent 24a05f0 commit bfe29c1
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 74 deletions.
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -19,7 +22,6 @@
import com.mapbox.mapboxsdk.annotations.PolylineOptions;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import timber.log.Timber;
Expand Down Expand Up @@ -55,7 +57,7 @@ class AnnotationManager {
this.markerViewManager = markerViewManager;
if (view != null) {
// null checking needed for unit tests
nativeMapView.addOnMapChangedListener(markerViewManager);
view.addOnMapChangedListener(markerViewManager);
}
}

Expand Down Expand Up @@ -616,84 +618,135 @@ void reloadMarkers() {
// 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<Marker> 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<Annotation> 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<Marker> markers;

MarkerHit(RectF tapRect, List<Marker> markers) {
this.tapRect = tapRect;
this.markers = markers;
}

float getTapPointX() {
return tapRect.centerX();
}

float getTapPointY() {
return tapRect.centerY();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import java.util.ArrayList;
import java.util.List;

import timber.log.Timber;

/**
* Responsible for managing icons added to the Map.
* <p>
Expand Down Expand Up @@ -105,6 +107,8 @@ private void updateAverageIconSize(int width, int height) {
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ public boolean onSingleTapUp(MotionEvent motionEvent) {
@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()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M19,5v14H5V5h14m0,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z"/>
</vector>

0 comments on commit bfe29c1

Please sign in to comment.