By Kevin Gaudin
8th July 2010.
07:49
Just added to EmailAlbum, now supporting zoom with dynamics, long press, double tap and multitouch.
Thanks a lot for these great tutorials !
I hope there will be more !!!
0
0
Welcome to the fourth and final part of the Android tutorial on how to make your own zoom control like the one used in Sony Ericsson X10 Mini in the Camera and Album applications. Click here to go to the prevoius part of this tutorial. As usual the source code is included, see below. Don’t forgett to download ‘Sony Ericsson Tutorials’ from Android market to see demos of this and other tutorials in action.
[Download] One Finger Zoom sample project – Part 4 (220kb)
In this part we’ll focus on introducing dynamic behavior to our zoom such as fling and bounce by animating the zoom state. Dynamic behavior adds a lot in terms of looks, feedback and usability.
To implement dynamic behavior we’re going to subclass the Dynamics class introduced in the final part of the list tutorial. Make sure to read through that tutorial if you want to know more about the Dynamics base class.
The Dynamics class is useful for applying dynamic behavior to a value, the class itself holds a position and a velocity and functionality for setting min and max positions. When subclassing Dynamics we must implement the onUpdate(int) method that is responsible for updating the state. This gives us control over the dynamic behavior and in our Dynamics sub-class we’ll implement basic friction and spring physics to handle fling and edge bounce. If you want to know more about spring physics then this is a nice place to start.
Let start by defining the necessary attributes for our dynamic behavior. The friction factor will be used to decelerate the fling animation when we are inside the pan limits, and the stiffness and damping factor will be used to simulate a spring pulling the zoom window back to the content if we are outside of the pan limits. To simplify setting up the physics we let the user specify a damping ratio which we then internally recalculate to the damping factor we use in our calculations. Damping ratio is easy to use, basically a value of less than 1 makes the animation overshoot while a value of 1 or more doesn’t. Here you can learn more about damping.
[java]
public class SpringDynamics extends Dynamics {
private float mFriction;
private float mStiffness;
private float mDamping;
public void setFriction(float friction) {
mFriction = friction;
}
public void setSpring(float stiffness, float dampingRatio) {
mStiffness = stiffness;
mDamping = dampingRatio * 2 * (float)Math.sqrt(stiffness);
}
[/java]
Next we’ll implement the onUpdate() method which is responsible for updating the position and velocity of the Dynamics object. The input parameter, dt, is the number of milliseconds passed since the method was previously called. (By the way: dt stands for delta time. Delta is commonly used in mathematics and denotes change and thus: dt = change in time.)
To implement friction and spring physics we need to numerically solve the equations of motion. To do this we’ll use Euler integration. Although it has low accuracy and is prone to instability it works fine in a simple case like this. Here you can read more about numerical integration.
[java]
@Override
protected void onUpdate(int dt) {
final float fdt = dt / 1000f;
final float a = calculateAcceleration();
mPosition += mVelocity * fdt + .5f * a * fdt * fdt;
mVelocity += a * fdt;
}[/java]
Our algorithm requires us to be able to calculate the acceleration at the current position which we’ll do by using spring physics if we’re outside of the pan limits. If we are inside of the pan limits we’ll calculate the acceleration caused by friction as the product of the friction coefficient and the current velocity.
[java]
private float calculateAcceleration() {
float acceleration;
final float distanceFromSnap = getDistanceFromSnap();
if (distanceFromSnap != 0) {
acceleration = distanceFromSnap * mStiffness – mDamping * mVelocity;
} else {
acceleration = -mFriction * mVelocity;
}
return acceleration;
}
[/java]
Once we have the SpringDynamics implementation down we can improve our zoom control. As you might remember from the previous tutorials the zoom control is responsible for imposing limits and other similar logic. Let’s introduce our new SpringDynamics class to the zoom control by creating one of them for each dimension and setting fitting friction and spring coefficients.
[java]
private final SpringDynamics mPanDynamicsX = new SpringDynamics();
private final SpringDynamics mPanDynamicsY = new SpringDynamics();
public DynamicZoomControl() {
mPanDynamicsX.setFriction(2f);
mPanDynamicsY.setFriction(2f);
mPanDynamicsX.setSpring(50f, 1f);
mPanDynamicsY.setSpring(50f, 1f);
}
[/java]
Next, as we no longer will have hard limits on the pan but instead a spring pulling behavior when we are outside the pan limits, we need to modify the pan method:
[java]
private static final float PAN_OUTSIDE_SNAP_FACTOR = .4f;
private float mPanMinX;
private float mPanMaxX;
private float mPanMinY;
private float mPanMaxY;
public void pan(float dx, float dy) {
final float aspectQuotient = mAspectQuotient.get();
dx /= mState.getZoomX(aspectQuotient);
dy /= mState.getZoomY(aspectQuotient);
if (mState.getPanX() > mPanMaxX && dx > 0 || mState.getPanX() < mPanMinX && dx < 0) {
dx *= PAN_OUTSIDE_SNAP_FACTOR;
}
if (mState.getPanY() > mPanMaxY && dy > 0 || mState.getPanY() < mPanMinY && dy < 0) {
dy *= PAN_OUTSIDE_SNAP_FACTOR;
}
final float newPanX = mState.getPanX() + dx;
final float newPanY = mState.getPanY() + dy;
mState.setPanX(newPanX);
mState.setPanY(newPanY);
mState.notifyObservers();
}
[/java]
We will no longer limit the pan by clamping it inside bounds when the user is panning around. Instead we’ll impose a penalty factor when panning outside the bounds giving the user clear visible feedback and a sense of struggling. This also means we’ll no longer need the limitPan() method we used in the previous tutorials. However, we still need to calculate the pan limits so we can do the pull back animation. We’ll replace the limitPan() method with a new method, updatePanLimits(), that calculates the pan limits depending on the zoom level, and then call that method every time the zoom level changes:
[java]
private void updatePanLimits() {
final float aspectQuotient = mAspectQuotient.get();
final float zoomX = mState.getZoomX(aspectQuotient);
final float zoomY = mState.getZoomY(aspectQuotient);
mPanMinX = .5f – getMaxPanDelta(zoomX);
mPanMaxX = .5f + getMaxPanDelta(zoomX);
mPanMinY = .5f – getMaxPanDelta(zoomY);
mPanMaxY = .5f + getMaxPanDelta(zoomY);
}
[/java]
Next we’ll add functionality for starting a fling which will be called when the user lifts his or her finger from the screen after panning.
[java]
private final Handler mHandler = new Handler();
public void startFling(float vx, float vy) {
final float aspectQuotient = mAspectQuotient.get();
final long now = SystemClock.uptimeMillis();
mPanDynamicsX.setState(mState.getPanX(), vx / mState.getZoomX(aspectQuotient), now);
mPanDynamicsY.setState(mState.getPanY(), vy / mState.getZoomY(aspectQuotient), now);
mPanDynamicsX.setMinPosition(mPanMinX);
mPanDynamicsX.setMaxPosition(mPanMaxX);
mPanDynamicsY.setMinPosition(mPanMinY);
mPanDynamicsY.setMaxPosition(mPanMaxY);
mHandler.post(mUpdateRunnable);
}
[/java]
In the startFling() method we prepare the dynamics objects for taking over control of the pan, setting to it the current pan and velocity values as well as the current time. We also set the min and max values of the dynamics object to the current pan limits. Finally we post a runnable that is responsible for updating and animating the zoom state.
[java]
private static final float REST_VELOCITY_TOLERANCE = 0.004f;
private static final float REST_POSITION_TOLERANCE = 0.01f;
private static final int FPS = 50;
private final Runnable mUpdateRunnable = new Runnable() {
public void run() {
final long startTime = SystemClock.uptimeMillis();
mPanDynamicsX.update(startTime);
mPanDynamicsY.update(startTime);
final boolean isAtRest = mPanDynamicsX.isAtRest(REST_VELOCITY_TOLERANCE, REST_POSITION_TOLERANCE) && mPanDynamicsY.isAtRest(REST_VELOCITY_TOLERANCE, REST_POSITION_TOLERANCE);
mState.setPanX(mPanDynamicsX.getPosition());
mState.setPanY(mPanDynamicsY.getPosition());
if (!isAtRest) {
final long stopTime = SystemClock.uptimeMillis();
mHandler.postDelayed(mUpdateRunnable, 1000 / FPS – (stopTime – startTime));
}
mState.notifyObservers();
}
};
[/java]
In the runnable that is posted for animating the pan values we update the dynamics objects and then we set the updated pan values to the ZoomState. Then we check if the animation should go on or by checking if the the dynamics objects are both at rest. If the dynamics objects are not at rest we post this runnable again using a delay calculated to try to meet a chosen FPS. Finally we notify observers of the ZoomState object that it’s been changed, ultimately causing the zoom view to invalidate.
Lastly our dynamic zoom control will need a method for stopping the fling that will be used when the user puts his or her finger down on the screen again after making a fling. Since our fling animation uses a runnable posted on a handler we simply remove the runnable from the handler, thus no longer triggering the automatic zoom state updates and effectively stopping the animation.
[java]
public void stopFling() {
mHandler.removeCallbacks(mUpdateRunnable);
}
[/java]
Alright, so we have implemented dynamic behavior to our zoom control, now lets start using the new methods we created so we can start flinging and bouncing around! To do this we will build on our OnTouchListener implementation and make sure the onTouch method calls our new methods in all the correct places.
The startFling method we declared above takes two velocity parameters, one for each dimension, and luckily the Android platform has just the right tools for calculating the velocity of a fling motion. The onTouch method is will use VelocityTracker for tracking the velocity of motion events by simply adding the events to the tracker object:
[java]
private VelocityTracker mVelocityTracker;
public boolean onTouch(View v, MotionEvent event) {
final int action = event.getAction();
final float x = event.getX();
final float y = event.getY();
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(event);
[/java]
The handling of down events will be largely unchanged with the exception of the call to the stopFling() method on the zoom control, stopping any current animation when the user puts his or her finger back on the screen:
[java]
switch (action) {
case MotionEvent.ACTION_DOWN:
mZoomControl.stopFling();
v.postDelayed(mLongPressRunnable, mLongPressTimeout);
mDownX = x;
mDownY = y;
mX = x;
mY = y;
break;
[/java]
The handling of move events is unchanged since the previous part of the tutorial so lets skip ahead to the handling of up events. Here we will use the velocity tracker for calculating the velocity of the fling event if we were panning around. If we were not panning but instead perhaps zooming or simply in an undefined state, for example if the user has done a quick tap on the screen, we’ll start a fling without velocity. Even if we don’t give the fling any initial velocity it is important that we start it since the pan values might be outside of the limits which makes the springs physics accelerate the pan values, pulling them back within limits.
[java]
case MotionEvent.ACTION_UP:
if (mMode == Mode.PAN) {
mVelocityTracker.computeCurrentVelocity(1000, mScaledMaximumFlingVelocity);
mZoomControl.startFling(-mVelocityTracker.getXVelocity() / v.getWidth(), -mVelocityTracker.getYVelocity() / v.getHeight());
} else {
mZoomControl.startFling(0, 0);
}
mVelocityTracker.recycle();
mVelocityTracker = null;
v.removeCallbacks(mLongPressRunnable);
mMode = Mode.UNDEFINED;
break;
default:
mVelocityTracker.recycle();
mVelocityTracker = null;
v.removeCallbacks(mLongPressRunnable);
mMode = Mode.UNDEFINED;
break;
}
return true;
}
[/java]
Note that the fling uses the negative of the calculated velocities similar to how the negative of the dx and dy values are used in the part of the switch statement that handles move events.
And with that, we’re done! We now have a dynamic zoom with a nicely separated software architecture, and while this is where this tutorial series will end I hope it can be the start of experiments and projects for some of you. In this tutorial series we’ve limited ourselves to zooming in images, but the code can be used to zoom in any application. To do this you need to write your own View subclass and implement the drawing in this view to honor the values you get from ZoomState.
Please feel free to share your zoom experiences, and don’t hesitate to ask any questions.
Good luck!
By Kevin Gaudin
8th July 2010.
07:49
Just added to EmailAlbum, now supporting zoom with dynamics, long press, double tap and multitouch.
Thanks a lot for these great tutorials !
I hope there will be more !!!
0
0
By Andreas Agvard
9th July 2010.
09:59
@Kevin Gaudin
Nice to hear you like the tutorials! I tried your application and I like the idea, you’ve identified a problem and you’re providing a solution. Have you given any thought to giving the user feedback when long pressing for zoom?
Also you can probably enable the user to scroll when fully out zoomed even if zooming out wasn’t accomplished through a double click.
Great work!
0
0
By Jonathan
14th July 2010.
16:29
Nice application.
I have several suggestions for further improvement. Please do get in touch via e-mail to discuss.
Great Work.
0
0
By Kevin Gaudin
18th July 2010.
11:04
@Andreas Agvard : there should be haptic feedback on long press, I use http://developer.android.com/intl/fr/reference/android/view/View.html#performHapticFeedback(int) doesn’t it vibrate on your device ? I am thinking about adding a visual feedback for devices which don’t have a vibrator (Archos 5 IT for example)
I had not noticed that picture switching wasn’t possible anymore without double tap. Thanks for telling, it was possible before I added dynamics.
Thanks a lot for testing !
0
0
By Kevin Gaudin
18th July 2010.
11:07
@Jonathan : I would be really pleased to read your suggestions. Ok to get in touch via email… but what is your email ? Please write to kevin{dot}gaudin{at}gmail{dot}com or use the project issue tracker on http://emailalbum.googlecode.com
(I forgot to tell that EmailAlbum source is open so you cant go see how it works… even if everything is not as clean as this tutorial)
0
0
By jichen
5th August 2010.
09:36
Good lession. But why do not use OnGestureListener? It will simplify the code much in my opinion.
0
0
By Andreas Agvard
9th August 2010.
13:00
@jichen
You are probably right. The reason is that I have got bad experiences with it from sometime early on when I started with Android development. The problems I had then probably don’t exist any longer, so I might as well have used it…
0
0
By Gene
16th August 2010.
03:16
When is one finger zoom coming to X10? This is sorely needed in the browser and mediascapce gallery. Unless X10 screen is actually capable of multitouch. In which case you should enable it quickly.
0
0
By Anders Lindell
23rd August 2010.
13:00
Hi
Do you know more programmers that have used this one finger zoom feature in their Applications?
Anders
0
0
By ricardo
24th August 2010.
16:18
Andreas
One question the one finger zoom will be added as std zooming function in 2.1 update for the mediascape, web browser and maybe at the camera???
Tks.
0
0
Sort by