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

Commit

Permalink
[android] - Add angular velocity effect on rotation gesture.
Browse files Browse the repository at this point in the history
  • Loading branch information
Ramin Mirsharifi authored and tobrun committed Oct 30, 2017
1 parent c351f65 commit 9b41791
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 71 deletions.
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,12 +1,15 @@
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.os.Handler;
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;
Expand Down Expand Up @@ -59,11 +62,13 @@ final class MapGestureDetector {

private boolean scaleGestureOccurred;
private boolean recentScaleGestureOccurred;
private boolean scaleAnimating;
private long scaleBeginTime;

private VelocityTracker velocityTracker = null;
private boolean wasZoomingIn = false;
private final Handler handler = new Handler();
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,
Expand Down Expand Up @@ -192,8 +197,7 @@ boolean onTouchEvent(MotionEvent event) {
boolean isTap = tapInterval <= ViewConfiguration.getTapTimeout();
boolean inProgress = rotateGestureDetector.isInProgress()
|| scaleGestureDetector.isInProgress()
|| shoveGestureDetector.isInProgress()
|| scaleGestureOccurred;
|| shoveGestureDetector.isInProgress();

if (twoTap && isTap && !inProgress) {
if (focalPoint != null) {
Expand Down Expand Up @@ -419,7 +423,7 @@ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float d
return false;
}

if (tiltGestureOccurred || scaleGestureOccurred) {
if (tiltGestureOccurred) {
return false;
}

Expand Down Expand Up @@ -455,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 @@ -465,39 +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) {
double velocityXY = Math.abs(velocityTracker.getYVelocity()) + Math.abs(velocityTracker.getXVelocity());
if (velocityXY > MapboxConstants.VELOCITY_THRESHOLD_IGNORE_FLING / 2) {
long animationTime = (long)(Math.log(velocityXY) * 66);
double zoomAddition = (float) (Math.log(velocityXY) / 7.7);
if (!wasZoomingIn) {
zoomAddition = -zoomAddition;
}
scaleGestureOccurred = true;
transform.zoom(zoomAddition, new PointF(detector.getFocusX(), detector.getFocusY()), animationTime);
handler.postDelayed(new Runnable() {
@Override
public void run() {
scaleGestureOccurred = false;
}
}, animationTime);
} else {
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 @@ -506,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 @@ -514,13 +498,13 @@ public boolean onScale(ScaleGestureDetector detector) {
// Also ignore small scales
long time = detector.getEventTime();
long interval = time - scaleBeginTime;
if (!scaleGestureOccurred && (interval <= ViewConfiguration.getTapTimeout() / 3)) {
if (!scaleGestureOccurred && (interval <= ViewConfiguration.getTapTimeout())) {
return false;
}

// 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 @@ -539,41 +523,101 @@ 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
wasZoomingIn = (Math.log(detector.getScaleFactor()) / Math.log(Math.PI / 2)) >= 0;
if (focalPoint != null) {
// arround user provided focal point
transform.zoomBy(Math.log(detector.getScaleFactor()) / Math.log(Math.PI / 2), focalPoint.x, focalPoint.y);
} 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 @@ -589,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 @@ -605,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 @@ -627,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 @@ -645,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
Loading

0 comments on commit 9b41791

Please sign in to comment.