Circles in Android Java

In Java programming language we can also write programs for Android operating system used by many phones and tablets. So is it enough to write a program once and run it everywhere? No, it is not that easy. A mobile device is not the same thing as a computer; it is a bit different tool, it is used a bit differently, and this requires some changes in writing programs. Also, mobile devices put greater emphasis on using less memory, which causes additional changes.

To try the similarities and differences between the standard Java and the Android Java, I converted the program from "Circles in Java" article to Android. Unlike in standard Java, I don't have experience with Android Java, so the program probably has some deficiencies I am not aware of. The important thing is that it works, and it gives to a beginner some idea about what would it mean to switch from one Java to the other Java.

Besides converting the code from one Java to the other Java, there were also small changes in functionality. First, the program takes the whole screen, because on a mobile device it is not usual to have multiple windows open next to each other. (You can start multiple programs, but you see only one of them, and you can switch between them.) Second, the original program used the left mouse button to drag the circles, and the right mouse button to add or remove them. On a touchscreen device, I don't know which finger was used, so I modified the functionality to use only one finger: clicking on an empty place add a circle; clicking on a circle moves it; and you remove the circle simply by moving it out of the screen (more precisely: the circle is removed when its center is out of the screen). Third, the circles are larger, to make "clicking" on them more convenient. Besided these changes, the programs are functionally equivalent, an the further differences are caused by moving to a different platform.

When programming on Android, it is usually necessary to provide various settings and resources, but I have avoided all of that in this example. There are no texts nor bitmaps, the program behavior does not depend on the screen size and resolution. (Although, it probably would make sense to adjust the circle size to the screen parameters.) Simply create a new project with a new activity called "MainActivity", and replace its code with the code from this page; ignore the rest.

package sk.bur.blog.circles;

import java.util.*;
import android.app.Activity;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.graphics.*;
import android.os.Bundle;
import android.view.*;

public class MainActivity extends Activity {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
		Display display = new Display(this);
		display.init();
		setContentView(display);
	}

	static class Display extends View {
		Collection<Circle> circles = new ArrayList<Circle>();
		Circle draggedCircle = null;
		float draggedOriginalCenterX;
		float draggedOriginalCenterY;
		float draggedStartX;
		float draggedStartY;
		Random random = new Random();
		Paint backPaint;
		Paint circlePaint;

		Display(Context context) {
			super(context);
		}

		Circle findCircle(float x, float y) {
			Circle found = null;
			for (Circle circle : circles) {
				if (circle.contains(x, y)) {
					found = circle;
				}
			}
			return found;
		}

		void init() {
			backPaint = new Paint();
			backPaint.setColor(Color.WHITE);
			backPaint.setStyle(Paint.Style.FILL);
			circlePaint = new Paint();
			circlePaint.setStyle(Paint.Style.FILL);
			setOnTouchListener(new OnTouchListener() {

				@Override
				public boolean onTouch(View v, MotionEvent event) {
					if (MotionEvent.ACTION_DOWN == event.getActionMasked()) {
						touchDown(
						  event.getX(event.getActionIndex()),
						  event.getY(event.getActionIndex())
						);
						return true;
					}
					if (MotionEvent.ACTION_MOVE == event.getActionMasked()) {
						touchMove(
						  event.getX(event.getActionIndex()),
						  event.getY(event.getActionIndex())
						);
						return true;
					}
					if (MotionEvent.ACTION_UP == event.getActionMasked()) {
						touchUp(
						  event.getX(event.getActionIndex()),
						  event.getY(event.getActionIndex())
						);
						return true;
					}
					return false;
				}

				private void touchDown(float fx, float fy) {
					Circle clickedCircle = findCircle(fx, fy);
					if (null != clickedCircle) {
						// start dragging
						draggedCircle = clickedCircle;
						draggedOriginalCenterX = draggedCircle.centerX;
						draggedOriginalCenterY = draggedCircle.centerY;
						draggedStartX = fx;
						draggedStartY = fy;
					} else {
						// add circle
						Circle circle = new Circle();
						circle.centerX = fx;
						circle.centerY = fy;
						circle.radius = 50 + 30 * random.nextFloat();
						// some nice colors
						circle.fillColor = Color.HSVToColor(new float[] {
						  360 * random.nextFloat(),
						  (1 + random.nextFloat()) / 2,
						  (1 + random.nextFloat()) / 2
						});
						circles.add(circle);
					}
					invalidate();
				}

				private void touchMove(float fx, float fy) {
					// dragging
					if (null != draggedCircle) {
						draggedCircle.centerX =
						  draggedOriginalCenterX - draggedStartX + fx;
						draggedCircle.centerY =
						  draggedOriginalCenterY - draggedStartY + fy;
						invalidate();
					}
				}

				private void touchUp(float fx, float fy) {
					if (null != draggedCircle) {
						if (!draggedCircle.inView(getWidth(), getHeight())) {
							// remove circle
							circles.remove(draggedCircle);
						}
						// stop dragging
						draggedCircle = null;
						invalidate();
					}
				}

			});
		}

		@Override
		protected void onDraw(Canvas canvas) {
			canvas.drawRect(0, 0, getWidth(), getHeight(), backPaint);
			for (Circle circle : circles) {
				circlePaint.setColor(circle.fillColor);
				canvas.drawOval(
				  new RectF(
				    circle.centerX - circle.radius,
				    circle.centerY - circle.radius,
				    circle.centerX + circle.radius,
				    circle.centerY + circle.radius
				  ), circlePaint
				);
			}
		}

	}

	static class Circle {

		public float centerX;
		public float centerY;
		public float radius;
		public int fillColor;

		float sqr(float i) {
			return i * i;
		}

		boolean contains(float x, float y) {
			return sqr(x - centerX) + sqr(y - centerY) <= sqr(radius);
		}

		boolean inView(int width, int height) {
			return (0 <= centerX) && (centerX < width)
			  && (0 <= centerY) && (centerY < height);
		}

	}

}

I will suppose that you have read the explanation of the original version, and that you generally understood it. Let's only talk about the differences.

Let's start with you having nothing to download on this page, because Android applications are typically installed from the Google Play page. Publishers can register an account there, and the published applications are signed by their digital signatures. I don't have any of this, yet. You can run your own programs by installing Android Developer Tools, which is a plugin to the Eclipse development environment, creating a program there, connecting your phone or tablet to the computer, for example using the USB cable, and then starting the program from Eclipse. (Installation and using of Android Developer Tools is a longer story, you can find it on "developer.android.com".)

ADDED: I have added a few details, and published the result on Google Play under name Relax Bubbles.

When you write a program and publish it on Google Play, some things cannot be changed later. Specifically: the main application package (like "sk.bur.blog.circles" in this example) and the name of the main activity ("MainActivity" here). This information is used by the system to register the application and create the desktop shortcut. This is why I added "circles" to the package name, to avoid a conflict with other (hypothetical) Android applications on this blog.

There are no more "java.awt..." (and "java.applet") package imports, because those packages don't exist on Android. They are replaced by classes from "android..." packages. The reason for not using the same windows and their controls is obvious. But there are also other kinds of classes in AWT packages, such as Point, Rectangle, Color, which also have to be replaced by their equivalents; and those are sometimes a bit different.

The program starts by creating the main activity, which is approximately an equivalent of a window. We don't create the activity instance; the system creates the instance and then calls its "onCreate" method. The system also takes care of stopping the activity and ending the application. (We don't have to care about multithreading with this simple application.) This part is simpler. So far.

The "setRequestedOrientation" method prevents changing the window orientation when the tablet is tilted. Without this line, on tilting the tablet the window would rotate by 90 degrees, restart, change the size, and lose the data. You can prevent the data loss, but that would be an additional functionality. The original application did not allow changing the window size.

The area we can paint in, is called "View" on Android. (It is approximately an equivalent of "Component" and "Canvas" classes from AWT.) We need to override only one method, "onDraw", and we don't have to care about flickering screen. Again, this part is more simple. The name "Canvas" is used for the object that paints. (Would be "Graphics" in AWT.) The methods for painting are also a bit different: while in standard Java we use one command to set a color and another command to draw, on Android we have a "Paint" object containing the information about color, and we use it as the last parameter for drawing commands. If we use the same "Paint" repeatedly, for example if the picture background has always the same color, we can prepare the object in advance, so we don't have to create it in every "onDraw" call.

Another difference is that in standard Java, the screen coordinates are specified as "int", an integer number, but on Android it is "float", a decimal number. (I guess it is to make zooming views in and out easier.) To follow this convention, the center coordinates and the radius in the "Circle" object are also specified as "float".

Also, the color is not a "Color" object, but an "int". We have a "Color" class, but it contains only the constants and static methods. Careful, some of these methods may expect different parameters. For example when creating a color using HSV, the "Color" in standard Java expects three numbers from 0 to 1, but the "Color" from Android expects the first parameter (hue) a number from 0 to 360, and the remaining two parameters (saturation, value) numbers from 0 to 1. -- One has to be careful about such insidious changes. Similarly, the "Rect" and "RectF" classes (equivalents of "Rectangle" and "Rectangle2D.Float" from AWT) are specified by four numbers, but they don't mean "left, top, width, height", but "left, top, right, bottom".

The finger navigation is implemented using "OnTouchListener", which has only one method, "onTouch", called for various events. These events are also a bit more complicated, because the touchscreen can support multiple fingers touching and moving simultaneously. The "event.getActionIndex" returns the index of the finger causing the event (laying, moving, lifting), and we use this number as the parameters for "event.getX" and "event.getY" methods. Then it is similar, just using "float" coordinates.

We redraw the screen using the "invalidate" command. (I am not completely sure this is the correct approach, but it works.)

To detect whether the circle is yet inside the canvas or it needs to be removed, we have an "inView" method in the "Circle" class.

Summary: The Android Java is similar to Java SE: the syntax is the same; the objects a bit different but analogical; the size and the general structure of the program remained pretty much the same. But it still contains a lot of details which can confuse the beginner. I suppose that for a more complex game, containing reading bitmaps and sounds, saving and loading, and similar stuff, the situation would be similar: the similar principles and many new details one needs to take care of.

If you can write a Java SE program, you have a good starting position for making mobile games. But even if you already have the game ready in Java SE, you should expect the conversion to take a day or a few; especially when you do it for the first time. Of course it is not mandatory to have two versions of the same game, you can develop directly on Android; but to me such development seems slower (perhaps I am just not used to tablets). A third option would be to make some general API encompassing desktop computers and phones and tablets, implement this API on both platforms, and then develop the games in this API; this option would probably be the best in long run.

viliam@bur.sk