diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationCameraController.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationCameraController.java index 13b61dfec..e00cc3884 100644 --- a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationCameraController.java +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationCameraController.java @@ -2,11 +2,13 @@ import android.content.Context; import android.graphics.PointF; +import android.graphics.RectF; import android.location.Location; +import android.view.MotionEvent; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import android.view.MotionEvent; import com.mapbox.android.gestures.AndroidGesturesManager; import com.mapbox.android.gestures.MoveGestureDetector; @@ -314,6 +316,7 @@ private void adjustGesturesThresholds() { moveGestureDetector.setMoveThreshold(options.trackingInitialMoveThreshold()); } else { moveGestureDetector.setMoveThreshold(0f); + moveGestureDetector.setMoveThresholdRect(null); } } } @@ -359,17 +362,43 @@ private void notifyCameraTrackingChangeListener(boolean wasTracking) { @Override public void onMoveBegin(@NonNull MoveGestureDetector detector) { - if (options.trackingGesturesManagement() - && detector.getPointersCount() > 1 - && detector.getMoveThreshold() != options.trackingMultiFingerMoveThreshold() - && isLocationTracking()) { - detector.setMoveThreshold(options.trackingMultiFingerMoveThreshold()); - interrupt = true; + if (options.trackingGesturesManagement() && isLocationTracking()) { + if (detector.getPointersCount() > 1) { + applyMultiFingerThresholdArea(detector); + applyMultiFingerMoveThreshold(detector); + } else { + applySingleFingerMoveThreshold(detector); + } } else { setCameraMode(CameraMode.NONE); } } + private void applyMultiFingerThresholdArea(@NonNull MoveGestureDetector detector) { + RectF currentRect = detector.getMoveThresholdRect(); + if (currentRect != null && !currentRect.equals(options.trackingMultiFingerProtectedMoveArea())) { + detector.setMoveThresholdRect(options.trackingMultiFingerProtectedMoveArea()); + interrupt = true; + } else if (currentRect == null && options.trackingMultiFingerProtectedMoveArea() != null) { + detector.setMoveThresholdRect(options.trackingMultiFingerProtectedMoveArea()); + interrupt = true; + } + } + + private void applyMultiFingerMoveThreshold(@NonNull MoveGestureDetector detector) { + if (detector.getMoveThreshold() != options.trackingMultiFingerMoveThreshold()) { + detector.setMoveThreshold(options.trackingMultiFingerMoveThreshold()); + interrupt = true; + } + } + + private void applySingleFingerMoveThreshold(@NonNull MoveGestureDetector detector) { + if (detector.getMoveThreshold() != options.trackingInitialMoveThreshold()) { + detector.setMoveThreshold(options.trackingInitialMoveThreshold()); + interrupt = true; + } + } + @Override public void onMove(@NonNull MoveGestureDetector detector) { if (interrupt) { @@ -387,6 +416,7 @@ public void onMove(@NonNull MoveGestureDetector detector) { public void onMoveEnd(@NonNull MoveGestureDetector detector) { if (options.trackingGesturesManagement() && !interrupt && isLocationTracking()) { detector.setMoveThreshold(options.trackingInitialMoveThreshold()); + detector.setMoveThresholdRect(null); } interrupt = false; } diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentOptions.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentOptions.java index e5b56865d..86602ffd4 100644 --- a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentOptions.java +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentOptions.java @@ -3,8 +3,10 @@ import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; +import android.graphics.RectF; import android.os.Parcel; import android.os.Parcelable; + import androidx.annotation.ColorInt; import androidx.annotation.Dimension; import androidx.annotation.DrawableRes; @@ -109,6 +111,8 @@ public class LocationComponentOptions implements Parcelable { private boolean trackingGesturesManagement; private float trackingInitialMoveThreshold; private float trackingMultiFingerMoveThreshold; + @Nullable + private RectF trackingMultiFingerProtectedMoveArea; private String layerAbove; private String layerBelow; private float trackingAnimationDurationMultiplier; @@ -144,6 +148,7 @@ public LocationComponentOptions( boolean trackingGesturesManagement, float trackingInitialMoveThreshold, float trackingMultiFingerMoveThreshold, + RectF trackingMultiFingerProtectedMoveArea, String layerAbove, String layerBelow, float trackingAnimationDurationMultiplier, @@ -180,6 +185,7 @@ public LocationComponentOptions( this.trackingGesturesManagement = trackingGesturesManagement; this.trackingInitialMoveThreshold = trackingInitialMoveThreshold; this.trackingMultiFingerMoveThreshold = trackingMultiFingerMoveThreshold; + this.trackingMultiFingerProtectedMoveArea = trackingMultiFingerProtectedMoveArea; this.layerAbove = layerAbove; this.layerBelow = layerBelow; this.trackingAnimationDurationMultiplier = trackingAnimationDurationMultiplier; @@ -665,6 +671,7 @@ public float minZoomIconScale() { * @return true if gestures are adjusted when in one of the camera tracking modes, false otherwise * @see Builder#trackingInitialMoveThreshold(float) * @see Builder#trackingMultiFingerMoveThreshold(float) + * @see Builder#trackingMultiFingerProtectedMoveArea(RectF) */ public boolean trackingGesturesManagement() { return trackingGesturesManagement; @@ -688,6 +695,20 @@ public float trackingMultiFingerMoveThreshold() { return trackingMultiFingerMoveThreshold; } + /** + * Protected multi pointer gesture area. When the camera is in a tracking mode, any multi finger gesture with focal + * point inside the provided screen coordinate rectangle is not going to break the tracking. + *
+ * Best paired with the {@link LocationComponentOptions.Builder#trackingMultiFingerMoveThreshold(float)}
+ * set to 0 or a relatively small value to not interfere with gestures outside of the defined rectangle.
+ *
+ * @return the protected multi finger area while camera is tracking
+ */
+ @Nullable
+ public RectF trackingMultiFingerProtectedMoveArea() {
+ return trackingMultiFingerProtectedMoveArea;
+ }
+
/**
* Gets the id of the layer that's referenced when placing the component on the map using
* {@link com.mapbox.mapboxsdk.maps.Style#addLayerAbove(Layer, String)}.
@@ -772,6 +793,7 @@ public String toString() {
+ "trackingGesturesManagement=" + trackingGesturesManagement + ", "
+ "trackingInitialMoveThreshold=" + trackingInitialMoveThreshold + ", "
+ "trackingMultiFingerMoveThreshold=" + trackingMultiFingerMoveThreshold + ", "
+ + "trackingMultiFingerProtectedMoveArea=" + trackingMultiFingerProtectedMoveArea + ", "
+ "layerAbove=" + layerAbove
+ "layerBelow=" + layerBelow
+ "trackingAnimationDurationMultiplier=" + trackingAnimationDurationMultiplier
@@ -840,6 +862,11 @@ public boolean equals(Object o) {
if (Float.compare(options.trackingAnimationDurationMultiplier, trackingAnimationDurationMultiplier) != 0) {
return false;
}
+ if (trackingMultiFingerProtectedMoveArea != null
+ ? !trackingMultiFingerProtectedMoveArea.equals(options.trackingMultiFingerProtectedMoveArea) :
+ options.trackingMultiFingerProtectedMoveArea != null) {
+ return false;
+ }
if (compassAnimationEnabled != options.compassAnimationEnabled) {
return false;
}
@@ -927,6 +954,8 @@ public int hashCode() {
? Float.floatToIntBits(trackingInitialMoveThreshold) : 0);
result = 31 * result + (trackingMultiFingerMoveThreshold != +0.0f
? Float.floatToIntBits(trackingMultiFingerMoveThreshold) : 0);
+ result = 31 * result + (trackingMultiFingerProtectedMoveArea != null
+ ? trackingMultiFingerProtectedMoveArea.hashCode() : 0);
result = 31 * result + (layerAbove != null ? layerAbove.hashCode() : 0);
result = 31 * result + (layerBelow != null ? layerBelow.hashCode() : 0);
result = 31 * result + (trackingAnimationDurationMultiplier != +0.0f
@@ -936,45 +965,86 @@ public int hashCode() {
return result;
}
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeFloat(this.accuracyAlpha);
+ dest.writeInt(this.accuracyColor);
+ dest.writeInt(this.backgroundDrawableStale);
+ dest.writeString(this.backgroundStaleName);
+ dest.writeInt(this.foregroundDrawableStale);
+ dest.writeString(this.foregroundStaleName);
+ dest.writeInt(this.gpsDrawable);
+ dest.writeString(this.gpsName);
+ dest.writeInt(this.foregroundDrawable);
+ dest.writeString(this.foregroundName);
+ dest.writeInt(this.backgroundDrawable);
+ dest.writeString(this.backgroundName);
+ dest.writeInt(this.bearingDrawable);
+ dest.writeString(this.bearingName);
+ dest.writeValue(this.bearingTintColor);
+ dest.writeValue(this.foregroundTintColor);
+ dest.writeValue(this.backgroundTintColor);
+ dest.writeValue(this.foregroundStaleTintColor);
+ dest.writeValue(this.backgroundStaleTintColor);
+ dest.writeFloat(this.elevation);
+ dest.writeByte(this.enableStaleState ? (byte) 1 : (byte) 0);
+ dest.writeLong(this.staleStateTimeout);
+ dest.writeIntArray(this.padding);
+ dest.writeFloat(this.maxZoomIconScale);
+ dest.writeFloat(this.minZoomIconScale);
+ dest.writeByte(this.trackingGesturesManagement ? (byte) 1 : (byte) 0);
+ dest.writeFloat(this.trackingInitialMoveThreshold);
+ dest.writeFloat(this.trackingMultiFingerMoveThreshold);
+ dest.writeParcelable(this.trackingMultiFingerProtectedMoveArea, flags);
+ dest.writeString(this.layerAbove);
+ dest.writeString(this.layerBelow);
+ dest.writeFloat(this.trackingAnimationDurationMultiplier);
+ dest.writeByte(this.compassAnimationEnabled ? (byte) 1 : (byte) 0);
+ dest.writeByte(this.accuracyAnimationEnabled ? (byte) 1 : (byte) 0);
+ }
+
+ protected LocationComponentOptions(Parcel in) {
+ this.accuracyAlpha = in.readFloat();
+ this.accuracyColor = in.readInt();
+ this.backgroundDrawableStale = in.readInt();
+ this.backgroundStaleName = in.readString();
+ this.foregroundDrawableStale = in.readInt();
+ this.foregroundStaleName = in.readString();
+ this.gpsDrawable = in.readInt();
+ this.gpsName = in.readString();
+ this.foregroundDrawable = in.readInt();
+ this.foregroundName = in.readString();
+ this.backgroundDrawable = in.readInt();
+ this.backgroundName = in.readString();
+ this.bearingDrawable = in.readInt();
+ this.bearingName = in.readString();
+ this.bearingTintColor = (Integer) in.readValue(Integer.class.getClassLoader());
+ this.foregroundTintColor = (Integer) in.readValue(Integer.class.getClassLoader());
+ this.backgroundTintColor = (Integer) in.readValue(Integer.class.getClassLoader());
+ this.foregroundStaleTintColor = (Integer) in.readValue(Integer.class.getClassLoader());
+ this.backgroundStaleTintColor = (Integer) in.readValue(Integer.class.getClassLoader());
+ this.elevation = in.readFloat();
+ this.enableStaleState = in.readByte() != 0;
+ this.staleStateTimeout = in.readLong();
+ this.padding = in.createIntArray();
+ this.maxZoomIconScale = in.readFloat();
+ this.minZoomIconScale = in.readFloat();
+ this.trackingGesturesManagement = in.readByte() != 0;
+ this.trackingInitialMoveThreshold = in.readFloat();
+ this.trackingMultiFingerMoveThreshold = in.readFloat();
+ this.trackingMultiFingerProtectedMoveArea = in.readParcelable(RectF.class.getClassLoader());
+ this.layerAbove = in.readString();
+ this.layerBelow = in.readString();
+ this.trackingAnimationDurationMultiplier = in.readFloat();
+ this.compassAnimationEnabled = in.readByte() != 0;
+ this.accuracyAnimationEnabled = in.readByte() != 0;
+ }
+
public static final Parcelable.Creator
+ * Best paired with the {@link LocationComponentOptions.Builder#trackingMultiFingerMoveThreshold(float)}
+ * set to 0 or a relatively small value to not interfere with gestures outside of the defined rectangle.
+ *
+ * @param rect the protected multi finger area while camera is tracking
+ */
+ @NonNull
+ public LocationComponentOptions.Builder trackingMultiFingerProtectedMoveArea(@Nullable RectF rect) {
+ this.trackingMultiFingerProtectedMoveArea = rect;
+ return this;
+ }
+
/**
* Sets the id of the layer that's referenced when placing the component on the map using
* {@link com.mapbox.mapboxsdk.maps.Style#addLayerAbove(Layer, String)}.
@@ -1768,6 +1765,7 @@ LocationComponentOptions autoBuild() {
trackingGesturesManagement,
this.trackingInitialMoveThreshold,
this.trackingMultiFingerMoveThreshold,
+ this.trackingMultiFingerProtectedMoveArea,
this.layerAbove,
this.layerBelow,
this.trackingAnimationDurationMultiplier,
diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationCameraControllerTest.java b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationCameraControllerTest.java
index f068c94ce..acf5a9dac 100644
--- a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationCameraControllerTest.java
+++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationCameraControllerTest.java
@@ -1,6 +1,7 @@
package com.mapbox.mapboxsdk.location;
import android.graphics.PointF;
+import android.graphics.RectF;
import android.location.Location;
import com.mapbox.android.gestures.AndroidGesturesManager;
@@ -40,6 +41,8 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.atMost;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -73,6 +76,7 @@ public void setCameraMode_gestureThresholdIsAdjusted() {
camera.setCameraMode(TRACKING_GPS);
verify(moveGestureDetector).setMoveThreshold(moveThreshold);
+ verify(moveGestureDetector, times(0)).setMoveThresholdRect(any(RectF.class));
}
@Test
@@ -89,6 +93,7 @@ public void setCameraMode_gestureThresholdNotAdjustedWhenDisabled() {
verify(moveGestureDetector, times(0)).setMoveThreshold(moveThreshold);
verify(moveGestureDetector, times(0)).setMoveThreshold(0f);
+ verify(moveGestureDetector, times(0)).setMoveThresholdRect(any(RectF.class));
}
@Test
@@ -102,6 +107,7 @@ public void setCameraMode_gestureThresholdIsResetWhenNotTracking() {
camera.setCameraMode(NONE);
verify(moveGestureDetector, times(2)).setMoveThreshold(0f); // one for initialization
+ verify(moveGestureDetector, times(2)).setMoveThresholdRect(null); // one for initialization
}
@Test
@@ -495,6 +501,190 @@ public void gesturesManagement_optionNotChangedInternal() {
verify(mapboxMap, times(0)).setGesturesManager(internalGesturesManager, true, true);
}
+ @Test
+ public void gesturesManagement_moveGesture_notTracking() {
+ MoveGestureDetector moveGestureDetector = mock(MoveGestureDetector.class);
+ when(moveGestureDetector.getPointersCount()).thenReturn(1);
+ LocationCameraController camera = buildCamera(moveGestureDetector);
+ LocationComponentOptions options = mock(LocationComponentOptions.class);
+ when(options.trackingGesturesManagement()).thenReturn(true);
+ float initial = 100;
+ float multiFinger = 200;
+ RectF multiFingerArea = mock(RectF.class);
+ when(options.trackingInitialMoveThreshold()).thenReturn(initial);
+ when(options.trackingMultiFingerMoveThreshold()).thenReturn(multiFinger);
+ when(options.trackingMultiFingerProtectedMoveArea()).thenReturn(multiFingerArea);
+ camera.initializeOptions(options);
+
+ camera.onMoveListener.onMoveBegin(moveGestureDetector);
+
+ verify(moveGestureDetector, times(2)).setMoveThreshold(0);
+ verify(moveGestureDetector, times(2)).setMoveThresholdRect(null);
+ }
+
+ @Test
+ public void gesturesManagement_moveGesture_singlePointer_tracking() {
+ MoveGestureDetector moveGestureDetector = mock(MoveGestureDetector.class);
+ when(moveGestureDetector.getPointersCount()).thenReturn(1);
+ LocationCameraController camera = buildCamera(moveGestureDetector);
+ LocationComponentOptions options = mock(LocationComponentOptions.class);
+ when(options.trackingGesturesManagement()).thenReturn(true);
+ float initial = 100;
+ when(options.trackingInitialMoveThreshold()).thenReturn(initial);
+ camera.initializeOptions(options);
+
+ camera.setCameraMode(TRACKING);
+ when(moveGestureDetector.getMoveThreshold()).thenReturn(initial);
+ camera.onMoveListener.onMoveBegin(moveGestureDetector);
+
+ verify(moveGestureDetector, atMost(1)).setMoveThreshold(initial);
+ verify(moveGestureDetector, times(0)).setMoveThresholdRect(any(RectF.class));
+ }
+
+ @Test
+ public void gesturesManagement_moveGesture_singlePointer_tracking_duplicateCall() {
+ MoveGestureDetector moveGestureDetector = mock(MoveGestureDetector.class);
+ when(moveGestureDetector.getPointersCount()).thenReturn(1);
+ LocationCameraController camera = buildCamera(moveGestureDetector);
+ LocationComponentOptions options = mock(LocationComponentOptions.class);
+ when(options.trackingGesturesManagement()).thenReturn(true);
+ float initial = 100;
+ when(options.trackingInitialMoveThreshold()).thenReturn(initial);
+ camera.initializeOptions(options);
+
+ camera.setCameraMode(TRACKING);
+ when(moveGestureDetector.getMoveThreshold()).thenReturn(initial);
+ camera.onMoveListener.onMoveBegin(moveGestureDetector);
+
+ verify(moveGestureDetector, atMost(1)).setMoveThreshold(initial);
+ verify(moveGestureDetector, times(0)).setMoveThresholdRect(any(RectF.class));
+ }
+
+ @Test
+ public void gesturesManagement_moveGesture_singlePointer_tracking_thresholdMet() {
+ MoveGestureDetector moveGestureDetector = mock(MoveGestureDetector.class);
+ when(moveGestureDetector.getPointersCount()).thenReturn(1);
+ LocationCameraController camera = buildCamera(moveGestureDetector);
+ LocationComponentOptions options = mock(LocationComponentOptions.class);
+ when(options.trackingGesturesManagement()).thenReturn(true);
+ float initial = 100;
+ when(options.trackingInitialMoveThreshold()).thenReturn(initial);
+ camera.initializeOptions(options);
+
+ // verify the number of detector interruptions
+ camera.setCameraMode(TRACKING);
+ camera.onMoveListener.onMoveBegin(moveGestureDetector);
+ when(moveGestureDetector.getMoveThreshold()).thenReturn(initial);
+ camera.onMoveListener.onMove(moveGestureDetector);
+ verify(moveGestureDetector, times(1)).interrupt();
+ camera.onMoveListener.onMoveEnd(moveGestureDetector);
+ camera.onMoveListener.onMoveBegin(moveGestureDetector);
+ camera.onMoveListener.onMove(moveGestureDetector);
+ verify(moveGestureDetector, times(2)).interrupt();
+ camera.onMoveListener.onMoveEnd(moveGestureDetector);
+ camera.onMoveListener.onMoveBegin(moveGestureDetector);
+ camera.onMoveListener.onMove(moveGestureDetector);
+ camera.onMoveListener.onMoveEnd(moveGestureDetector);
+
+ verify(moveGestureDetector, times(2)).interrupt();
+
+ // verify that threshold are reset
+ ArgumentCaptor