Developer

Use Android's scale animation to simulate a 3D flip

App developer William J. Francis presents a tutorial on how to create a flip animation to simulate dealing a card, which is something he's using in his next Android game.

If you read my blog posts very often, you know one of the things I like to do when I'm off from my day gig as a mobile consultant is to play around with Android's animation classes. Last year my 15-year-old son and I wrote a game, and now we are putting the finishing touches on a follow-up title.

As part of our most recent endeavor, we needed to "flip" a tile over when the user tapped on it. The desired effect is similar to that of turning over a playing card, where all the backs look the same, but each face is unique. I began googling Android 3D animations thinking there would be a simple way to pull off the effect using XML transformations and listeners.

Unfortunately, what I found is that to do a "true" 3D flip animation means busting out the android.graphics.Camera package. It requires a matrix, pre and post translations, your own runnable, and a decent amount of the "M" word...math.

Not wanting to get bogged down with a task I'd expected to take no more than a few minutes, I decided for grins I'd try simulating what I was looking for using a simple xScale translation. I didn't have high hopes, but to my delight the resulting animation wasn't half bad (fair to Midland as we say here in Texas). In fact, after a bit of tweaking it actually turned our pretty well!

The tutorial that follows will demonstrate how you can use this simple technique in your own applications. Feel free to follow along or download and import the entire project directly into Eclipse.

1. Create a new Android application in Eclipse. Target Android 2.2 or higher.

2. Create a new folder under/res called /drawable. This is where we will store our card front and back images as PNGs.

3. While we are still in the /res directory, add another folder called /anim. Here we will include two XML files representing our two transitional animations.

to_middle.xml

<?xml version="1.0" encoding="utf-8"?>
<scale
    xmlns:android="http://schemas.android.com/apk/res/android"
       android:fromXScale="1.0" android:toXScale="0.0"
       android:pivotX="50%"
       android:fromYScale="1.0" android:toYScale="1.0"
       android:pivotY="50%"
       android:duration="250" />
from_middle.xml
<?xml version="1.0" encoding="utf-8"?>
<scale
    xmlns:android="http://schemas.android.com/apk/res/android"
       android:fromXScale="0.0" android:toXScale="1.0"
       android:pivotX="50%"
       android:fromYScale="1.0" android:toYScale="1.0"
       android:pivotY="50%"
       android:duration="250" />

4. Because this is a graphics intensive application, one in which we expect the orientation to be maintained throughout, we will open up the AndroidManifest.xml and add the android:screenOrientation="portrait" flag to our main activity.

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.authorwjf.deal"
    android:versionCode="1"
    android:versionName="1.0" >
    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="17" />
    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="com.authorwjf.deal.MainActivity"
            android:label="@string/app_name"
            android:screenOrientation="portrait">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
          </activity>
      </application>
</manifest>

5. In the /res/layout folder, we define the activity as a linear layout with a label, image view, and button all stacked vertically.

activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation = "vertical"
    android:background="#006600"
    android:gravity = "center">
    <TextView
        android:id="@+id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Simulated Card Deal"
        android:textColor="#ffffff"
        android:textSize="26sp"
        android:layout_margin="10dip"
        android:textStyle="bold" />
    <ImageView
        android:id="@+id/imageView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="10dip"
        android:gravity="center"
        android:src="@drawable/card_back" />
    <Button
       android:id="@+id/button1"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_margin="10dip"
       android:padding="10dip"
       android:text="Hit Me!" />
</LinearLayout>

6. It's time to code up our /src/MainActivity.java file. The majority of the code just handles applying our animations and swapping the images between the front and the back of the card when required.

MainActivity.java

package com.authorwjf.deal;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.animation.Animation;
import android.view.animation.Animation.AnimationListener;
import android.view.animation.AnimationUtils;
import android.widget.ImageView;
import android.app.Activity;
public class MainActivity extends Activity implements OnClickListener,
AnimationListener {

       private Animation animation1;
       private Animation animation2;
       private boolean isBackOfCardShowing = true;
       @Override
       protected void onCreate(Bundle savedInstanceState) {
             super.onCreate(savedInstanceState);
             setContentView(R.layout.activity_main);
             animation1 = AnimationUtils.loadAnimation(this, R.anim.to_middle);
             animation1.setAnimationListener(this);
             animation2 = AnimationUtils.loadAnimation(this, R.anim.from_middle);
             animation2.setAnimationListener(this);
             findViewById(R.id.button1).setOnClickListener(this);
       }
       @Override
       public void onClick(View v) {
              v.setEnabled(false);
              ((ImageView)findViewById(R.id.imageView1)).clearAnimation();
              ((ImageView)findViewById(R.id.imageView1)).setAnimation(animation1);
              ((ImageView)findViewById(R.id.imageView1)).startAnimation(animation1);
        }
        @Override
        public void onAnimationEnd(Animation animation) {
              if (animation==animation1) {
                     if (isBackOfCardShowing) {
        ((ImageView)findViewById(R.id.imageView1)).setImageResource(R.drawable.card_front);
                       } else {
        ((ImageView)findViewById(R.id.imageView1)).setImageResource(R.drawable.card_back);
                       }
                       ((ImageView)findViewById(R.id.imageView1)).clearAnimation();
         ((ImageView)findViewById(R.id.imageView1)).setAnimation(animation2);
         ((ImageView)findViewById(R.id.imageView1)).startAnimation(animation2);
                 } else {
                        isBackOfCardShowing=!isBackOfCardShowing;
                        findViewById(R.id.button1).setEnabled(true);
                 }
        }
        @Override
        public void onAnimationRepeat(Animation animation) {
               // TODO Auto-generated method stub
        }
        @Override
        public void onAnimationStart(Animation animation) {
        // TODO Auto-generated method stub
        }
}

Ready to give it a try? Load the APK onto an emulator or device and click the button to deal up the face down card.

Something interesting I discovered while experimenting with the to/from xml files is that even though the Y scale doesn't change, you must include a 1.0 start and finish value for it; otherwise, the animation fails to render with no indication as to why.

About

William J Francis began programming computers at age eleven. Specializing in embedded and mobile platforms, he has more than 20 years of professional software engineering under his belt, including a four year stint in the US Army's Military Intellige...

Editor's Picks