Making Android wallpaper with parallax scrolling support using AndEngine
With the latest update of our Flock of Birds live wallpaper, we added new, nice-looking feature called parallax scrolling. Today we want to share with you some tips on how to achieve such an effect in your wallpaper ;)
Parallax scrolling is a simple trick used to create an illusion of depth in 2D scene. During scroll, the objects, that are further away from the observer, are moved slower than the foreground images. Wikipedia has a good exemplary animation of that technique.
Nowadays many of the Android devices are using UI with many visible desktops (Samsung phones with TouchWiz UI are one of them, but not only; you can also change your phone default launcher to turn on the multiple desktops support). As user can scroll between those, this is a perfect use-case for a live wallpaper, that can use the parallax scrolling technique!
Now, we'd like to show you, how to do this using AndEngine - a free, OpenGL-based game engine, that we used to create our Flock of Birds wallpaper. Our application is based on GLES2 AndEngine version taken from git repository. If you are new to the AndEngine, AndEngine from scratch series will be a good starting point ;)
1. Get the AndEngine and the AndEngineLiveWallpaperExtension.
2. Create new android project with WallpaperService, that extends BaseLiveWallpaperService class. Implement all the required methods. Below are some hints of where to put which type of code:
class FlockOfBirdsWallpaperService extends BaseLiveWallpaperService {
private Scene mScene;
@Override
public EngineOptions onCreateEngineOptions() {
// place for creating AndEngineOptions class with configuration
EngineOptions engineOptions = new EngineOptions(/* conf params */);
return engineOptions;
}
@Override
public void onCreateResources(OnCreateResourcesCallback rsrCallback)
throws Exception {
// resource loading code goes here
// ...
// call this when done
rsrCallback.onCreateResourcesFinished();
}
@Override
public void onCreateScene(OnCreateSceneCallback sceneCallback)
throws Exception {
// creating main scene
mScene = new Scene();
sceneCallback.onCreateSceneFinished(mScene);
}
@Override
public void onPopulateScene(Scene scene, OnPopulateSceneCallback pupulateCallback)
throws Exception {
// filling the scene content, setting bacgrounds, etc.
// ...
pupulateCallback.onPopulateSceneFinished();
}
}
As you can see, our FlockOfBirdsWallpaperService consist of four main methods:
onCreateEngineOptions() - this is a place to set the basic configuration, such as camera width, height and resolution policy. You have to experiment with those settings to figure out how it works - we will cover this topic in the future.
onCraeteResources() - in this method your code should load all resources needed to create textures for your wallpaper. As new AndEngine introduces callbacks mechanism to give you more control over the engine initialization, you will notice the rsrCallback.onCreateResourcesFinished() line at the end of this method - you should call this after all resources are loaded to trigger further engine initialization steps.
onCreateScene - as name suggest, this is a perfect place to create new scene ;) After creation, scene object is passed to the sceneCallback object.
onPopulateScene - in this method you should fill your scene with the objects that you intend to draw (sprites).
Now, if you take a look at our Flock of Birds Wallpaper, you will notice three layers: planes in the background, clouds in the middle and birds in the foreground. So, let's add those layers to our scene:
public void onPopulateScene(Scene scene, OnPopulateSceneCallback pupulateCallback)
throws Exception {
// filling the scene content, setting bacgrounds, etc.
mPlanesLayer = new Entity();
scene.attachChild(mPlanesLayer);
mCloudsLayer = new Entity();
scene.attachChild(mCloudsLayer);
mBirdsLayer = new Entity();
scene.attachChild(mBirdsLayer);
// ... and start graphics engine update thread
this.getEngine().startUpdateThread();
pupulateCallback.onPopulateSceneFinished();
}
One thing to note: to start drawing, the method startUpdateThread() is now called on the engine object. It will execute the update thread creation, which will redraw our scene several times per second. If you are using AndEngine revision older than 05/2012, this is called automatically.
The scrolling trick that we would like to achieve basically means moving our three layers every time the user switches desktops. To get nice parallax effect, planes layer have to be moved with less offset than the clouds and birds layers. Also, all of our layers have to be wider than the screen, as we want user to see only a part of our wallpaper under each of the desktops.
Assuming our offset will change from 0.0 to 1.0 (0.0 offset for the first desktop, 1.0 for the last one), the following code will do the parallax scrolling trick:
// difference between camera width and the layers width
widthDiff = layersWidth - cameraWidth;
mPlanesLayer.setX(mPlanesLayer.getX() - 0.2 * offset * widthDiff);
mCloudsLayer.setX(mCloudsLayer.getX() - 0.6 * offset * widthDiff);
mBirdsLayer.setX (mBirdsLayer.getX() - 1.0 * offset * widthDiff);
Of course you have to tweak the coefficients that the screen offset is multiplied by to get the nice effect, it all depends on how the wallpaper content looks like. But one question still remains: how to get the offset value?
Every time user switches desktops, onOffsetsChanged() method from BaseLiveWallpaperService is called a few times, giving us the current offset value. So we have to override:
@Override
protected void onOffsetsChanged(final float pXOffset, final float pYOffset, final float pXOffsetStep, final float pYOffsetStep, final int pXPixelOffset, final int pYPixelOffset) {
mPlanesLayer.setX(mPlanesLayer.getX() - 0.2 * offset * widthDiff);
mCloudsLayer.setX(mCloudsLayer.getX() - 0.6 * offset * widthDiff);
mBirdsLayer.setX (mBirdsLayer.getX() - 1.0 * offset * widthDiff);
}
As you can see, we use pXOffset value, that is responsible for vertical scrolling. Offsets work this way: if we have 3 desktops, pXOffsetStep will be equal to 0.5 and the pXOffset value will be changing from 0.0 for the 1st desktop, 0.5 for the 2nd up to 1.0 for 3rd.
The code above will work, but the final effect will look poor - the scrolling will not be smooth. It turns out that during the scroll from one desktop to another, the onOffsetChanged() method may be called 50 times by the WallpaperService, but it can also be called only 5 times :) This will result in relatively big offset changes and impact the fluency of the scrolling. But there is a simple solution to avoid this. Let's assume that in our onOffsetsChanged() method we only store the desired offset:
@Override
protected void onOffsetsChanged(final float pXOffset, final float pYOffset, final float pXOffsetStep, final float pYOffsetStep, final int pXPixelOffset, final int pYPixelOffset) {
mTargetOffset = pXOffset;
}
Then, during every scene update, our layers should be moved by a small unit until their position matches the one implied by the mTargetOffset.
To listen to scene update events, add this code to onPopulateScene method():
scene.registerUpdateHandler(new IUpdateHandler(){
@Override
public void onUpdate(float secondsElapsed) {
moveLayers();
}
@Override
public void reset() {
}
});
And the final moveLayers() method:
public void moveLayers(){
float widthDiff = mWpWidth - mCameraWidth;
final float k = 0.2f; // error scaling coefficient
// birds layer update
{
float desiredPos = 1.0 * -mTargetOffset * widthDiff;
float currentPos = mBirdsLayer.getX();
float error = desiredPos - currentPos;
mBirdsLayer.setX(k * error + currentPos);
}
// ...
}
The above code should be pretty clear. The k coefficient is used to scale offset error to make layers move only a small offset value at a time. You can choose it experimentally and see which value looks the best for you. And of course you have to create similar code for the other layers, using different coefficients for moving. And that should be all.
Don't forget to let us know, if it worked for your wallpapers! :)
Note: If you encounter a problem with Andengine on an Android Froyo (2.2) device, check out this post.