[android] execute quickzoom scale change based on the Y axis delta change

Using finger's Y axis position allows for a linear zoom additions/deductions during the quick-zoom changes. This is in contrast to the previously used scale factor, which is based on the current span (distance from the origin) to previous span ratio and increases the closer the finger is to the origin of a gesture because the values are smaller.

The scale ratio based changes are also reliant on series of motion events' values and because the Android framework can skip some events, going back and forth during a quick-zoom might not have resulted in the same transformation. This was reproduced by the round-tripping test introduced in this commit.
This commit is contained in:
Łukasz Paczos
2019-08-15 17:34:04 +02:00
committed by Łukasz Paczos
parent 074dba57a2
commit 8e1bd18eae
4 changed files with 68 additions and 18 deletions

View File

@@ -86,6 +86,11 @@ public class MapboxConstants {
*/
public static final double MAX_ABSOLUTE_SCALE_VELOCITY_CHANGE = 2.5;
/**
* Maximum possible zoom change during the quick zoom gesture executed across the whole screen
*/
public static final double QUICK_ZOOM_MAX_ZOOM_CHANGE = 4.0;
/**
* Scale velocity animation duration multiplier.
*/
@@ -156,12 +161,18 @@ public class MapboxConstants {
/**
* The currently used minimum scale factor to clamp to when a quick zoom gesture occurs
*
* @deprecated unused
*/
@Deprecated
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
*
* @deprecated unused
*/
@Deprecated
public static final float MAXIMUM_SCALE_FACTOR_CLAMP = 0.15f;
/**

View File

@@ -4,6 +4,7 @@ import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.PointF;
import android.os.Handler;
import android.support.annotation.NonNull;
@@ -31,11 +32,13 @@ import java.util.concurrent.CopyOnWriteArrayList;
import static com.mapbox.mapboxsdk.constants.MapboxConstants.MAXIMUM_ANGULAR_VELOCITY;
import static com.mapbox.mapboxsdk.constants.MapboxConstants.MAX_ABSOLUTE_SCALE_VELOCITY_CHANGE;
import static com.mapbox.mapboxsdk.constants.MapboxConstants.QUICK_ZOOM_MAX_ZOOM_CHANGE;
import static com.mapbox.mapboxsdk.constants.MapboxConstants.ROTATE_VELOCITY_RATIO_THRESHOLD;
import static com.mapbox.mapboxsdk.constants.MapboxConstants.SCALE_VELOCITY_ANIMATION_DURATION_MULTIPLIER;
import static com.mapbox.mapboxsdk.constants.MapboxConstants.SCALE_VELOCITY_RATIO_THRESHOLD;
import static com.mapbox.mapboxsdk.constants.MapboxConstants.ZOOM_RATE;
import static com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraMoveStartedListener.REASON_API_GESTURE;
import static com.mapbox.mapboxsdk.utils.MathUtils.normalize;
/**
* Manages gestures events on a MapView.
@@ -76,6 +79,9 @@ final class MapGestureDetector {
@Nullable
private PointF constantFocalPoint;
@NonNull
private PointF doubleTapFocalPoint = new PointF();
private AndroidGesturesManager gesturesManager;
private Animator scaleAnimator;
@@ -321,8 +327,6 @@ final class MapGestureDetector {
}
private final class StandardGestureListener extends StandardGestureDetector.SimpleStandardOnGestureListener {
private PointF doubleTapFocalPoint;
private final float doubleTapMovementThreshold;
StandardGestureListener(float doubleTapMovementThreshold) {
@@ -495,6 +499,8 @@ final class MapGestureDetector {
private final double scaleVelocityRatioThreshold;
private boolean quickZoom;
private float spanSinceLast;
private double screenHeight;
private double startZoom;
ScaleGestureListener(double densityMultiplier, float minimumGestureSpeed, float minimumAngledGestureSpeed,
float minimumVelocity) {
@@ -548,6 +554,9 @@ final class MapGestureDetector {
}
}
screenHeight = Resources.getSystem().getDisplayMetrics().heightPixels;
startZoom = transform.getRawZoom();
cancelTransitionsIfRequired();
notifyOnScaleBeginListeners(detector);
@@ -562,10 +571,24 @@ final class MapGestureDetector {
// dispatching camera start event only when the movement actually occurred
cameraChangeDispatcher.onCameraMoveStarted(CameraChangeDispatcher.REASON_API_GESTURE);
float scaleFactor = detector.getScaleFactor();
double zoomBy = getNewZoom(scaleFactor, quickZoom);
PointF focalPoint = getScaleFocalPoint(detector);
transform.zoomBy(zoomBy, focalPoint);
if (quickZoom) {
double pixelDeltaChange = Math.abs(detector.getCurrentEvent().getY() - doubleTapFocalPoint.y);
boolean zoomedOut = detector.getCurrentEvent().getY() < doubleTapFocalPoint.y;
// normalize the pixel delta change, ranging from 0 to screen height, to a constant zoom change range
double normalizedDeltaChange = normalize(pixelDeltaChange, 0, screenHeight, 0, QUICK_ZOOM_MAX_ZOOM_CHANGE);
// calculate target zoom and adjust for a multiplier
double targetZoom = (zoomedOut ? startZoom - normalizedDeltaChange : startZoom + normalizedDeltaChange);
targetZoom *= uiSettings.getZoomRate();
transform.setZoom(targetZoom, focalPoint);
} else {
double zoomBy =
(Math.log(detector.getScaleFactor()) / Math.log(Math.PI / 2)) * ZOOM_RATE * uiSettings.getZoomRate();
transform.zoomBy(zoomBy, focalPoint);
}
notifyOnScaleListeners(detector);
@@ -628,19 +651,6 @@ final class MapGestureDetector {
}
return zoomAddition;
}
private double getNewZoom(float scaleFactor, boolean quickZoom) {
double zoomBy = (Math.log(scaleFactor) / Math.log(Math.PI / 2)) * ZOOM_RATE * uiSettings.getZoomRate();
if (quickZoom) {
// clamp scale factors we feed to core #7514
boolean negative = zoomBy < 0;
zoomBy = MathUtils.clamp(Math.abs(zoomBy),
MapboxConstants.MINIMUM_SCALE_FACTOR_CLAMP,
MapboxConstants.MAXIMUM_SCALE_FACTOR_CLAMP);
return negative ? -zoomBy : zoomBy;
}
return zoomBy;
}
}
private final class RotateGestureListener extends RotateGestureDetector.SimpleOnRotateGestureListener {

View File

@@ -46,4 +46,19 @@ public class MathUtils {
return secondMod + min;
}
/**
* Scale a value from an arbitrary range to a normalized range.
*
* @param x The value to be normalized.
* @param dataLow lowest expected value from a data set
* @param dataHigh highest expected value from a data set
* @param normalizedLow normalized lowest value
* @param normalizedHigh normalized highest value
* @return The result of the normalization.
*/
public static double normalize(double x, double dataLow, double dataHigh,
double normalizedLow, double normalizedHigh) {
return ((x - dataLow) / (dataHigh - dataLow)) * (normalizedHigh - normalizedLow) + normalizedLow;
}
}

View File

@@ -176,4 +176,18 @@ class MapGestureDetectorTest : BaseTest() {
Assert.assertNotEquals(initialCameraPosition!!.target.longitude, mapboxMap.cameraPosition.target.longitude, 1.0)
}
}
@Test
fun quickZoom_roundTripping() {
validateTestSetup()
rule.runOnUiThread {
mapboxMap.moveCamera(CameraUpdateFactory.newLatLngZoom(LatLng(51.0, 16.0), 3.0))
}
onView(withId(R.id.mapView)).perform(quickScale(300f, withVelocity = false, duration = 750L))
onView(withId(R.id.mapView)).perform(quickScale(-300f, withVelocity = false, duration = 750L))
rule.runOnUiThread {
Assert.assertEquals(3.0, mapboxMap.cameraPosition.zoom, 0.0001)
}
}
}