Monday, 24 August 2009

Android Animations 3D flip

In this post we are going to look at how to create a 3D flip animation, with a FrameLayout.

In the first few posts I've written on Android and animations we have only looked at the predefined animations supplied in the android.view.animations package. In fact we've only used the translate animation, but as I've mentioned before there are also rotate, scale and alpha animations.


In this tutorial i want to take a further look at animations and how we can created our own custom animations using the Android library. I've going to base the tutorial on some of the examples that can be found in the samples folders that are downloaded with the android SDK. These are some great examples, but unfortunately they seem to lack a little on documentation and explanation as to how, and what, the code is doing. Hopefully by the end of this tutorial things should be a little clearer.

Before we start here's an example of the end result of our 3d flip:











So lets get started. First Create a new Android project in Eclipse with these setting:
Project name: Flip3d
Application Name: Flip3d
package name: com.example.flip3d
Activity: Flip3d

I've used some firefox image icons to animate, they are part of a very good and free icon set that can be found here. If you just want the images i've used they can be found here and here. Download these images and place them in the res/drawable folder of the Flip3d project.

Lets start with defining a layout that we can use. As I mentioned we are going to use a FrameLayout. As with the ViewFlipper the FrameLayout can contain a number of child views, but unlike ViewFlipper, FrameLayout stacks it's child views on top of each other with the most recent added child on top. Initially without any intervention all the child views of the FrameLayout are visible, but don't worry about that for now, we will solve this at a later stage. In the FrameLayout we will define two child views, these are the views that we will animate between. In res/layout edit the main.xml file so that it looks like this:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/container"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<include android:id="@+id/notelist" layout="@layout/first_view" />
<include android:id="@+id/notelist" layout="@layout/second_view" />
</FrameLayout>
This is the frame layout and we've specified that it contains two child views, first_view and second_view. Lets Create these views. In res/layout create two files first_view.xml and second_view.xml. The first view file needs to look like this:
<RelativeLayout android:id="@+id/Layout01"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
xmlns:android="http://schemas.android.com/apk/res/android">
<mageView
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:layout_width="wrap_content"
android:id="@+id/ImageView01"
android:layout_height="wrap_content"
android:src="@drawable/firefox"
>
</ImageView>
</RelativeLayout>
And second_view.xml contains this xml:
<RelativeLayout android:id="@+id/Layout02"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
xmlns:android="http://schemas.android.com/apk/res/android">
<ImageView
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:layout_width="wrap_content"
android:id="@+id/ImageView02"
android:layout_height="wrap_content"
android:src="@drawable/firefox_alt"
>
</ImageView>
</RelativeLayout>
These two views are almost identical and only contain two different images that we will flip between.Now we have our layouts defined we need to write some Java to implement our 3d flip. Create a Java class called Flip3dAnimation in the com.example.flip3d package and cut and paste in this code:
package com.example.flip3d;

import android.graphics.Camera;
import android.graphics.Matrix;
import android.view.animation.Animation;
import android.view.animation.Transformation;

public class Flip3dAnimation extends Animation {
private final float mFromDegrees;
private final float mToDegrees;
private final float mCenterX;
private final float mCenterY;
private Camera mCamera;

public Flip3dAnimation(float fromDegrees, float toDegrees,
float centerX, float centerY) {
mFromDegrees = fromDegrees;
mToDegrees = toDegrees;
mCenterX = centerX;
mCenterY = centerY;
}

@Override
public void initialize(int width, int height, int parentWidth, int parentHeight) {
super.initialize(width, height, parentWidth, parentHeight);
mCamera = new Camera();
}

@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
final float fromDegrees = mFromDegrees;
float degrees = fromDegrees + ((mToDegrees - fromDegrees) * interpolatedTime);

final float centerX = mCenterX;
final float centerY = mCenterY;
final Camera camera = mCamera;

final Matrix matrix = t.getMatrix();

camera.save();

camera.rotateY(degrees);

camera.getMatrix(matrix);
camera.restore();

matrix.preTranslate(-centerX, -centerY);
matrix.postTranslate(centerX, centerY);

}

}
Custom Animation class

This class extends the Animation class and implements the applyTransformation method. Each Animation has a transformation object that Defines the transformation to be applied at a point in time of the Animation. When this animation is running the applyTransformation method will be called a number of times to allow us to calculate the transformation to be applied. Each time applyTransformation is called the interpolation value passed in will be increased slightly starting at 0 and ending up at 1. So our float 'degree' at line LINE NUMBER will increase slightly each time this method is called.

The main steps that happen in the applyTransformation are:

Calculate the degrees rotation for the current transformation
Get the tranformation matrix for the Animation
Generate a rotation matrix using camera.rotate(degrees)
Apply that matrix to the Animation transform
Set a pre translate so that the view is moved to the edge of the screen and rotates around it centre and not it's edge
Set a post translate so that the animated view is placed back in the centre of the screen.

Camera in android.graphics

You can see that we also use the android.graphics.Camera class, don't get this confused with camera class in android.hardware which is used to control the Camera on the android device. The Camera class in the android.graphic package is very different, it is used to calculate 3d transformations that can then be applied to animations. This Camera class represents a virtual view as if we were looking at our Android views through a camera. As we move our virtual camera to the left , we have the effect of the android view moving to the right , and if we rotate our virtual camera, we have the effect of rotating our Android view. Here the Camera class is use to calculate a rotate transformation about the Y axis, it does this every time the applyTransformation method is called and so gives each incremental transformation that is needed to give a smooth rotation effect. Camera uses transformation matrices to store and calculate transforms. It's not really necessary to have an in depth knowledge of how transformation matricies work, but a good article on them can be found here. The article is based around flash, but the same principles apply.

Now that we've got our rotation animation we need to come up with a plan of how to use it. We currently have two images but just rotating these isn't going to give us the effect we want. So what we need to do is hide the second image and display only the first. We will then rotate this image through 90 degree until it's edge on and we can no longer see it. At this point we will make the first image invisible and the second image visible, we will start the animation of the second image at a 90 degree angle and then roatate round until it is fully visible. To go back from the second to the first image we will just reverse the process.

We'll start with our main Activity class Flip3d.java:

package com.example.flip3d;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import android.widget.ImageView;

public class Flip3d extends Activity {


private ImageView image1;
private ImageView image2;

private boolean isFirstImage = true;


/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

image1 = (ImageView) findViewById(R.id.ImageView01);
image2 = (ImageView) findViewById(R.id.ImageView02);
image2.setVisibility(View.GONE);

image1.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
if (isFirstImage) {
applyRotation(0, 90);
isFirstImage = !isFirstImage;

} else {
applyRotation(0, -90);
isFirstImage = !isFirstImage;
}
}
});
}

private void applyRotation(float start, float end) {
// Find the center of image
final float centerX = image1.getWidth() / 2.0f;
final float centerY = image1.getHeight() / 2.0f;

// Create a new 3D rotation with the supplied parameter
// The animation listener is used to trigger the next animation
final Flip3dAnimation rotation =
new Flip3dAnimation(start, end, centerX, centerY);
rotation.setDuration(500);
rotation.setFillAfter(true);
rotation.setInterpolator(new AccelerateInterpolator());
rotation.setAnimationListener(new DisplayNextView(isFirstImage, image1, image2));

if (isFirstImage)
{
image1.startAnimation(rotation);
} else {
image2.startAnimation(rotation);
}

}
}
In the main activity class we have the usual onCreate method. In this method we get references to the two images that we are displaying. Here we also set the visibility of our second image to View.Gone. The final thing to be done in onCreate is to set up a click listener for our fist image (we don't need a click listener for the second image). In the click listener we have a boolean, isFirstImage, which tells us which image is currently visible, if it's true the first image is visible and if false the second image is visible. Depending on which image is visible, we call the applyRotation method with different start and end values, since we rotate the first image in a different direction to the second image.

The applyRotation method is fairly straight forward, we find the centre of our image, create a new instance of our Flip3dAnimation, and apply and start it for the visible image. Well, there's a little more yet... As you may have already noticed we've only done half the animation here, from 0 to 90 degrees, we still have another 90 degrees to go, and we still have to swap the images over half way through. So this is were we need an Animation listener, which we are going to call DisplayNextView. Create a class of this name and paste in this code:
package com.example.flip3d;

import android.view.animation.Animation;
import android.widget.ImageView;

public final class DisplayNextView implements Animation.AnimationListener {
private boolean mCurrentView;
ImageView image1;
ImageView image2;

public DisplayNextView(boolean currentView, ImageView image1, ImageView image2) {
mCurrentView = currentView;
this.image1 = image1;
this.image2 = image2;
}

public void onAnimationStart(Animation animation) {
}

public void onAnimationEnd(Animation animation) {
image1.post(new SwapViews(mCurrentView, image1, image2));
}

public void onAnimationRepeat(Animation animation) {
}
}
The DisplayNextView Animation Listener is very simple and just listens for the end of the rotate animation, Since out first rotate animation only goes from 0 to 90 onAnimatonEnd method will be called when our first image is at 90 degrees. So now we need to Swap the images and this is were out SwapViews class comes in. So create a class called swap views and paste in this code:
package com.example.flip3d;

import android.view.View;
import android.view.animation.DecelerateInterpolator;
import android.widget.ImageView;

public final class SwapViews implements Runnable {
private boolean mIsFirstView;
ImageView image1;
ImageView image2;

public SwapViews(boolean isFirstView, ImageView image1, ImageView image2) {
mIsFirstView = isFirstView;
this.image1 = image1;
this.image2 = image2;
}

public void run() {
final float centerX = image1.getWidth() / 2.0f;
final float centerY = image1.getHeight() / 2.0f;
Flip3dAnimation rotation;

if (mIsFirstView) {
image1.setVisibility(View.GONE);
image2.setVisibility(View.VISIBLE);
image2.requestFocus();

rotation = new Flip3dAnimation(-90, 0, centerX, centerY);
} else {
image2.setVisibility(View.GONE);
image1.setVisibility(View.VISIBLE);
image1.requestFocus();

rotation = new Flip3dAnimation(90, 0, centerX, centerY);
}

rotation.setDuration(500);
rotation.setFillAfter(true);
rotation.setInterpolator(new DecelerateInterpolator());

if (mIsFirstView) {
image2.startAnimation(rotation);
} else {
image1.startAnimation(rotation);
}
}
}
SwapViews does exactly what its name suggests and swaps the images, setting one image to invisible and the other to visible depending on which image was already visable, but it also does one last important thing, and that is to create and apply the last half of the animation.

You should now have a working flip animation. This technique can also be applied to ViewGroups , so it could be used as a way to transition between different views. There are a few more things that we can do to this animation to improve it such as adding a depth effect, but I'll leave that to another post. Like I mentioned earlier, this example is based on the samples that are in the downloadable SDK files, so have a look at these samples as well.

43 comments:

  1. Thanks heaps for sharing this - works great. I've got it working with interactive views but noticed to allow the visible view to be interactive you also need to add bringToFront();
    i.e
    if (mIsFirstView) {

    view1.setVisibility(View.GONE);
    view2.setVisibility(View.VISIBLE);
    view2.bringToFront();
    rotation = new Flip3dAnimation(-90, 0, centerX, centerY);

    } else {
    view2.setVisibility(View.GONE);
    view1.setVisibility(View.VISIBLE);
    view1.bringToFront();
    rotation = new Flip3dAnimation(90, 0, centerX, centerY);

    }

    ReplyDelete
  2. Thanks a lot for sharing. Helped a lot :)

    ReplyDelete
  3. Excellent help!
    Probably the best android tutorial I have seen since I started developing.
    Thanks!

    ReplyDelete
  4. Thanks a lot - though on the iPhone this particular animation (and a few others) can be done much much easier ... but of course with Android you have way more flexibility.

    ReplyDelete
  5. Excellent tutorial thanks for posting it.....

    ReplyDelete
  6. Great tutorial! You really help me so much!
    Many thanks!
    Enjoy programming and post more so excellent tutorials!

    ReplyDelete
  7. Thanks a lot! it's hard to find a decent tutorial...Again ! thank you very much

    ReplyDelete
  8. How would I make this work with a grid of images so that each image in the grid flips?

    ReplyDelete
  9. Nice! Working like a charm! Thank you!

    ReplyDelete
  10. Great example, thanks! any ideas how can I rotate the images in loop (by clicking once).

    ReplyDelete
  11. Hello Neil,

    We just released with New BSD licence a reusable widget based largely on your code, we made some changes and improvements, added our build system and so on..... and will add more still. I thought you might want to know it. We made a contribution to your work in the description of the project and linked to your page. I hope you don't mind and other's can use it:
    Project here: http://code.google.com/p/android-flip3d/

    ReplyDelete
  12. Having trouble starting an activity after the first flip. Any ideas where I would insert the startActivity line at?

    ReplyDelete
  13. Great tutorial. Can anyone help me how to make this animation in an infinite loop? And stop it when I click for the second time

    ReplyDelete
  14. I really appreciate your work.

    ReplyDelete
  15. how about if i want to flip3d a layout that contains some views?
    i try change ImageView using RelativeLayout that contain some button n text
    but not succeed.
    please help me.

    thanks in advance

    ReplyDelete
  16. I'm with the same doubt of Richy.
    How can i rotate a layout that contains some views?
    For example, i had an image with a button on top and a title (TextView) on bottom, how can i rotate all this views?
    Thanks

    ReplyDelete
  17. when i use a Button instead of ImageView, the first rotate (button1 to button2) was complete, but when i click on the button2, the rotate can't be display. Someone know what is happening?

    ReplyDelete
  18. How would I make this work with a grid of images so that each image in the grid flips?

    ReplyDelete
  19. i have the same doubt of Richy... how can i use the flip effect but applying it to the all screen (layout)?

    ReplyDelete
  20. Tiago (are you brazilian?)
    Falo em português ou inglês?
    UehAUHeUAHEuhAE
    This effects is able to be applied on any view.
    If you aply this effect on a LinearLayout, all the layout will be rotated.
    I did a test, and it worked fine.

    ReplyDelete
  21. Thank you for you reply :)
    Podemos falar em portuguÊs sim.
    Uma duvida, eu consegui implementar este efeito entre viewgroups, mas tem um probleminha. Eu tenho um cartao no centro do ecrã sob um fundo branco (que é o background), quando acontece o efeito de rotação, o cartão muda da frente para o verso. O problema é que quando o cartão vira, por vezes (não sempre :S) vai deixando umas barrinhas de pixels verticais, que depois desaparecem mas sao visiveis e fica muito feio assim.

    Alguma ideia porque acontece e como posso resolver este problema?

    ReplyDelete
  22. I got to this post from:
    http://www.inter-fuser.com/2009/07/android-transistions-slide-in-and-slide.html

    which said that this post would show how to do a slide-in/out using more XML and less Java.

    Am I confused?
    My client wants a ton of slides, so I have to find a super concise way to do them.

    Q: Does this tutorial apply to slide-in as well as 3d-flip?

    thanks for posting!
    jim

    ReplyDelete
  23. Hello,
    thats an excellent tutorial....
    although I want to perform exactly opposite of this.
    I mean when the application starts the image should start flipping continuously without clicking on it and when I click on it, it should stop flipping.
    So how to deal with this?
    Please help.
    will be grateful.
    thank you!
    -tejas

    ReplyDelete
  24. Hi..Its an excellent one. But I need to rotate two or more layouts... how can do this.

    ReplyDelete
  25. Put the views in a viewgroup, such as a linearlayout and then apply the rotation to the linearlayout.

    ReplyDelete
  26. Hi ! Thanks for the code!
    I've managed to implement it for changing between layout, but I can't seem to make it work for activities.
    That is - Calling start activity will create a new screen and override the flip.
    Can this be done using the above code?
    I've tried placing it in different places but to no avail.
    Please HELP !!!!

    ReplyDelete
  27. Thanks for code.
    I want to execute the both the animation at same time.But it is not working .Will u please help me.

    ReplyDelete
  28. i have few doubts regarding 3d Animation. i have created imageview in my application is there anyway in android rotate the image on 3d Animation please help me on this.

    Regards
    Raj.

    ReplyDelete
  29. Great Post, very good Job, you seems to be a pro', i was honored to study from this post,
    many thanks.

    ReplyDelete
  30. OMG, this is done with one line of code in ActionScript :)
    Thanks, great tutorial!

    ReplyDelete
  31. Thanks a lot Neil Davies sir that was an awesome tutorial works like charm..!!

    Regards:
    Gaurav Chawla

    ReplyDelete
  32. I'd like to use this as a view transition (vs. flipping over an image). Can this technique be applied to that with minimal adjustments, or does it come with significant performance hits?

    ReplyDelete
  33. Yes you can apply this and use it as a transition. Might need to make sure you have hardware acceleration enable in the manifest for devices with high resolutions.

    ReplyDelete
  34. Hello, I am trying to add this effect to a Gallery when clicking on the selected item. How could I achieve this?

    ReplyDelete
  35. Thank you ^^..

    ReplyDelete
  36. i run this app with android 2.1 and compile it with emulator 2.1 and 2.2 and it doesn't work
    what should i do ?

    ReplyDelete
  37. EXTREMELY helpful. Took awhile to digest, and also noticed a couple of typos that threw me for a while. Thank you so much.

    ReplyDelete
  38. Thanks for the brilliant tutorial!

    Btw, you have a little typo. In your xml Code for "first_view" it says "mageView" instead of ImageView.
    And I was not able to send a comment using Chrome, because the captcha is out of reach;)

    I would like to call the rotation multiple times. But if I do that, all the rotations seem to be done simultaniously. How might I prevent that from happening?

    ReplyDelete
  39. Hi,
    i' m a realy newbe in android and mybe my question is a litle freaky.
    I look for a way to make a small memory Game for my son and i have make it with button is the a way to make this animation with a button or is there a way to release it with the Views ?
    Great sample thx
    Dusan

    ReplyDelete
  40. @Dusan I think if you replace all ImageViews with Buttons in Flip3dAnimation and SwapViews class same code will work.

    ReplyDelete