Mobility

Building a slot machine in Android: ViewFlipper meet gesture detector

William Francis combines Android's ViewFlipper class with the gesture detector to simulate the spinning wheel on a game of chance. Follow the steps or download this fun project.

I've been to Vegas a few times, though I don't much enjoy gambling. The one time I won $150 from a slot machine I immediately cashed out and used the money to buy front row seats to a Penn & Teller magic show. The autographed ticket stub is tucked away amongst my most prized possessions — right next to the Jayne hat I picked up when Adam Baldwin came to Dallas Comic Con a few years ago. I digress.

In last week's post we spent time poking at Android's gesture detector. In my closing remarks I suggested that the gesture API might be used for spinning the wheels of a slot machine, and that got me to thinking. While a lot of my TechRepublic tutorials stem from real-life challenges I encounter as part of my full-time gig as an Android consultant, once in a while I use the blog as an outlet to try something for no reason other than I think it would be fun.

So while I don't really enjoy playing the slots with my own money, the engineer in me was intrigued and a little excited about the idea of combining Android's ViewFlipper class with the gesture detector to simulate the spinning wheel on a game of chance. I needed to come up with a way to apply the animations dynamically, as well as an algorithm for calculating the number of rotations... oh and let's not forget a method to decrease velocity over time based on how fast or slow the user flings the wheel. See, I told you it sounds like fun.

Are you still with me? If so, follow along with the step-by-step tutorial below or download and import the entire project directly into Eclipse.

1. Create a new project in Eclipse targeting Android 1.6 or higher. Be sure to rename the startup activity to Main.java.

2. We never want our slot machine app to run in anything other than portrait mode, so modify your AndroidManifest.xml file to swallow orientation changes.

AndroidMainfest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.authorwjf.slot"
    android:versionCode="1"
    android:versionName="1.0" >
    <uses-sdk android:minSdkVersion="4" />
    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:name=".Main"
            android:configChanges="orientation"
                 android:screenOrientation="portrait"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

3. You need to create a /drawable folder under your /res directory and add three PNG images — any fruit will do. I found public domain images of a cherry, a lemon, and a pear. Mmmmm!

4. Defining the layout for our slot machine is a little more complex than what I normally try to do in these tutorials; the reason is the ViewFlipper. The good news is because so much of the behavior of the ViewFlipper is defined in XML, when we get around to using it there is almost no code associated with the widget. For now, just open /res/layout/main.xml and paste in the following layout.

main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >
    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:textSize="28sp"
        android:paddingTop="10dip"
        android:textColor="#00ff00"
        android:text="Slot Machine Example"
        android:paddingBottom="50dip" />
    <ViewFlipper
        android:layout_margin="6dip"
                android:id="@+id/view_flipper"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:gravity="center">
                <LinearLayout
                        android:layout_width="fill_parent"
                        android:layout_height="fill_parent"
                        android:orientation="vertical"
                        android:layout_gravity="center"
                        android:gravity="center">
                       <ImageView
                               android:layout_height="wrap_content"
                               android:layout_width="fill_parent"
                               android:layout_gravity="center"
                               android:gravity="center"
                               android:src="@drawable/pear"/>
              </LinearLayout>
              <LinearLayout
                      android:layout_width="fill_parent"
                      android:layout_height="fill_parent"
                      android:orientation="vertical"
                      android:layout_gravity="center"
                      android:gravity="center">
                      <ImageView
                              android:layout_height="wrap_content"
                              android:layout_width="fill_parent"
                              android:layout_gravity="center"
                              android:src="@drawable/cherry"/>
           </LinearLayout>
           <LinearLayout
                   android:layout_width="fill_parent"
                   android:layout_height="fill_parent"
                   android:orientation="vertical"
                   android:layout_gravity="center"
                   android:gravity="center">
                   <ImageView
                           android:layout_height="wrap_content"
                           android:layout_width="fill_parent"
                           android:layout_gravity="center"
                           android:src="@drawable/lemon"/>
            </LinearLayout>
       </ViewFlipper>
       <TextView
           android:layout_width="fill_parent"
           android:layout_height="wrap_content"
           android:id="@+id/velocity"
           android:gravity="center"/>
      <TextView
           android:layout_width="fill_parent"
           android:layout_height="wrap_content"
           android:id="@+id/counter"
           android:gravity="center"/>
      <TextView
          android:layout_width="fill_parent"
          android:layout_height="wrap_content"
          android:id="@+id/speed"
          android:gravity="center"/>
</LinearLayout>

5. Now it's time to get coding! In the /src folder open Main.java and begin by creating some class variables and initializing them in the on create override.

Main.java
package com.authorwjf.slot;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.GestureDetector.OnGestureListener;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.Animation;
import android.view.animation.TranslateAnimation;
import android.widget.TextView;
import android.widget.ViewFlipper;
public class Main extends Activity implements OnGestureListener {
        private ViewFlipper mViewFlipper;
        private GestureDetector mDetector;
        private int mSpeed;
        private int mCount;
        private int mFactor;
        private boolean mAnimating;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        mViewFlipper = (ViewFlipper) findViewById(R.id.view_flipper);
        mDetector = new GestureDetector(this);
        mAnimating = false;
        mCount = 0;
        mSpeed = 0;
     }
}

6. If you remember from last week, letting the main activity implement the on gesture listener means we need to add seven call back functions — five of which we can just let the Android Eclipse plug-in generate for us.

@Override
public void onLongPress(MotionEvent arg0) {
        // TODO Auto-generated method stub
}
@Override
public boolean onScroll(MotionEvent arg0, MotionEvent arg1, float arg2,
        float arg3) {
        // TODO Auto-generated method stub
        return false;
}
@Override
public void onShowPress(MotionEvent arg0) {
        // TODO Auto-generated method stub
}
@Override
public boolean onSingleTapUp(MotionEvent arg0) {
        // TODO Auto-generated method stub
        return false;
}
@Override
public boolean onDown(MotionEvent arg0) {
        // TODO Auto-generated method stub
        return false;
}

7. The remaining gesture-related overrides require custom code. The first propagates the touch event to the gesture detector, while the second implements our on fling logic. You may have to read through the on fling logic a couple times; it's responsible for initiating a number of rotations, as well as the start speed and timed decrease factor, based on the initial vertical velocity argument. The try / catch prevents a divide by zero error that could occur if a user swiped the display painfully slow, thus incurring a near zero velocity rate.  Since we are talking about the Y-axis, anything more than 0 is considered a downward swipe; otherwise, we assume up.

@Override
public boolean onTouchEvent(MotionEvent me) {
        return mDetector.onTouchEvent(me);
}
@Override
public boolean onFling(MotionEvent start, MotionEvent finish, float xVelocity, float yVelocity) {
        try {
                 if (mAnimating) return true;
                 mAnimating = true;
                 mCount = (int) Math.abs(yVelocity) / 300;
                 mFactor = (int) 300 / mCount;
                 mSpeed = mFactor;
                 if (yVelocity>0) {
                        //down
                        Handler h = new Handler();
                     h.postDelayed(r2, mSpeed);
                 } else {
                         //up
                         Handler h = new Handler();
                     h.postDelayed(r1, mSpeed);
                  }
                  ((TextView)findViewById(R.id.velocity)).setText("VELOCITY => "+Float.toString(yVelocity));
           } catch (ArithmeticException e) {
                   //swiped too slow doesn't register
                   mAnimating = false;
           }
           return true;
}

8. As is frequently the case with Android, some of the most difficult logic comes into play when applying animations. For this purpose I've employed two matching pairs of routines. For handling the up gesture I have a runnable along with the dynamic animations. Since animation timing doesn't need to be exact I am using a handler and chaining posts.

private Runnable r1 = new Runnable() {
@Override
public void run() {
        up();
                if (mCount<1) {
                        mAnimating = false;
                } else {
                       Handler h = new Handler();
                       h.postDelayed(r1, mSpeed);
                }
        }
};
private void up() {
               mCount—;
               mSpeed+=mFactor;
               Animation inFromBottom = new TranslateAnimation(
                                        Animation.RELATIVE_TO_PARENT, 0.0f,
                                        Animation.RELATIVE_TO_PARENT, 0.0f,
                                        Animation.RELATIVE_TO_PARENT, 1.0f,
                                        Animation.RELATIVE_TO_PARENT, 0.0f);
              inFromBottom.setInterpolator(new AccelerateInterpolator());
              inFromBottom.setDuration(mSpeed);
              Animation outToTop = new TranslateAnimation(
                                       Animation.RELATIVE_TO_PARENT, 0.0f,
                                       Animation.RELATIVE_TO_PARENT, 0.0f,
                                       Animation.RELATIVE_TO_PARENT, 0.0f,
                                       Animation.RELATIVE_TO_PARENT, -1.0f);
              outToTop.setInterpolator(new AccelerateInterpolator());
              outToTop.setDuration(mSpeed);
              mViewFlipper.clearAnimation();
              mViewFlipper.setInAnimation(inFromBottom);
              mViewFlipper.setOutAnimation(outToTop);
              if (mViewFlipper.getDisplayedChild()==0) {
                      mViewFlipper.setDisplayedChild(2);
             } else {
                     mViewFlipper.showPrevious();
             }
             ((TextView)findViewById(R.id.counter)).setText("COUNTER => "+Integer.toString(mCount));
             ((TextView)findViewById(R.id.speed)).setText("SPEED => "+Integer.toString(mSpeed));
}

9. Not withstanding the direction, we have two nearly identical functions to handle a downward spin of our wheel.

private Runnable r2 = new Runnable() {
@Override
public void run() {
        down();
               if (mCount<1) {
                       mAnimating = false;
               } else {
                      Handler h = new Handler();
                      h.postDelayed(r2, mSpeed);
               }
        }
};
private void down() {
                 mCount—;
                 mSpeed+=mFactor;
                 Animation outToBottom = new TranslateAnimation(
                                 Animation.RELATIVE_TO_PARENT, 0.0f,
                                 Animation.RELATIVE_TO_PARENT, 0.0f,
                                 Animation.RELATIVE_TO_PARENT, 0.0f,
                                 Animation.RELATIVE_TO_PARENT, 1.0f);
                outToBottom.setInterpolator(new AccelerateInterpolator());
                outToBottom.setDuration(mSpeed);
                Animation inFromTop = new TranslateAnimation(
                                Animation.RELATIVE_TO_PARENT, 0.0f,
                                Animation.RELATIVE_TO_PARENT, 0.0f,
                                Animation.RELATIVE_TO_PARENT, -1.0f,
                                Animation.RELATIVE_TO_PARENT, 0.0f);
                inFromTop.setInterpolator(new AccelerateInterpolator());
                inFromTop.setDuration(mSpeed);
                mViewFlipper.clearAnimation();
                mViewFlipper.setInAnimation(inFromTop);
                mViewFlipper.setOutAnimation(outToBottom);
                if (mViewFlipper.getDisplayedChild()==0) {
                        mViewFlipper.setDisplayedChild(2);
                } else {
                       mViewFlipper.showPrevious();
                }
                ((TextView)findViewById(R.id.counter)).setText("COUNTER => "+Integer.toString(mCount));
                ((TextView)findViewById(R.id.speed)).setText("SPEED => "+Integer.toString(mSpeed));
}

Jackpot! While the final solution ended up being more code than I imagined at the onset, the engineer in me is satisfied with the results. The animation is pretty smooth on my Nexus S running Jelly Bean. If you've got an older phone I'd be curious to know how it looks. Be sure to post any thoughts about this topic in the discussion thread. In the meantime I'm going to spin the wheel a few more times and then sign off.

About William J. Francis

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

Free Newsletters, In your Inbox