Skip to content
This repository has been archived by the owner on Aug 8, 2023. It is now read-only.

Add velocity to gestures / port animations to SDK animators #10202

Merged
merged 3 commits into from
Oct 30, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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
Expand All @@ -440,22 +473,14 @@ 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()),
MapboxEvent.GESTURE_PINCH_START, transform));
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
Expand All @@ -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;
}
Expand All @@ -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;
Expand All @@ -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) {
Expand All @@ -506,31 +531,93 @@ 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;
}
}

/**
* Responsible for handling rotation gestures.
*/
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
Expand All @@ -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
Expand All @@ -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();
Expand All @@ -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);
Expand All @@ -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;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Loading