diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/almeros/android/multitouch/gesturedetectors/TwoFingerGestureDetector.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/almeros/android/multitouch/gesturedetectors/TwoFingerGestureDetector.java index 3d28c2295d1..db492b65560 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/almeros/android/multitouch/gesturedetectors/TwoFingerGestureDetector.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/almeros/android/multitouch/gesturedetectors/TwoFingerGestureDetector.java @@ -54,9 +54,7 @@ public TwoFingerGestureDetector(Context context) { ViewConfiguration config = ViewConfiguration.get(context); - // lowering the edgeSlop allows to execute gesture faster - // https://github.com/mapbox/mapbox-gl-native/issues/10102 - edgeSlop = config.getScaledEdgeSlop() / 3.0f; + edgeSlop = config.getScaledEdgeSlop(); } @Override diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/MapboxConstants.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/MapboxConstants.java index 97a9ea94ee8..fc448ccf7b6 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/MapboxConstants.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/MapboxConstants.java @@ -80,12 +80,12 @@ public class MapboxConstants { /** * The currently used minimun scale factor to clamp to when a quick zoom gesture occurs */ - public static final float MINIMUM_SCALE_FACTOR_CLAMP = 0.65f; + public static final float MINIMUM_SCALE_FACTOR_CLAMP = 0.00f; /** * The currently used maximum scale factor to clamp to when a quick zoom gesture occurs */ - public static final float MAXIMUM_SCALE_FACTOR_CLAMP = 1.35f; + public static final float MAXIMUM_SCALE_FACTOR_CLAMP = 0.45f; /** * Fragment Argument Key for MapboxMapOptions 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 f309570a7b9..4120e164a4b 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 @@ -1,14 +1,19 @@ package com.mapbox.mapboxsdk.maps; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; import android.content.Context; import android.graphics.PointF; import android.location.Location; import android.support.annotation.Nullable; import android.support.v4.view.GestureDetectorCompat; import android.support.v4.view.ScaleGestureDetectorCompat; +import android.support.v4.view.animation.FastOutSlowInInterpolator; import android.view.InputDevice; import android.view.MotionEvent; import android.view.ScaleGestureDetector; +import android.view.VelocityTracker; import android.view.ViewConfiguration; import com.almeros.android.multitouch.gesturedetectors.RotateGestureDetector; @@ -57,8 +62,14 @@ final class MapGestureDetector { private boolean scaleGestureOccurred; private boolean recentScaleGestureOccurred; + private boolean scaleAnimating; private long scaleBeginTime; + private VelocityTracker velocityTracker; + private boolean wasZoomingIn; + private boolean wasClockwiseRotating; + private boolean rotateGestureOccurred; + MapGestureDetector(Context context, Transform transform, Projection projection, UiSettings uiSettings, TrackingSettings trackingSettings, AnnotationManager annotationManager, CameraChangeDispatcher cameraChangeDispatcher) { @@ -153,6 +164,12 @@ boolean onTouchEvent(MotionEvent event) { // Handle two finger tap switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: + if (velocityTracker == null) { + velocityTracker = VelocityTracker.obtain(); + } else { + velocityTracker.clear(); + } + velocityTracker.addMovement(event); // First pointer down, reset scaleGestureOccurred, used to avoid triggering a fling after a scale gesture #7666 recentScaleGestureOccurred = false; transform.setGestureInProgress(true); @@ -203,11 +220,23 @@ boolean onTouchEvent(MotionEvent event) { twoTap = false; transform.setGestureInProgress(false); + if (velocityTracker != null) { + velocityTracker.recycle(); + } + velocityTracker = null; break; case MotionEvent.ACTION_CANCEL: twoTap = false; transform.setGestureInProgress(false); + if (velocityTracker != null) { + velocityTracker.recycle(); + } + velocityTracker = null; + break; + case MotionEvent.ACTION_MOVE: + velocityTracker.addMovement(event); + velocityTracker.computeCurrentVelocity(1000); break; } @@ -430,7 +459,11 @@ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float d */ private class ScaleGestureListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { + private static final int ANIMATION_TIME_MULTIPLIER = 77; + private static final double ZOOM_DISTANCE_DIVIDER = 5; + private float scaleFactor = 1.0f; + private PointF scalePointBegin; // Called when two fingers first touch the screen @Override @@ -440,6 +473,7 @@ public boolean onScaleBegin(ScaleGestureDetector detector) { } recentScaleGestureOccurred = true; + scalePointBegin = new PointF(detector.getFocusX(), detector.getFocusY()); scaleBeginTime = detector.getEventTime(); MapboxTelemetry.getInstance().pushEvent(MapboxEventWrapper.buildMapClickEvent( getLocationFromGesture(detector.getFocusX(), detector.getFocusY()), @@ -447,15 +481,6 @@ public boolean onScaleBegin(ScaleGestureDetector detector) { return true; } - // Called when fingers leave screen - @Override - public void onScaleEnd(ScaleGestureDetector detector) { - scaleGestureOccurred = false; - scaleBeginTime = 0; - scaleFactor = 1.0f; - cameraChangeDispatcher.onCameraIdle(); - } - // Called each time a finger moves // Called for pinch zooms and quickzooms/quickscales @Override @@ -464,6 +489,7 @@ public boolean onScale(ScaleGestureDetector detector) { return super.onScale(detector); } + wasZoomingIn = (Math.log(detector.getScaleFactor()) / Math.log(Math.PI / 2)) >= 0; if (tiltGestureOccurred) { return false; } @@ -478,7 +504,7 @@ public boolean onScale(ScaleGestureDetector detector) { // If scale is large enough ignore a tap scaleFactor *= detector.getScaleFactor(); - if ((scaleFactor > 1.05f) || (scaleFactor < 0.95f)) { + if ((scaleFactor > 1.1f) || (scaleFactor < 0.9f)) { // notify camera change listener cameraChangeDispatcher.onCameraMoveStarted(REASON_API_GESTURE); scaleGestureOccurred = true; @@ -497,7 +523,6 @@ public boolean onScale(ScaleGestureDetector detector) { // make an assumption here; if the zoom center is specified by the gesture, it's NOT going // to be in the center of the map. Therefore the zoom will translate the map center, so tracking // should be disabled. - trackingSettings.resetTrackingModesIfRequired(!quickZoom, false, false); // Scale the map if (focalPoint != null) { @@ -506,19 +531,79 @@ public boolean onScale(ScaleGestureDetector detector) { } else if (quickZoom) { cameraChangeDispatcher.onCameraMove(); // clamp scale factors we feed to core #7514 - float scaleFactor = MathUtils.clamp(detector.getScaleFactor(), + float scaleFactor = detector.getScaleFactor(); + // around center map + double zoomBy = Math.log(scaleFactor) / Math.log(Math.PI / 2); + boolean negative = zoomBy < 0; + zoomBy = MathUtils.clamp(Math.abs(zoomBy), MapboxConstants.MINIMUM_SCALE_FACTOR_CLAMP, MapboxConstants.MAXIMUM_SCALE_FACTOR_CLAMP); - // around center map - transform.zoomBy(Math.log(scaleFactor) / Math.log(Math.PI / 2), - uiSettings.getWidth() / 2, uiSettings.getHeight() / 2); + transform.zoomBy(negative ? -zoomBy : zoomBy, uiSettings.getWidth() / 2, uiSettings.getHeight() / 2); + recentScaleGestureOccurred = true; } else { // around gesture transform.zoomBy(Math.log(detector.getScaleFactor()) / Math.log(Math.PI / 2), - detector.getFocusX(), detector.getFocusY()); + scalePointBegin.x, scalePointBegin.y); } return true; } + + // Called when fingers leave screen + @Override + public void onScaleEnd(final ScaleGestureDetector detector) { + if (rotateGestureOccurred || quickZoom) { + reset(); + return; + } + + double velocityXY = Math.abs(velocityTracker.getYVelocity()) + Math.abs(velocityTracker.getXVelocity()); + if (velocityXY > MapboxConstants.VELOCITY_THRESHOLD_IGNORE_FLING / 2) { + scaleAnimating = true; + double zoomAddition = calculateScale(velocityXY); + double currentZoom = transform.getRawZoom(); + long animationTime = (long) (Math.log(velocityXY) * ANIMATION_TIME_MULTIPLIER); + createScaleAnimator(currentZoom, zoomAddition, animationTime).start(); + } else if (!scaleAnimating) { + reset(); + } + } + + private void reset() { + scaleAnimating = false; + scaleGestureOccurred = false; + scaleBeginTime = 0; + scaleFactor = 1.0f; + cameraChangeDispatcher.onCameraIdle(); + } + + private double calculateScale(double velocityXY) { + double zoomAddition = (float) (Math.log(velocityXY) / ZOOM_DISTANCE_DIVIDER); + if (!wasZoomingIn) { + zoomAddition = -zoomAddition; + } + return zoomAddition; + } + + private Animator createScaleAnimator(double currentZoom, double zoomAddition, long animationTime) { + ValueAnimator animator = ValueAnimator.ofFloat((float) currentZoom, (float) (currentZoom + zoomAddition)); + animator.setDuration(animationTime); + animator.setInterpolator(new FastOutSlowInInterpolator()); + animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + transform.setZoom((Float) animation.getAnimatedValue(), scalePointBegin); + } + }); + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + reset(); + } + }); + return animator; + } } /** @@ -526,11 +611,13 @@ public boolean onScale(ScaleGestureDetector detector) { */ private class RotateGestureListener extends RotateGestureDetector.SimpleOnRotateGestureListener { - private static final long ROTATE_INVOKE_WAIT_TIME = 750; - private static final float ROTATE_INVOKE_ANGLE = 17.5f; + private static final float ROTATE_INVOKE_ANGLE = 15.30f; + private static final float ROTATE_LIMITATION_ANGLE = 3.35f; + private static final float ROTATE_LIMITATION_DURATION = ROTATE_LIMITATION_ANGLE * 1.85f; private long beginTime = 0; private boolean started = false; + private boolean animating = false; // Called when two fingers first touch the screen @Override @@ -546,14 +633,6 @@ public boolean onRotateBegin(RotateGestureDetector detector) { return true; } - // Called when the fingers leave the screen - @Override - public void onRotateEnd(RotateGestureDetector detector) { - // notify camera change listener - beginTime = 0; - started = false; - } - // Called each time one of the two fingers moves // Called for rotation @Override @@ -562,14 +641,6 @@ public boolean onRotate(RotateGestureDetector detector) { return false; } - // Ignore short touches in case it is a tap - // Also ignore small rotate - long time = detector.getEventTime(); - long interval = time - beginTime; - if (!started && (interval <= ViewConfiguration.getTapTimeout() || isScaleGestureActive(time))) { - return false; - } - // If rotate is large enough ignore a tap // Also is zoom already started, don't rotate float angle = detector.getRotationDegreesDelta(); @@ -584,6 +655,11 @@ public boolean onRotate(RotateGestureDetector detector) { return false; } + wasClockwiseRotating = detector.getRotationDegreesDelta() > 0; + if (scaleBeginTime != 0) { + rotateGestureOccurred = true; + } + // rotation constitutes translation of anything except the center of // rotation, so cancel both location and bearing tracking if required trackingSettings.resetTrackingModesIfRequired(true, true, false); @@ -602,11 +678,81 @@ public boolean onRotate(RotateGestureDetector detector) { return true; } - private boolean isScaleGestureActive(long time) { - long scaleExecutionTime = time - scaleBeginTime; - boolean scaleGestureStarted = scaleBeginTime != 0; - boolean scaleOffsetTimeValid = scaleExecutionTime > ROTATE_INVOKE_WAIT_TIME; - return (scaleGestureStarted && scaleOffsetTimeValid) || scaleGestureOccurred; + // Called when the fingers leave the screen + @Override + public void onRotateEnd(RotateGestureDetector detector) { + long interval = detector.getEventTime() - beginTime; + if ((!started && (interval <= ViewConfiguration.getTapTimeout())) || scaleAnimating || interval > 500) { + reset(); + return; + } + + double angularVelocity = calculateVelocityVector(detector); + if (Math.abs(angularVelocity) > 0.001 && rotateGestureOccurred && !animating) { + animateRotateVelocity(); + } else if (!animating) { + reset(); + } + } + + private void reset() { + beginTime = 0; + started = false; + animating = false; + rotateGestureOccurred = false; + } + + private void animateRotateVelocity() { + animating = true; + double currentRotation = transform.getRawBearing(); + double rotateAdditionDegrees = calculateVelocityInDegrees(); + createAnimator(currentRotation, rotateAdditionDegrees).start(); + } + + private double calculateVelocityVector(RotateGestureDetector detector) { + return ((detector.getFocusX() * velocityTracker.getYVelocity()) + + (detector.getFocusY() * velocityTracker.getXVelocity())) + / (Math.pow(detector.getFocusX(), 2) + Math.pow(detector.getFocusY(), 2)); + } + + private double calculateVelocityInDegrees() { + double angleRadians = Math.atan2(velocityTracker.getXVelocity(), velocityTracker.getYVelocity()); + double angle = angleRadians / (Math.PI / 180); + if (angle <= 0) { + angle += 360; + } + + // limit the angle + angle = angle / ROTATE_LIMITATION_ANGLE; + + // correct direction + if (!wasClockwiseRotating) { + angle = -angle; + } + + return angle; + } + + private Animator createAnimator(double currentRotation, double rotateAdditionDegrees) { + ValueAnimator animator = ValueAnimator.ofFloat( + (float) currentRotation, + (float) (currentRotation + rotateAdditionDegrees) + ); + animator.setDuration((long) (Math.abs(rotateAdditionDegrees) * ROTATE_LIMITATION_DURATION)); + animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + transform.setBearing((Float) animation.getAnimatedValue()); + } + }); + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + reset(); + } + }); + return animator; } } diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapView.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapView.java index de27964a5de..f581aa0285d 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapView.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapView.java @@ -423,6 +423,9 @@ public boolean onTrackballEvent(MotionEvent event) { @Override public boolean onGenericMotionEvent(MotionEvent event) { + if (mapGestureDetector == null) { + return super.onGenericMotionEvent(event); + } return mapGestureDetector.onGenericMotionEvent(event) || super.onGenericMotionEvent(event); } diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Transform.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Transform.java index 6f63c2eba8f..c5fd37f21e3 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Transform.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Transform.java @@ -210,6 +210,10 @@ void setOnCameraChangeListener(@Nullable MapboxMap.OnCameraChangeListener listen return cameraPosition.zoom; } + double getRawZoom() { + return mapView.getZoom(); + } + void zoom(boolean zoomIn, @NonNull PointF focalPoint) { CameraPosition cameraPosition = invalidateCameraPosition(); if (cameraPosition != null) { @@ -221,6 +225,17 @@ void zoom(boolean zoomIn, @NonNull PointF focalPoint) { } } + void zoom(double zoomAddition, @NonNull PointF focalPoint, long duration) { + CameraPosition cameraPosition = invalidateCameraPosition(); + if (cameraPosition != null) { + int newZoom = (int) Math.round(cameraPosition.zoom + zoomAddition); + setZoom(newZoom, focalPoint, duration); + } else { + // we are not transforming, notify about being idle + cameraChangeDispatcher.onCameraIdle(); + } + } + void setZoom(double zoom, @NonNull PointF focalPoint) { setZoom(zoom, focalPoint, 0); }