Animator2: physical fling (#497)

This commit is contained in:
Gustl22 2018-02-01 22:47:52 +01:00 committed by Emux
parent 241f112b58
commit 03eb28f09e
No known key found for this signature in database
GPG Key ID: 89C6921D7AF2BDD0
11 changed files with 413 additions and 32 deletions

View File

@ -21,7 +21,9 @@ import com.badlogic.gdx.input.GestureDetector.GestureListener;
import com.badlogic.gdx.math.Vector2;
import org.oscim.core.Tile;
import org.oscim.map.Animator2;
import org.oscim.map.Map;
import org.oscim.utils.Parameters;
public class GestureHandler implements GestureListener {
private boolean mayFling = true;
@ -84,7 +86,10 @@ public class GestureHandler implements GestureListener {
//log.debug("fling " + button + " " + velocityX + "/" + velocityY);
if (mayFling && button == Buttons.LEFT) {
int m = Tile.SIZE * 4;
mMap.animator().animateFling((int) velocityX, (int) velocityY, -m, m, -m, m);
if (Parameters.ANIMATOR2)
((Animator2) mMap.animator()).animateFlingScroll(velocityX, velocityY, -m, m, -m, m);
else
mMap.animator().animateFling(velocityX, velocityY, -m, m, -m, m);
return true;
}
return false;

View File

@ -31,7 +31,7 @@ import org.oscim.layers.tile.buildings.BuildingLayer;
import org.oscim.map.Map;
import org.oscim.map.ViewController;
import org.oscim.theme.VtmThemes;
import org.oscim.utils.Easing;
import org.oscim.utils.animation.Easing;
import java.util.List;

View File

@ -19,15 +19,19 @@
*/
package org.oscim.layers;
import org.oscim.backend.CanvasAdapter;
import org.oscim.backend.Platform;
import org.oscim.core.MapPosition;
import org.oscim.core.Tile;
import org.oscim.event.Event;
import org.oscim.event.Gesture;
import org.oscim.event.GestureListener;
import org.oscim.event.MotionEvent;
import org.oscim.map.Animator2;
import org.oscim.map.Map;
import org.oscim.map.Map.InputListener;
import org.oscim.map.ViewController;
import org.oscim.utils.Parameters;
import static org.oscim.backend.CanvasAdapter.dpi;
import static org.oscim.utils.FastMath.withinSquaredDist;
@ -469,8 +473,14 @@ public class MapEventLayer extends AbstractMapEventLayer implements InputListene
int w = Tile.SIZE * 5;
int h = Tile.SIZE * 5;
mMap.animator().animateFling(velocityX * 2, velocityY * 2,
-w, w, -h, h);
if (Parameters.ANIMATOR2) {
if (!CanvasAdapter.platform.isDesktop() && CanvasAdapter.platform != Platform.WEBGL) {
velocityX *= 2;
velocityY *= 2;
}
((Animator2) mMap.animator()).animateFlingScroll(velocityX, velocityY, -w, w, -h, h);
} else
mMap.animator().animateFling(velocityX * 2, velocityY * 2, -w, w, -h, h);
return true;
}

View File

@ -21,14 +21,17 @@
package org.oscim.layers;
import org.oscim.backend.CanvasAdapter;
import org.oscim.backend.Platform;
import org.oscim.core.MapPosition;
import org.oscim.core.Tile;
import org.oscim.event.Event;
import org.oscim.event.Gesture;
import org.oscim.event.MotionEvent;
import org.oscim.map.Animator2;
import org.oscim.map.Map;
import org.oscim.map.Map.InputListener;
import org.oscim.map.ViewController;
import org.oscim.utils.Parameters;
import org.oscim.utils.async.Task;
import static org.oscim.backend.CanvasAdapter.dpi;
@ -569,8 +572,14 @@ public class MapEventLayer2 extends AbstractMapEventLayer implements InputListen
int w = Tile.SIZE * 5;
int h = Tile.SIZE * 5;
mMap.animator().animateFling(velocityX * 2, velocityY * 2,
-w, w, -h, h);
if (Parameters.ANIMATOR2) {
if (!CanvasAdapter.platform.isDesktop() && CanvasAdapter.platform != Platform.WEBGL) {
velocityX *= 2;
velocityY *= 2;
}
((Animator2) mMap.animator()).animateFlingScroll(velocityX, velocityY, -w, w, -h, h);
} else
mMap.animator().animateFling(velocityX * 2, velocityY * 2, -w, w, -h, h);
return true;
}

View File

@ -28,8 +28,8 @@ import org.oscim.core.MapPosition;
import org.oscim.core.Point;
import org.oscim.core.Tile;
import org.oscim.renderer.MapRenderer;
import org.oscim.utils.Easing;
import org.oscim.utils.ThreadUtils;
import org.oscim.utils.animation.Easing;
import org.oscim.utils.async.Task;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -48,21 +48,21 @@ public class Animator {
public final static int ANIM_TILT = 1 << 3;
public final static int ANIM_FLING = 1 << 4;
private final Map mMap;
final Map mMap;
private final MapPosition mCurPos = new MapPosition();
private final MapPosition mStartPos = new MapPosition();
private final MapPosition mDeltaPos = new MapPosition();
final MapPosition mCurPos = new MapPosition();
final MapPosition mStartPos = new MapPosition();
final MapPosition mDeltaPos = new MapPosition();
private final Point mScroll = new Point();
private final Point mPivot = new Point();
final Point mPivot = new Point();
private final Point mVelocity = new Point();
private float mDuration = 500;
private long mAnimEnd = -1;
private Easing.Type mEasingType = Easing.Type.LINEAR;
float mDuration = 500;
long mAnimEnd = -1;
Easing.Type mEasingType = Easing.Type.LINEAR;
private int mState = ANIM_NONE;
int mState = ANIM_NONE;
public Animator(Map map) {
mMap = map;
@ -251,7 +251,7 @@ public class Animator {
animStart(duration, ANIM_FLING, Easing.Type.SINE_OUT);
}
private void animStart(float duration, int state, Easing.Type easingType) {
void animStart(float duration, int state, Easing.Type easingType) {
if (!isActive())
mMap.events.fire(Map.ANIM_START, mMap.mMapPosition);
mCurPos.copy(mStartPos);
@ -332,7 +332,7 @@ public class Animator {
}
}
private Task updateTask = new Task() {
Task updateTask = new Task() {
@Override
public int go(boolean canceled) {
if (!canceled)
@ -341,7 +341,7 @@ public class Animator {
}
};
private double doScale(ViewController v, float adv) {
double doScale(ViewController v, float adv) {
double newScale = mStartPos.scale + mDeltaPos.scale * Math.sqrt(adv);
v.scaleMap((float) (newScale / mCurPos.scale),

View File

@ -0,0 +1,254 @@
/*
* Copyright 2013 Hannes Janetzek
* Copyright 2016 Stephan Leuschner
* Copyright 2016 devemux86
* Copyright 2016 Izumi Kawashima
* Copyright 2017 Wolfgang Schramm
* Copyright 2018 Gustl22
*
* This file is part of the OpenScienceMap project (http://www.opensciencemap.org).
*
* This program is free software: you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.oscim.map;
import org.oscim.backend.CanvasAdapter;
import org.oscim.core.Point;
import org.oscim.core.Tile;
import org.oscim.renderer.MapRenderer;
import org.oscim.utils.ThreadUtils;
import org.oscim.utils.animation.DragForce;
import org.oscim.utils.animation.Easing;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.oscim.utils.FastMath.clamp;
public class Animator2 extends Animator {
private static final Logger log = LoggerFactory.getLogger(Animator2.class);
private final static int ANIM_KINETIC = 1 << 5;
/**
* The minimum changes that are pleasant for users.
*/
private static final float DEFAULT_MIN_VISIBLE_CHANGE_PIXELS = 0.5f;
private static final float FLING_FRICTION_MOVE = 0.9f;
private final DragForce mFlingRotateForce = new DragForce();
private final DragForce mFlingScaleForce = new DragForce();
private final DragForce mFlingScrollForce = new DragForce();
private final Point mMovePoint = new Point();
private final Point mScrollRatio = new Point();
private long mFrameStart = -1;
private float mScrollDet2D = 1f;
public Animator2(Map map) {
super(map);
}
/**
* @param velocityX the x velocity depends on screen resolution
* @param velocityY the y velocity depends on screen resolution
*/
public void animateFlingScroll(float velocityX, float velocityY,
int xmin, int xmax, int ymin, int ymax) {
ThreadUtils.assertMainThread();
if (velocityX * velocityX + velocityY * velocityY < 2048)
return;
mMap.getMapPosition(mStartPos);
float flingFactor = 2.0f; // Can be changed but should be standardized for all callers
float screenFactor = CanvasAdapter.DEFAULT_DPI / CanvasAdapter.dpi;
velocityX *= screenFactor * flingFactor;
velocityY *= screenFactor * flingFactor;
velocityX = clamp(velocityX, xmin, xmax);
velocityY = clamp(velocityY, ymin, ymax);
float sumVelocity = Math.abs(velocityX) + Math.abs(velocityY);
mScrollRatio.x = velocityX / sumVelocity;
mScrollRatio.y = velocityY / sumVelocity;
mScrollDet2D = (float) (mScrollRatio.x * mScrollRatio.x + mScrollRatio.y * mScrollRatio.y);
mFlingScrollForce.setValueThreshold(DEFAULT_MIN_VISIBLE_CHANGE_PIXELS);
mFlingScrollForce.setFrictionScalar(FLING_FRICTION_MOVE);
mFlingScrollForce.setValueAndVelocity(0f, (float) Math.sqrt(velocityX * velocityX + velocityY * velocityY));
animFlingStart(ANIM_MOVE);
}
private void animFlingStart(int state) {
if (!isActive())
mMap.events.fire(Map.ANIM_START, mMap.mMapPosition);
mCurPos.copy(mStartPos);
mState |= ANIM_FLING | state;
mFrameStart = MapRenderer.frametime; // CurrentTimeMillis would cause negative delta
mMap.render();
}
/**
* @param velocityX the x velocity depends on screen resolution
* @param velocityY the y velocity depends on screen resolution
*/
public void kineticScroll(float velocityX, float velocityY,
int xmin, int xmax, int ymin, int ymax) {
ThreadUtils.assertMainThread();
if (velocityX * velocityX + velocityY * velocityY < 2048)
return;
mMap.getMapPosition(mStartPos);
float duration = 500;
float screenFactor = CanvasAdapter.DEFAULT_DPI / CanvasAdapter.dpi;
velocityX = velocityX * screenFactor;
velocityY = velocityY * screenFactor;
velocityX = clamp(velocityX, xmin, xmax);
velocityY = clamp(velocityY, ymin, ymax);
if (Float.isNaN(velocityX) || Float.isNaN(velocityY)) {
log.debug("fling NaN!");
return;
}
double tileScale = mStartPos.scale * Tile.SIZE;
ViewController.applyRotation(-velocityX, -velocityY, mStartPos.bearing, mMovePoint);
mDeltaPos.setX(mMovePoint.x / tileScale);
mDeltaPos.setY(mMovePoint.y / tileScale);
animStart(duration, ANIM_KINETIC | ANIM_MOVE, Easing.Type.SINE_OUT);
}
/**
* called by MapRenderer at begin of each frame.
*/
@Override
void updateAnimation() {
if (mState == ANIM_NONE)
return;
ViewController v = mMap.viewport();
/* cancel animation when position was changed since last
* update, i.e. when it was modified outside the animator. */
if (v.getMapPosition(mCurPos)) {
log.debug("cancel anim - changed");
cancel();
return;
}
final long currentFrametime = MapRenderer.frametime;
if ((mState & ANIM_FLING) == 0) {
// Do predicted animations
long millisLeft = mAnimEnd - currentFrametime;
float adv = clamp(1.0f - millisLeft / mDuration, 1E-6f, 1);
// Avoid redundant calculations in case of linear easing
if (mEasingType != Easing.Type.LINEAR) {
adv = Easing.ease(0, (long) (adv * Long.MAX_VALUE), Long.MAX_VALUE, mEasingType);
adv = clamp(adv, 0, 1);
}
double scaleAdv = 1;
if ((mState & ANIM_SCALE) != 0) {
scaleAdv = doScale(v, adv);
}
if ((mState & ANIM_KINETIC) != 0) {
adv = (float) Math.sqrt(adv);
}
if ((mState & ANIM_MOVE) != 0) {
v.moveTo(mStartPos.x + mDeltaPos.x * (adv / scaleAdv),
mStartPos.y + mDeltaPos.y * (adv / scaleAdv));
}
if ((mState & ANIM_ROTATE) != 0) {
v.setRotation(mStartPos.bearing + mDeltaPos.bearing * adv);
}
if ((mState & ANIM_TILT) != 0) {
v.setTilt(mStartPos.tilt + mDeltaPos.tilt * adv);
}
if (millisLeft <= 0) {
//log.debug("animate END");
cancel();
}
} else {
// Do physical fling animation
long deltaT = currentFrametime - mFrameStart;
mFrameStart = currentFrametime;
boolean isAnimationFinished = true;
if ((mState & ANIM_SCALE) != 0) {
float valueDelta = mFlingScaleForce.updateValueAndVelocity(deltaT) / 1000f;
float velocity = mFlingScaleForce.getVelocity();
if (valueDelta != 0) {
valueDelta = valueDelta > 0 ? valueDelta + 1 : -1 / (valueDelta - 1);
v.scaleMap(valueDelta, (float) mPivot.x, (float) mPivot.y);
}
isAnimationFinished = (velocity == 0);
}
if ((mState & ANIM_MOVE) != 0) {
float valueDelta = mFlingScrollForce.updateValueAndVelocity(deltaT);
float velocity = mFlingScrollForce.getVelocity();
float valFactor = (float) Math.sqrt((valueDelta * valueDelta) / mScrollDet2D);
float dx = (float) mScrollRatio.x * valFactor;
float dy = (float) mScrollRatio.y * valFactor;
if (dx != 0 || dy != 0) {
v.moveMap(dx, dy);
}
isAnimationFinished = isAnimationFinished && (velocity == 0);
}
if ((mState & ANIM_ROTATE) != 0) {
float valueDelta = mFlingRotateForce.updateValueAndVelocity(deltaT);
float velocity = mFlingRotateForce.getVelocity();
v.rotateMap(valueDelta, (float) mPivot.x, (float) mPivot.y);
isAnimationFinished = isAnimationFinished && (velocity == 0);
}
/*if ((mState & ANIM_TILT) != 0) {
// Do some tilt fling
isAnimationFinished = isAnimationFinished && (velocity == 0);
}*/
if (isAnimationFinished) {
//log.debug("animate END");
cancel();
}
}
/* remember current map position */
final boolean changed = v.getMapPosition(mCurPos);
if (changed) {
mMap.updateMap(true);
} else {
mMap.postDelayed(updateTask, 10);
}
}
}

View File

@ -132,6 +132,9 @@ public abstract class Map implements TaskQueue {
ThreadUtils.init();
mViewport = new ViewController();
if (Parameters.ANIMATOR2)
mAnimator = new Animator2(this);
else
mAnimator = new Animator(this);
mLayers = new Layers(this);

View File

@ -86,12 +86,12 @@ public class ViewController extends Viewport {
* @param mx the amount of pixels to move the map horizontally.
* @param my the amount of pixels to move the map vertically.
*/
public void moveMap(float mx, float my) {
public synchronized void moveMap(float mx, float my) {
ThreadUtils.assertMainThread();
Point p = applyRotation(mx, my);
applyRotation(mx, my, mPos.bearing, mMovePoint);
double tileScale = mPos.scale * Tile.SIZE;
moveTo(mPos.x - p.x / tileScale, mPos.y - p.y / tileScale);
moveTo(mPos.x - mMovePoint.x / tileScale, mPos.y - mMovePoint.y / tileScale);
}
/* used by MapAnimator */
@ -120,18 +120,26 @@ public class ViewController extends Viewport {
mPos.y = mMinY;
}
private synchronized Point applyRotation(double mx, double my) {
if (mPos.bearing == 0) {
mMovePoint.x = mx;
mMovePoint.y = my;
/**
* @param mx the amount of pixels to move the map horizontally.
* @param my the amount of pixels to move the map vertically.
* @param bearing the bearing to rotate the map.
* @param out the position where to move.
*/
public static void applyRotation(double mx, double my, float bearing, Point out) {
if (out == null)
out = new Point();
if (bearing == 0) {
out.x = mx;
out.y = my;
} else {
double rad = Math.toRadians(mPos.bearing);
double rad = Math.toRadians(bearing);
double rcos = Math.cos(rad);
double rsin = Math.sin(rad);
mMovePoint.x = mx * rcos + my * rsin;
mMovePoint.y = mx * -rsin + my * rcos;
out.x = mx * rcos + my * rsin;
out.y = mx * -rsin + my * rcos;
}
return mMovePoint;
}
/**

View File

@ -16,6 +16,11 @@ package org.oscim.utils;
public final class Parameters {
/**
* If true the <code>Animator2</code> will be used instead of default <code>Animator</code>.
*/
public static boolean ANIMATOR2 = false;
/**
* Allow custom tile size instead of the calculated one.
*/

View File

@ -0,0 +1,87 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.oscim.utils.animation;
/**
* See https://developer.android.com/reference/android/support/animation/FlingAnimation.html
* Class android.support.animation.FlingAnimation
*/
public final class DragForce {
private static final float DEFAULT_FRICTION = -4.2f;
private static final float DEFAULT_MIN_VISIBLE_CHANGE_PIXELS = 0.5f;
// This multiplier is used to calculate the velocity threshold given a certain value
// threshold. The idea is that if it takes >= 1 frame to move the value threshold amount,
// then the velocity is a reasonable threshold.
private static final float VELOCITY_THRESHOLD_MULTIPLIER = 1000f / 16f; // 1 frame 16 ms (62.5 fps)
private float mFriction = DEFAULT_FRICTION;
private float mVelocityThreshold = DEFAULT_MIN_VISIBLE_CHANGE_PIXELS * VELOCITY_THRESHOLD_MULTIPLIER;
// Internal state to hold a value/velocity pair.
private float mValue;
private float mVelocity;
public void setFrictionScalar(float frictionScalar) {
mFriction = frictionScalar * DEFAULT_FRICTION;
}
public float getFrictionScalar() {
return mFriction / DEFAULT_FRICTION;
}
/**
* Updates the animation state (i.e. value and velocity).
*
* @param deltaT time elapsed in millisecond since last frame
* @return the value delta since last frame
*/
public float updateValueAndVelocity(long deltaT) {
float velocity = mVelocity;
mVelocity = (float) (velocity * Math.exp((deltaT / 1000f) * mFriction));
float valueDelta = (mVelocity - velocity);
mValue += valueDelta;
if (isAtEquilibrium(mValue, mVelocity)) {
mVelocity = 0f;
}
return valueDelta;
}
public void setValueAndVelocity(float value, float velocity) {
mValue = value;
mVelocity = velocity;
}
public float getValue() {
return mValue;
}
public float getVelocity() {
return mVelocity;
}
public float getAcceleration(float position, float velocity) {
return velocity * mFriction;
}
public boolean isAtEquilibrium(float value, float velocity) {
return Math.abs(velocity) < mVelocityThreshold;
}
public void setValueThreshold(float threshold) {
mVelocityThreshold = threshold * VELOCITY_THRESHOLD_MULTIPLIER;
}
}

View File

@ -15,7 +15,7 @@
* You should have received a copy of the GNU Lesser General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.oscim.utils;
package org.oscim.utils.animation;
import static org.oscim.utils.FastMath.clamp;