summaryrefslogtreecommitdiff
path: root/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapGestureDetector.java
diff options
context:
space:
mode:
Diffstat (limited to 'platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapGestureDetector.java')
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapGestureDetector.java125
1 files changed, 121 insertions, 4 deletions
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 cf2d20179f..2b6c1fdd01 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
@@ -8,6 +8,8 @@ import android.graphics.PointF;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.util.Pair;
import android.view.InputDevice;
import android.view.MotionEvent;
import android.view.animation.DecelerateInterpolator;
@@ -26,6 +28,7 @@ import com.mapbox.mapboxsdk.camera.CameraPosition;
import com.mapbox.mapboxsdk.constants.MapboxConstants;
import com.mapbox.mapboxsdk.constants.TelemetryConstants;
import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.geometry.LatLngBounds;
import com.mapbox.mapboxsdk.utils.MathUtils;
import java.util.ArrayList;
@@ -34,6 +37,7 @@ import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
+import static com.mapbox.mapboxsdk.constants.MapboxConstants.ANIMATION_DURATION_FLING_BASE;
import static com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraMoveStartedListener.REASON_API_ANIMATION;
import static com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraMoveStartedListener.REASON_API_GESTURE;
@@ -42,6 +46,9 @@ import static com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraMoveStartedListener.RE
*/
final class MapGestureDetector {
+ @VisibleForTesting
+ static final int BOUND_REPEL_RATIO = 1000;
+
private final Transform transform;
private final Projection projection;
private final UiSettings uiSettings;
@@ -108,6 +115,18 @@ final class MapGestureDetector {
}
}
+ @VisibleForTesting
+ MapGestureDetector(Transform transform, Projection projection, UiSettings uiSettings,
+ AnnotationManager annotationManager, CameraChangeDispatcher cameraChangeDispatcher,
+ AndroidGesturesManager androidGesturesManager) {
+ this.annotationManager = annotationManager;
+ this.transform = transform;
+ this.projection = projection;
+ this.uiSettings = uiSettings;
+ this.cameraChangeDispatcher = cameraChangeDispatcher;
+ this.gesturesManager = androidGesturesManager;
+ }
+
private void initializeGestureListeners(@NonNull Context context, boolean attachDefaultListeners) {
if (attachDefaultListeners) {
StandardGestureListener standardGestureListener = new StandardGestureListener();
@@ -412,11 +431,42 @@ final class MapGestureDetector {
// tilt results in a bigger translation, limiting input for #5281
double tilt = transform.getTilt();
double tiltFactor = 1.5 + ((tilt != 0) ? (tilt / 10) : 0);
- double offsetX = velocityX / tiltFactor / screenDensity;
- double offsetY = velocityY / tiltFactor / screenDensity;
+ float offsetX = (float) (velocityX / tiltFactor / screenDensity);
+ float offsetY = (float) (velocityY / tiltFactor / screenDensity);
+
+ double animationTimeDivider = 1;
+
+ LatLngBounds bounds = transform.getLatLngBounds();
+ if (bounds != null) {
+ // camera movement is limited by bounds, calculate the acceptable offset that doesn't cross the bounds
+ Pair<Float, Float> offset = findBoundsLimitedOffset(projection, transform, bounds, -offsetX, -offsetY);
+ if (offset != null) {
+ float targetOffsetX = -offset.first;
+ float targetOffsetY = -offset.second;
+
+ // check how much did we cut from the original offset and shorten animation time by the same ratio
+ float offsetDiffRatioX = 0f;
+ if (targetOffsetX != 0) {
+ offsetDiffRatioX = offsetX / targetOffsetX;
+ }
+ float offsetDiffRatioY = 0f;
+ if (targetOffsetY != 0) {
+ offsetDiffRatioY = offsetY / targetOffsetY;
+ }
+
+ // pick the highest ratio as a divider
+ animationTimeDivider = offsetDiffRatioX > offsetDiffRatioY ? offsetDiffRatioX : offsetDiffRatioY;
+ // default to 1 if necessary
+ animationTimeDivider = animationTimeDivider > 1 ? animationTimeDivider : 1;
+
+ offsetX = targetOffsetX;
+ offsetY = targetOffsetY;
+ }
+ }
- // calculate animation time based on displacement
- long animationTime = (long) (velocityXY / 7 / tiltFactor + MapboxConstants.ANIMATION_DURATION_FLING_BASE);
+ // calculate animation time based on displacement and make it shorter if we've hit the bound
+ long animationTime =
+ (long) ((velocityXY / 7 / tiltFactor + ANIMATION_DURATION_FLING_BASE) / animationTimeDivider);
// update transformation
transform.moveBy(offsetX, offsetY, animationTime);
@@ -445,6 +495,16 @@ final class MapGestureDetector {
// dispatching camera start event only when the movement actually occurred
cameraChangeDispatcher.onCameraMoveStarted(CameraChangeDispatcher.REASON_API_GESTURE);
+ LatLngBounds bounds = transform.getLatLngBounds();
+ if (bounds != null) {
+ // camera movement is limited by bounds, calculate the acceptable offset that doesn't cross the bounds
+ Pair<Float, Float> offset = findBoundsLimitedOffset(projection, transform, bounds, distanceX, distanceY);
+ if (offset != null) {
+ distanceX = offset.first;
+ distanceY = offset.second;
+ }
+ }
+
// Scroll the map
transform.moveBy(-distanceX, -distanceY, 0 /*no duration*/);
@@ -914,6 +974,63 @@ final class MapGestureDetector {
return mapZoom >= MapboxConstants.MINIMUM_ZOOM && mapZoom <= MapboxConstants.MAXIMUM_ZOOM;
}
+ /**
+ * Finds acceptable camera offset based on the bounds that are limiting the camera movement.
+ * <p>
+ * To find the offset, we're hit testing all bounding box edges against the segment that is created from the current
+ * screen center and a target calculated from the provided offset.
+ * Because we are converting LatLng to unwrapped screen coordinates, we are going to find only one collision, or none,
+ * and return an acceptable offset based on the distance from the original point to bounds intersection,
+ * or the original target.
+ */
+ @Nullable
+ @VisibleForTesting
+ static Pair<Float, Float> findBoundsLimitedOffset(@NonNull Projection projection,
+ @NonNull Transform transform,
+ @NonNull LatLngBounds bounds,
+ float offsetX, float offsetY) {
+ // because core returns number with big precision, LatLngBounds#contains can fail when using raw values
+ LatLng coreScreenCenter = transform.getCenterCoordinate(false);
+ LatLng screenCenter = new LatLng(
+ MathUtils.round(coreScreenCenter.getLatitude(), 6),
+ MathUtils.round(coreScreenCenter.getLongitude(), 6));
+ if (!bounds.contains(screenCenter) || bounds.equals(LatLngBounds.world())) {
+ // return when screen center is not within bounds or the camera is not limited
+ return null;
+ }
+
+ PointF referencePixel = projection.toScreenLocationRaw(screenCenter);
+ PointF nwPixel = projection.toScreenLocationRaw(bounds.getNorthWest());
+ PointF nePixel = projection.toScreenLocationRaw(bounds.getNorthEast());
+ PointF sePixel = projection.toScreenLocationRaw(bounds.getSouthEast());
+ PointF swPixel = projection.toScreenLocationRaw(bounds.getSouthWest());
+
+ Pair[] edges = new Pair[] {
+ new Pair<>(nwPixel, nePixel), // north edge
+ new Pair<>(nePixel, sePixel), // east edge
+ new Pair<>(sePixel, swPixel), // south edge
+ new Pair<>(swPixel, nwPixel) // west edge
+ };
+
+ // to prevent the screen center from being permanently attracted to the bound we need to push the origin
+ // so that it isn't a part of the bound's edge segment, otherwise, we'd have a constant collision
+ PointF boundsCenter = projection.toScreenLocationRaw(bounds.getCenter());
+ PointF toCenterVector = new PointF(boundsCenter.x - referencePixel.x, boundsCenter.y - referencePixel.y);
+ referencePixel.offset(toCenterVector.x / BOUND_REPEL_RATIO, toCenterVector.y / BOUND_REPEL_RATIO);
+ PointF target = new PointF(referencePixel.x + offsetX, referencePixel.y + offsetY);
+
+ for (Pair edge : edges) {
+ PointF intersection =
+ MathUtils.findSegmentsIntersection(referencePixel, target, (PointF) edge.first, (PointF) edge.second);
+ if (intersection != null) {
+ // returning a limited offset vector
+ return new Pair<>(intersection.x - referencePixel.x, intersection.y - referencePixel.y);
+ }
+ }
+ // no collisions, returning the original offset vector
+ return new Pair<>(target.x - referencePixel.x, target.y - referencePixel.y);
+ }
+
void notifyOnMapClickListeners(@NonNull PointF tapPoint) {
for (MapboxMap.OnMapClickListener listener : onMapClickListenerList) {
if (listener.onMapClick(projection.fromScreenLocation(tapPoint))) {