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 CREATOR = new Parcelable.Creator() { @Override - public LocationComponentOptions createFromParcel(Parcel in) { - return new LocationComponentOptions( - in.readFloat(), - in.readInt(), - in.readInt(), - in.readInt() == 0 ? in.readString() : null, - in.readInt(), - in.readInt() == 0 ? in.readString() : null, - in.readInt(), - in.readInt() == 0 ? in.readString() : null, - in.readInt(), - in.readInt() == 0 ? in.readString() : null, - in.readInt(), - in.readInt() == 0 ? in.readString() : null, - in.readInt(), - in.readInt() == 0 ? in.readString() : null, - in.readInt() == 0 ? in.readInt() : null, - in.readInt() == 0 ? in.readInt() : null, - in.readInt() == 0 ? in.readInt() : null, - in.readInt() == 0 ? in.readInt() : null, - in.readInt() == 0 ? in.readInt() : null, - in.readFloat(), - in.readInt() == 1, - in.readLong(), - in.createIntArray(), - in.readFloat(), - in.readFloat(), - in.readInt() == 1, - in.readFloat(), - in.readFloat(), - in.readString(), - in.readString(), - in.readFloat(), - in.readInt() == 1, - in.readInt() == 1 - ); + public LocationComponentOptions createFromParcel(Parcel source) { + return new LocationComponentOptions(source); } @Override @@ -983,98 +1053,6 @@ public LocationComponentOptions[] newArray(int size) { } }; - @Override - public void writeToParcel(@NonNull Parcel dest, int flags) { - dest.writeFloat(accuracyAlpha()); - dest.writeInt(accuracyColor()); - dest.writeInt(backgroundDrawableStale()); - if (backgroundStaleName() == null) { - dest.writeInt(1); - } else { - dest.writeInt(0); - dest.writeString(backgroundStaleName()); - } - dest.writeInt(foregroundDrawableStale()); - if (foregroundStaleName() == null) { - dest.writeInt(1); - } else { - dest.writeInt(0); - dest.writeString(foregroundStaleName()); - } - dest.writeInt(gpsDrawable()); - if (gpsName() == null) { - dest.writeInt(1); - } else { - dest.writeInt(0); - dest.writeString(gpsName()); - } - dest.writeInt(foregroundDrawable()); - if (foregroundName() == null) { - dest.writeInt(1); - } else { - dest.writeInt(0); - dest.writeString(foregroundName()); - } - dest.writeInt(backgroundDrawable()); - if (backgroundName() == null) { - dest.writeInt(1); - } else { - dest.writeInt(0); - dest.writeString(backgroundName()); - } - dest.writeInt(bearingDrawable()); - if (bearingName() == null) { - dest.writeInt(1); - } else { - dest.writeInt(0); - dest.writeString(bearingName()); - } - if (bearingTintColor() == null) { - dest.writeInt(1); - } else { - dest.writeInt(0); - dest.writeInt(bearingTintColor()); - } - if (foregroundTintColor() == null) { - dest.writeInt(1); - } else { - dest.writeInt(0); - dest.writeInt(foregroundTintColor()); - } - if (backgroundTintColor() == null) { - dest.writeInt(1); - } else { - dest.writeInt(0); - dest.writeInt(backgroundTintColor()); - } - if (foregroundStaleTintColor() == null) { - dest.writeInt(1); - } else { - dest.writeInt(0); - dest.writeInt(foregroundStaleTintColor()); - } - if (backgroundStaleTintColor() == null) { - dest.writeInt(1); - } else { - dest.writeInt(0); - dest.writeInt(backgroundStaleTintColor()); - } - dest.writeFloat(elevation()); - dest.writeInt(enableStaleState() ? 1 : 0); - dest.writeLong(staleStateTimeout()); - dest.writeIntArray(padding()); - dest.writeFloat(maxZoomIconScale()); - dest.writeFloat(minZoomIconScale()); - dest.writeInt(trackingGesturesManagement() ? 1 : 0); - dest.writeFloat(trackingInitialMoveThreshold()); - dest.writeFloat(trackingMultiFingerMoveThreshold()); - dest.writeString(layerAbove()); - dest.writeString(layerBelow()); - dest.writeFloat(trackingAnimationDurationMultiplier); - dest.writeInt(compassAnimationEnabled() ? 1 : 0); - dest.writeInt(accuracyAnimationEnabled() ? 1 : 0); - } - @Override public int describeContents() { return 0; @@ -1151,6 +1129,7 @@ public LocationComponentOptions build() { private Boolean trackingGesturesManagement; private Float trackingInitialMoveThreshold; private Float trackingMultiFingerMoveThreshold; + private RectF trackingMultiFingerProtectedMoveArea; private String layerAbove; private String layerBelow; private Float trackingAnimationDurationMultiplier; @@ -1189,6 +1168,7 @@ private Builder(LocationComponentOptions source) { this.trackingGesturesManagement = source.trackingGesturesManagement(); this.trackingInitialMoveThreshold = source.trackingInitialMoveThreshold(); this.trackingMultiFingerMoveThreshold = source.trackingMultiFingerMoveThreshold(); + this.trackingMultiFingerProtectedMoveArea = source.trackingMultiFingerProtectedMoveArea(); this.layerAbove = source.layerAbove(); this.layerBelow = source.layerBelow(); this.trackingAnimationDurationMultiplier = source.trackingAnimationDurationMultiplier(); @@ -1588,6 +1568,7 @@ public LocationComponentOptions.Builder minZoomIconScale(float minZoomIconScale) * false otherwise * @see Builder#trackingInitialMoveThreshold(float) * @see Builder#trackingMultiFingerMoveThreshold(float) + * @see Builder#trackingMultiFingerProtectedMoveArea(RectF) */ @NonNull public LocationComponentOptions.Builder trackingGesturesManagement(boolean trackingGesturesManagement) { @@ -1618,6 +1599,22 @@ public LocationComponentOptions.Builder trackingMultiFingerMoveThreshold(float m return this; } + /** + * Sets 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. + * + * @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 moveThresholdCaptor = ArgumentCaptor.forClass(Float.class); + verify(moveGestureDetector, atLeastOnce()).setMoveThreshold(moveThresholdCaptor.capture()); + org.junit.Assert.assertEquals(Float.valueOf(0), moveThresholdCaptor.getValue()); + } + + @Test + public void gesturesManagement_moveGesture_multiPointer_tracking() { + MoveGestureDetector moveGestureDetector = mock(MoveGestureDetector.class); + when(moveGestureDetector.getPointersCount()).thenReturn(2); + 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.setCameraMode(TRACKING); + camera.onMoveListener.onMoveBegin(moveGestureDetector); + + verify(moveGestureDetector, atMost(1)).setMoveThreshold(multiFinger); + verify(moveGestureDetector, atMost(1)).setMoveThresholdRect(multiFingerArea); + } + + @Test + public void gesturesManagement_moveGesture_multiPointer_tracking_duplicateCall() { + MoveGestureDetector moveGestureDetector = mock(MoveGestureDetector.class); + when(moveGestureDetector.getPointersCount()).thenReturn(2); + 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.setCameraMode(TRACKING); + camera.onMoveListener.onMoveBegin(moveGestureDetector); + when(moveGestureDetector.getMoveThreshold()).thenReturn(multiFinger); + when(moveGestureDetector.getMoveThresholdRect()).thenReturn(multiFingerArea); + camera.onMoveListener.onMoveBegin(moveGestureDetector); + + verify(moveGestureDetector, atMost(1)).setMoveThreshold(multiFinger); + verify(moveGestureDetector, atMost(1)).setMoveThresholdRect(multiFingerArea); + } + + @Test + public void gesturesManagement_moveGesture_multiPointer_tracking_thresholdMet() { + MoveGestureDetector moveGestureDetector = mock(MoveGestureDetector.class); + when(moveGestureDetector.getPointersCount()).thenReturn(2); + 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); + + // verify the number of detector interruptions + camera.setCameraMode(TRACKING); + camera.onMoveListener.onMoveBegin(moveGestureDetector); + when(moveGestureDetector.getMoveThreshold()).thenReturn(initial); + when(moveGestureDetector.getMoveThreshold()).thenReturn(multiFinger); + when(moveGestureDetector.getMoveThresholdRect()).thenReturn(multiFingerArea); + 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 moveThresholdCaptor = ArgumentCaptor.forClass(Float.class); + verify(moveGestureDetector, atLeastOnce()).setMoveThreshold(moveThresholdCaptor.capture()); + org.junit.Assert.assertEquals(Float.valueOf(0), moveThresholdCaptor.getValue()); + + ArgumentCaptor areaCaptor = ArgumentCaptor.forClass(RectF.class); + verify(moveGestureDetector, atLeastOnce()).setMoveThresholdRect(areaCaptor.capture()); + org.junit.Assert.assertNull(areaCaptor.getValue()); + } + @Test public void onMove_notCancellingTransitionWhileNone() { MapboxMap mapboxMap = mock(MapboxMap.class); diff --git a/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/location/LocationModesActivity.java b/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/location/LocationModesActivity.java index f805918cc..d71570a1b 100644 --- a/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/location/LocationModesActivity.java +++ b/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/location/LocationModesActivity.java @@ -2,6 +2,7 @@ import android.annotation.SuppressLint; import android.content.res.Configuration; +import android.graphics.RectF; import android.location.Location; import android.os.Bundle; import androidx.annotation.NonNull; @@ -9,6 +10,7 @@ import androidx.appcompat.widget.ListPopupWindow; import android.view.Menu; import android.view.MenuItem; +import android.view.View; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.Toast; @@ -39,6 +41,7 @@ public class LocationModesActivity extends AppCompatActivity implements OnMapRea private MapView mapView; private Button locationModeBtn; private Button locationTrackingBtn; + private View protectedGestureArea; private PermissionsManager permissionsManager; @@ -64,6 +67,7 @@ protected void onCreate(Bundle savedInstanceState) { setContentView(R.layout.activity_location_layer_mode); mapView = findViewById(R.id.mapView); + protectedGestureArea = findViewById(R.id.view_protected_gesture_area); locationModeBtn = findViewById(R.id.button_location_mode); locationModeBtn.setOnClickListener(v -> { @@ -245,6 +249,9 @@ private void disableGesturesManagement() { return; } + protectedGestureArea.getLayoutParams().height = 0; + protectedGestureArea.getLayoutParams().width = 0; + LocationComponentOptions options = locationComponent .getLocationComponentOptions() .toBuilder() @@ -258,10 +265,16 @@ private void enableGesturesManagement() { return; } + RectF rectF = new RectF(0f, 0f, mapView.getWidth() / 2f, mapView.getHeight() / 2f); + protectedGestureArea.getLayoutParams().height = (int) rectF.bottom; + protectedGestureArea.getLayoutParams().width = (int) rectF.right; + LocationComponentOptions options = locationComponent .getLocationComponentOptions() .toBuilder() .trackingGesturesManagement(true) + .trackingMultiFingerProtectedMoveArea(rectF) + .trackingMultiFingerMoveThreshold(500) .build(); locationComponent.applyStyle(options); } diff --git a/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_location_layer_mode.xml b/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_location_layer_mode.xml index a13940f29..1a0102c76 100644 --- a/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_location_layer_mode.xml +++ b/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_location_layer_mode.xml @@ -16,6 +16,15 @@ app:layout_constraintTop_toTopOf="parent" app:mapbox_uiAttribution="false" /> + +