Live Wallpaper

Regular wallpaper just sits there. It looks nice, but it never changes. Yawn.

2. http://d.android.com/guide/topics/appwidgets

Figure 12.6: The wallpaper example reuses code from the OpenGL chapter.

Live wallpaper is a new feature in Android 2.1 (Eclair Maintenance Release 1). It lets you replace boring static wallpaper images with everything from vibrant and pulsing music visualizations to quiet, meditative ponds that respond to your touch with gentle ripples.

Displaying current weather conditions, slide shows, Magic 8 Balls, and pyrotechnics are just a few of the ideas that are possible. Let's pull back the curtain to see how the magic is done.

Creating the Wallpaper Project

In this example, we'll create a live wallpaper that displays a rotating cube using OpenGL. The final result is shown in Figure 12.6. Start by creating a new Android project using these values in the project wizard:

Project name: Wallpaper Build Target: Android 2.2 Application name: Wallpaper Package name: org.example.wallpaper Min SDK Version: 8

As with the widget project, leave the activity name blank, and turn off the check mark next to Create Activity. After the project is created, we need to perform some major changes to the AndroidManifest.xml file. Here's what it should look like:

Download Wallpaper/AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="org.example.wallpaper" android:versionCode="1" android:versionName="1.0"> <application android:label="@string/app_name"> <service android:name=".Wallpaper"

android:label="@string/service_name"

android:permission= "android.permission.BIND_WALLPAPER"> <intent-filter>

<action android:name=

"android.service.wallpaper.WallpaperService" /> </intent-filter>

<meta-data android:name="android.service.wallpaper" android:resource="@xml/wallpaper" /> </service> </application>

<uses-sdk android:minSdkVersion="7" android:targetSdkVersion="8" /> </manifest>

The <service> tag is new. It defines an Android service that will run in the background and respond to events. The android:permission= attribute means that any program that calls our service will need to have the specified permission. The Android Home screen program already has that permission set, so it will work fine.

The <intent-filter> tag tells Android what type of service this is, and the <meta-data> tag lets it know where to find extra information about the wallpaper. The android:resource="@xml/wallpaper" setting refers to the res/xml/wallpaper.xml file, which is a new file you should create now using the following contents:

Download Wallpaper/res/xml/wallpaper.xml

<?xml version="1.0" encoding="utf-8"?>

<wallpaper xmlns:android="http://schemas.android.com/apk/res/android" android:author="@+string/author" android:description="@string/description" android:thumbnail="@drawable/thumbnail" />

The wallpaper metadata specifies the author of the wallpaper (that's you), a short description of what it does, and a thumbnail image. The image and description will appear in a list when the user is asked to pick a wallpaper to display.

Before we get any further, let's define all the strings we'll need for the project in res/values/strings.xml:

Download Wallpaper/res/values/strings.xml

<?xml version="1.0" encoding="utf-8"?> <resources>

<string name="app_name">Wallpaper</string> <string name="service_name">Hello, Android!</string> <string name="author">Hello, Android!</string> <string name="description">Sample live wallpaper from Hello, Android!</string> </resources>

You should delete the layout file, res/layout/main.xml, because we won't be using it. We'll fill in the Wallpaper class (Wallpaper.java) after we learn a few things about Android services.

Introducing Services

One of the distinguishing features of Android is the ability to run programs in the background. To contrast them with foreground activities, Android calls these programs services.

The main Java class for a service inherits from the Service class. Services have a life cycle similar to activities (see Section 2.2, It's Alive!, on page 35) but are a little simpler. There's an onCreate() method that is called when the service is first created and an onDestroy( ) method that is called when it is destroyed.

In between, Android will call the onStartCommand( ) method (onStart( ) prior to version 2.0) when a client requests that the service be started. Services can be sticky or not sticky, depending upon whether you want the service to hang around between requests.

There are a few other methods for things such as low memory conditions that you can implement if you like. See the online documentation for all the gory details.3

For the wallpaper example, we don't need to worry about any of these methods because they are all handled by the WallpaperService class, which is a subclass of Service.

3. http://d.android.com/reference/android/app/Service.html

Our main class needs to extend WallpaperService like this:

Download Wallpaper/src/org/example/wallpaper/Wallpaper.java

package org.example.wallpaper;

import android.service.wallpaper.WallpaperService;

public class Wallpaper extends WallpaperService { private class MyEngine extends Engine { // Engine implementation goes here...

@Override public Engine onCreateEngine() { return new MyEngine();

All we had to do was implement the onCreateEngine() method, which is a single line. It's sole purpose is to create and return another class called MyEngine.

Building a Drawing Engine

The MyEngine class has to be an inner class of Wallpaper, so in Java it's declared inside the enclosing class's curly brackets. MyEngine extends the Engine class provided by Android. Here's the outline for MyEngine with all the methods stubbed out:

Download Wallpaper/src/org/example/wallpaper/Wallpaper.java

private class MyEngine extends Engine { @Override public void onCreate(final SurfaceHolder holder) { super.onCreate(holder);

@Override public void onDestroy() { super.onDestroy();

@Override public void onSurfaceCreated(final SurfaceHolder holder) { super.onSurfaceCreated(holder);

@Override public void onSurfaceDestroyed(final SurfaceHolder holder) { super.onSurfaceDestroyed(holder);

@Override public void onSurfaceChanged(final SurfaceHolder holder, final int format, final int width, final int height) { super.onSurfaceChanged(ho1der, format, width, height);

@Override public void onVisibi1ityChanged(final boolean visible) { super .onVisibilityChanged(visible);

@Override public void onOffsetsChanged(final float xOffset, final float yOffset, final float xOffsetStep, final float yOffsetStep, final int xPixelOffset, final int yPixelOffset) { super.onOffsetsChanged(xOffset, yOffset, xOffsetStep, yOffsetStep, xPixelOffset, yPixelOffset);

Note that every method should always call its superclass method. You can get Eclipse to generate these stubs for you in the Java editor by selecting the class name MyEngine, right-clicking, selecting Source > Override/Implement Methods, and picking the methods you want to create. There's one difference in what Eclipse generates and what was shown earlier, however. I've added the final keyword to every method parameter. That will be needed later so inner classes within those methods can access the parameters. If you forget and leave them out, the compiler will let you know.

During the engine's life cycle, Android will call these methods in a specific order. Here is the entire sequence:

onCreate onSurfaceCreated onSurfaceChanged (1+ calls in any order)

onOffsetsChanged (0+ calls in any order)

onVisibilityChanged (0+ calls in any order)

onSurfaceDestroyed onDestroy

We'll be filling out all these methods in the rest of the chapter.

We'll also need a few more import statements to prevent compiler errors. I usually let Eclipse create those for me while I'm coding (using Content Assist (Ctrl+spacebar]) or Quick Fix (Ctrl+1 ]) or in a batch with the Source > Organize Imports command ([Ctrl+Shift+Q). But if you prefer, you can just type them all in now. Here's the full list:

Download Wallpaper/src/org/example/wallpaper/Wallpaper.java

import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors;

import javax.microedition.khronos.egl.EGL10; import javax.microedition.khronos.egl.EGLll; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.egl.EGLContext; import javax.microedition.khronos.egl.EGLDisplay; import javax.microedition.khronos.egl.EGLSurface; import javax.microedition.khronos.opengles.GL10;

import android.service.wallpaper.WallpaperService; import android.view.SurfaceHolder;

Next we need to borrow some code to draw our cube. Reusing the OpenGL code

You don't have to use OpenGL for live wallpapers, but I like it because it's faster than the regular Android 2D graphics libraries (see Chapter 4, Exploring 2D Graphics, on page 73). Also, we just so happen to have a nice rotating 3D cube example already written from another chapter (Chapter 10, 3D Graphics in OpenGL, on page 198).

Grab three files from that project now: GLCube.java, GLRenderer.java, and android.png. Put the Java files in the org.example.wallpaper package (in other words, the org/example/wallpaper directory), and place the pNG file in the res/drawable-nodpi directory.

If you haven't done that chapter yet, you can download all these files from the book's website. Unzip the source code archive to a temporary location, and copy the files from the OpenGL project.

You shouldn't have to make any changes to the source code except for the package name at the top of both Java files, which should read as follows:

Download Wallpaper/src/org/example/wallpaper/GLRenderer.java

package org.example.wallpaper;

Now we just have to figure out how to call the code from our new wallpaper project.

Creating and Destroying the Engine

To begin, let's define a few fields for the engine to use. Put these at the start of the MyEngine class:

Download Wallpaper/src/org/example/wallpaper/Wallpaper.java

private GLRenderer glRenderer; private GL10 gl; private EGL10 egl; private EGLContext glc; private EGLDisplay glDisplay; private EGLSurface glSurface;

private ExecutorService executor; private Runnable drawCommand;

The most important variable here is executor. In Java, an executor is an object that can run snippets of code (called runnables) asynchronously in another thread. When the wallpaper engine is first created, we're going to create one of these executors to handle all interaction with OpenGL.

We have to do that because OpenGL can be called only from a single thread. The service has to create a thread to do drawing in the background anyway, and we can't call some of the OpenGL code in the background thread and some in the foreground thread. So, we use a single executor for everything. The definition of onCreate( ) shows how to initialize it:

Download Wallpaper/src/org/example/wallpaper/Wallpaper.java

@Override public void onCreate(final SurfaceHolder holder) { super.onCreate(holder);

executor = Executors.newSingleThreadExecutor();

drawCommand = new Runnable() { public void run() {

glRenderer.onDrawFrame(gl); egl.eglSwapBuffers(glDisplay, glSurface); if (isVisible()

&& egl.eglGetError() != EGL11.EGL_CONTEXT_LOST) { executor.execute(drawCommand);

Besides the executor, we also create a runnable called drawCommand, which will be used later to draw each frame of the cube animation. This is a Java anonymous inner class, so it has access to all the fields and final parameters of its parents. Note that we haven't initialized any of those variables yet (such as glRenderer and glDisplay), but that's OK because we're just defining this code to be run later, after everything is ready.

The opposite of onCreate( ) is onDestroy( ). onDestroy( ) is called when Android is finished with the wallpaper engine. All it needs to do is shut down the executor we created in onCreate( ):

Download Wallpaper/src/org/example/wallpaper/Wallpaper.java

@Override public void onDestroy() { executor.shutdownNow(); super.onDestroy();

Note that we call super.onDestroy() at the end of the method, not at the beginning like we do in most places. That's just a standard Java idiom to let the superclass clean up after itself in a mirror image of the way creation was done. I don't know if it's really necessary in this case, but by following the convention, we don't even have to think about it.

Managing the Surface

During the engine's lifetime, Android will create a Surface representing the background of the Home screen for the engine to draw on. When the surface is first created, onSurfaceCreated() is called. We use this opportunity to initialize OpenGL and our GLRenderer class that we copied from the other project:

Download Wallpaper/src/org/example/wallpaper/Wallpaper.java

@Override public void onSurfaceCreated(final SurfaceHolder holder) { super.onSurfaceCreated(holder);

Runnable surfaceCreatedCommand = new Runnable() { @Override public void run() { // Initialize OpenGL egl = (EGL10) EGLContext.getEGL();

glDisplay = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); int[] version = new int[2]; egl.eglInitialize(glDisplay, version); int[] configSpec = { EGL10.EGL_RED_SIZE, 5,

EGL10.EGL_GREEN_SIZE, 6, EGL10.EGL_BLUE_SIZE, 5, EGL10.EGL_DEPTH_SIZE, 16, EGL10.EGL_N0NE };

EGLConfig[] configs = new EGLConfig[1]; int[] numConfig = new int[1];

egl.eglChooseConfigCglDisplay, configSpec, configs,

1, numConfig); EGLConfig config = configs[0];

glc = egl.eglCreateContextCglDisplay, config, EGL10.EGL_NO_CONTEXT, null);

glSurface = egl.eglCreateWindowSurfaceCglDisplay, config, holder, null); egl.eglMakeCurrentCglDisplay, glSurface, glSurface, glc);

gl = (GL10) (glc.getGLO); // Initialize Renderer glRenderer = new GLRenderer(Wallpaper.this); glRenderer.onSurfaceCreatedCgl, config);

executor.execute(surfaceCreatedCommand);

It would be nice if Android would provide a helper class to hide some of this OpenGL boilerplate detail from the programmer (something like GLSurfaceView, but for wallpapers), but until then, just hold your nose and copy the code.

The onSurfaceDestroyed( ) method is called when the background surface is about to go away. This is a good place to terminate all that OpenGL stuff we initialized earlier:

Download Wallpaper/src/org/example/wallpaper/Wallpaper.java

@Override public void onSurfaceDestroyed(final SurfaceHolder holder) { Runnable surfaceDestroyedCommand = new Runnable() { public void run() {

// Free OpenGL resources egl.eglMakeCurrentCglDisplay, EGL10.EGL_NO_SURFACE,

EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT); egl.eglDestroySurfaceCglDisplay, glSurface); egl.eglDestroyContextCglDisplay, glc); egl.eglTerminate(glDisplay);

executor.execute(surfaceDestroyedCommand); super.onSurfaceDestroyed(holder);

While the surface is up, Android calls the onSurfaceChanged() method to tell you its width and height.

Download Wallpaper/src/org/example/wallpaper/Wallpaper.java

@Override public void onSurfaceChanged(final SurfaceHolder holder, final int format, final int width, final int height) { super.onSurfaceChanged(holder, format, width, height); Runnable surfaceChangedCommand = new Runnable() { public void run() {

glRenderer.onSurfaceChanged(gl, width, height);

executor.execute(surfaceChangedCommand);

So far, the surface has not been visible to the user, so we shouldn't draw anything on it yet. That's about to change.

Making the Wallpaper Visible

After the WallpaperService is initialized, the Engine is initialized, and the Surface is initialized (whew!), the only thing left to do is to make the surface visible. When it's ready to do that, Android calls the onVisibility-Changed( ) method with a boolean parameter that says whether it has become visible or invisible:

Download Wallpaper/src/org/example/wallpaper/Wallpaper.java

@Override public void onVisibilityChanged(final boolean visible) { super .onVisibilityChanged(visible); if (visible) {

executor.execute(drawCommand);

If it's visible, all we need to do is queue up a call to the drawCommand runnable, which will draw one frame of the animation and, if the surface is still visible, queue itself up again over and over. To save the battery, it is very important that a wallpaper run only while it is visible.

At this point, you should be able to try the example in the emulator or on a real device. Right-click the project, and select Run As > Android Application. As with widgets, the wallpaper will only be installed, not executed, by running it from Eclipse. To really run it, go to the device or emulator, and press and hold your finger (or mouse) on the Home screen. A menu will appear with options listing all the types of things you can add (see Figure 12.3, on page 238).

Select Wallpapers from the menu, and then Live wallpapers. A list of all live wallpapers will appear. pick the one called Hello, Android! and the wallpaper should start spinning around happily in preview mode (if it doesn't, then follow the instructions in Section 3.10, Debugging, on page 69 to diagnose the problem). Touch the Set wallpaper button to make this the wallpaper for your Home screen, or press the Back button to return to the list. For the live wallpaper in action, see Figure 12.6, on page 243.

Responding to User Input

When you use regular, static wallpaper and you pan the Home screen left or right with your finger, the wallpaper moves a little as well. However, if you try that with the live wallpaper example now, nothing happens.

To get that functionality, we have to implement the onOffsetsChanged( ) method:

Download Wallpaper/src/org/example/wallpaper/Wallpaper.java

@Override public void onOffsetsChanged(final float xOffset, final float yOffset, final float xOffsetStep, final float yOffsetStep, final int xPixelOffset, final int yPixelOffset) { super.onOffsetsChanged(xOffset, yOffset, xOffsetStep, yOffsetStep, xPixelOffset, yPixelOffset); Runnable offsetsChangedCommand = new Runnable() { public void run() {

glRenderer.setParallax(xOffset - 0.5f);

executor.execute(offsetsChangedCommand);

If xOffsetStep is 0, it means panning is disabled (for example, in preview mode). For other values, we shift the view based on the xOffset variable. If the offset is 0, the user has panned all the way to the left, and if it's 1, they have panned all the way to the right. Care to guess what 0.5 means?

Now I'm about to go back on what I said earlier about not modifying GLRenderer. Since the OpenGL example did not have any user input, we have to add an extra field and a setParallax() method that sets it to the GLRenderer class:

Download Wallpaper/src/org/example/wallpaper/GLRenderer.java

class GLRenderer implements GLSurfaceView.Renderer { // ...

private float xOffset;

public void setParallax(float xOffset) { this.xOffset = -xOffset;

Then in the middle of the GLRenderer.onDrawFrame() method, we have to change one line to use that new field to translate the model a little to the left or right:

Download Wallpaper/src/org/example/wallpaper/GLRenderer.java

// Position model so we can see it gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadIdentity(); gl.glTranslatef(xOffset, 0, -3.0f);

// Other drawing commands go here...

We could have moved the eye point instead, but it was easier just to move the cube. That's it! Now try the example again, and you can pan left or right with ease.

There are two other ways to interact with live wallpaper that you can support if you like: commands and touch events. To support commands, implement the onCommand( ) method, and to support raw touch events, implement the onTouchEvent() method. I'll leave that as an exercise for you.

0 0

Post a comment