Tuesday, July 8, 2008

Motion Madness - Physics Gone Wild


Our LWUIT lists and transitions move according to various physics based algorithms allowing the motion to be very fluid and smooth. This is enabled by the Motion class which encapsulates physical motion properties, however to someone who never dealt with physics based animations this might seem like a very odd class.

First lets explain some animation basics, every animation has a state this might seem obvious but isn't as clear cut. The state indicates how we draw the animation but isn't the actual animation frame, e.g. say we want to move a ball across the screen. The state can be its X/Y coordinate, if we would update the state in the paint method the ball will move fast on a fast device and slow in a slow device. Worse, it will have linear or erratic motion rather than a smooth motion.

LWUIT solves this by providing the animate() method which is invoked in fixed intervals allowing you to update animation state. If animation state hasn't changed just return false from animate and no repaint will occur. How does all this fit with Motion?

Think of motion as a physics equation that allows you to calculate acceleration and deceleration of an animation on a single axis. So if we have a list scrolling and we want the scroll to accelerate/decelerate as it moves we can just create a spline motion (representing well known equation for such physical movement) and rather than immediately paint the updated position we can use animate to update the position based on the motion.

To demonstrate this I created a simple demo allowing you to view an image larger than the screen and move it using the motion class. Moving with the arrow keys will produce a smooth motion effect, you can also "flick" your finger to physically move the image with some friction using velocity.
/**
* A component that allows us to drag an image file with a physical drag motion
* effect.
*
* @author Shai Almog
*/

public class MotionComponent extends Component {
private Image img;
private int positionX;
private int positionY;
private Motion motionX;
private Motion motionY;
private int destX;
private int destY;
private static final int TIME = 800;
private static final int DISTANCE_X = Display.getInstance().getDisplayWidth() / 3;
private static final int DISTANCE_Y = Display.getInstance().getDisplayHeight() / 3;
private int dragBeginX = -1;
private int dragBeginY = -1;
private int dragCount = 0;

public MotionComponent(Image img) {
this.img = img;
}

protected Dimension calcPreferredSize() {
Style s = getStyle();
return new Dimension(img.getWidth() + s.getPadding(LEFT) + s.getPadding(RIGHT),
img.getHeight() + s.getPadding(TOP) + s.getPadding(BOTTOM));
}

public void initComponent() {
getComponentForm().registerAnimated(this);
}

public void paint(Graphics g) {
Style s = getStyle();
g.drawImage(img, getX() - positionX + s.getPadding(LEFT), getY() - positionY + s.getPadding(TOP));
}

public void keyPressed(int keyCode) {
super.keyPressed(keyCode);
switch(Display.getInstance().getGameAction(keyCode)) {
case Display.GAME_DOWN:
destY = Math.min(destY + DISTANCE_Y, img.getHeight() - Display.getInstance().getDisplayHeight());
motionY = Motion.createSplineMotion(positionY, destY, TIME);
motionY.start();
break;
case Display.GAME_UP:
destY = Math.max(destY - DISTANCE_Y, 0);
motionY = Motion.createSplineMotion(positionY, destY, TIME);
motionY.start();
break;
case Display.GAME_LEFT:
destX = Math.max(destX - DISTANCE_X, 0);
motionX = Motion.createSplineMotion(positionX, destX, TIME);
motionX.start();
break;
case Display.GAME_RIGHT:
destX = Math.min(destX + DISTANCE_X, img.getWidth() - Display.getInstance().getDisplayWidth());
motionX = Motion.createSplineMotion(positionX, destX, TIME);
motionX.start();
break;
default:
return;
}
}

public void pointerDragged(int x, int y) {
if(dragBeginX == -1) {
dragBeginX = x;
dragBeginY = y;
}
positionX = Math.max(0, Math.min(positionX + x - dragBeginX, img.getWidth() - Display.getInstance().getDisplayWidth()));
positionY = Math.max(0, Math.min(positionY + y - dragBeginY, img.getHeight() - Display.getInstance().getDisplayHeight()));
dragCount++;
}

public void pointerReleased(int x, int y) {
// this is a result of a more significant drag operation, some VM's always
// send a pointerDragged so we should ignore too few drag events
if(dragCount > 4) {
float velocity = -0.2f;
if(dragBeginX < x) {
velocity = 0.2f;
}
motionX = Motion.createFrictionMotion(positionX, velocity, 0.0004f);
motionX.start();

if(dragBeginY < y) {
velocity = 0.2f;
} else {
velocity = -0.2f;
}
motionY = Motion.createFrictionMotion(positionY, velocity, 0.0004f);
motionY.start();
}
dragCount = 0;
dragBeginX = -1;
dragBeginY = -1;
}

public boolean animate() {
boolean val = false;
if(motionX != null) {
positionX = motionX.getValue();
if(motionX.isFinished()) {
motionX = null;
}
// velocity might exceed image bounds
positionX = Math.max(0, Math.min(positionX, img.getWidth() - Display.getInstance().getDisplayWidth()));
val = true;
}
if(motionY != null) {
positionY = motionY.getValue();
if(motionY.isFinished()) {
motionY = null;
}
positionY = Math.max(0, Math.min(positionY, img.getHeight() - Display.getInstance().getDisplayHeight()));
val = true;
}
return val;
}
}
To use this component just use this code:
Form motionDrag = new Form("Motion");
motionDrag.setLayout(new BorderLayout());
motionDrag.addComponent(BorderLayout.CENTER, new MotionComponent(Image.createImage("/setu_bandhasana.jpg")));
motionDrag.show();
How does this work?

Look at this snippet from keyPressed:
case Display.GAME_RIGHT:
destX = Math.min(destX + DISTANCE_X, img.getWidth() - Display.getInstance().getDisplayWidth());
motionX = Motion.createSplineMotion(positionX, destX, TIME);
motionX.start();
We update the destX variable which is never used to draw, the positionX is the position where we actually draw the image. Then we create and start a spline motion over time.
Now in the animate method we have the following:
positionX = motionX.getValue();
if(motionX.isFinished()) {
motionX = null;
}
// velocity might exceed image bounds
positionX = Math.max(0, Math.min(positionX, img.getWidth() - Display.getInstance().getDisplayWidth()));
val = true;
We extract the current value from the motion (based on the current time) and update the position drawn on the screen accordingly. We make sure to repaint our changes by returning true from animate.

Motion for touch screens includes velocity and other complexities I don't want to get into (find a physicist...), but the general idea is similar. Most of the code above relates to the fact that we can move in 4 directions using both the touch screen and the keyboard.

8 comments:

  1. Hola. Tengo una lista, el problema es que cuando pulso ok sobre un item de la lista, esta se me queda pegada, veo de que en la demo tambien ocurre lo mismo.

    Salve el que se pegue cuando llega a los extremos (inferior y superior) captando el keyPressed, pero no tengo como salvar el pulsado del ok (FIRE) sobre algun Item :(

    Que me recomiendas para salvar ese caso?.

    Mil gracias.

    Rodolfo Burlando Makthon
    Lima - PerĂș

    ReplyDelete
  2. Please use English. I don't speak Spanish.

    ReplyDelete
  3. Hello. I have a list, the problem is that when I click ok on a list item, that I was stuck, I see that the demo is also true.

    Hail to paste when it reaches the ends (lower and upper) capturing the keyPressed, but I do not like the bridge down from ok (FIRE) on any Item: (

    I recommend to save this case?.

    ReplyDelete
  4. As I put an animated gif while in another thread that connects the application to the network?

    ReplyDelete
  5. Hi!

    Any idea on how I can take into account the height taken by the command bar?

    As this it is, the bottom lines of the image are behind the command bar.

    Thanks for any help.

    ReplyDelete
  6. When using the component height/width variables the menubar height is calculated for you.

    ReplyDelete
  7. Hello, I have tested this code in LWUIT 1.3, 1.4 and latest version from SVN and scroll size is not correctly calculated. Is this a regression or am I doing something wrong?

    Thanks in advance

    ReplyDelete
  8. Its possible there is a bug in this code that's exposed by newer versions of LWUIT. After all this is very old code by now...

    ReplyDelete