
Parallax effect for live wallpapers on Android
Everyone who tried to set their own live wallpapers noticed a parallax effect when moving between desktops. It looks very interesting, but in its implementation there are problems that will be highlighted in this article. We will talk about the implementation of the parallax effect for Android live wallpapers.
Below we will consider standard and custom implementation methods. The disadvantages and advantages of each of them are indicated.
Starting with API7, the WallpaperService.Engine class appeared with the onOffsetsChanged method . This method is called every time the desktop changes its position. To use it, it is enough to override it in the own implementation of the WallpaperService.Engine class . The method has the following signature:
Of all the transferred parameters, we are interested in xOffset and yOffset , and with respect to live wallpaper, it is enough to use xOffset . This parameter varies from 0 to 1, equal to 0 at one extreme position of the desktop and 1 at the other extreme position of the desktop. If the desktop is in the default position (in the middle), the xOffset parameter is 0.5. For example, for 3 desktops, xOffset will be 0, 0.5, 1, respectively. When moving from one desktop to another, the parameter changes smoothly, and the onOffsetsChanged method is called repeatedly. However, “smoothness” may vary on different devices.
Thus, passing this parameter to the Renderer of your wallpaper, you can shift them in the right direction, realizing the parallax effect. The advantages are obvious: a minimum of code and synchronous work with the desktop.
Everything would be fine if not for the disadvantages of this method:
Because of all these problems, it was decided to make a decision that would run on all devices. For this, the onTouchEvent method of the same WallpaperService.Engine class was found . To use this method, you must first enable its call:
Further, this method will accept all events related to touching the screen. However, I would like to convert the touch into an already favorite format of displacement from 0 to 1, taking into account inertia, motion animation and other joys. To do this, a touch handler was written, which at the output "gave out" exactly what you need. Below is the code for the resulting handler:
I want to make a reservation right away that the code does not pretend to be super clean and tidy, for me it was important that it carry out its task, there was no time for a hairstyle.
The ZTouchMove class has an onTouchEvent (MotionEvent e) method , like an input that is called from the onTouchEvent of the WallpaperService.Engine class . Next, your renderer should implement the ZTouchMoveListener interface , with the onTouchOffsetChanged (float xOffset) method , which in turn will take the result in the usual format from 0 to 1.
It is also necessary to initialize ZTouchMove by calling the init (Context ctx) methodpassing the application context to it. This is necessary to determine the width of the screen and some other parameters. And also register the renderer as an event listener:
Since I did not find a way to determine the number of virtual desktops, this parameter was hardcoded in the variable mNumVirtualScreens . If desired, you can add a method to change it and use it at your discretion.
Features of the implementation of animation and inertia of the ZTouchMove class : during slow movements, "inertia" is triggered, during fast movements, the "closer" to the next virtual desktop is triggered. In extreme positions, the "spring" works.
Among the shortcomings of this method, it is worth noting the non-synchronization of the movement of the desktop and wallpaper. That is, it may happen that the desktop has already "rested" in the extreme position, and the wallpaper can still be moved. Or on the desktop at a certain speed, the “closer” to the adjacent screen will work, and the “closer” of the wallpaper may not work. It is not possible to exclude these effects, since, in principle, we do not have information about the current position of the desktop.
The user will choose the “parallax” working method in the settings, or you can automatically determine whether the standard method works, and if not, switch to ZTouchMove . Here is the implementation of automatic detection:
It is based on the fact that xOffset does not accept values other than 0, 0.5, and 1 in the standard implementation, if the standard onOffsetsChanged method of the WallpaperService.Engine class does not work correctly. Accordingly, the mOffsetChangedEnabled flag is false by default , and means that the ZTouchMove class should work .
Personally, I chose a hybrid setting, where automatic detection works by default, and there are two more options: “Desktop mode” and “Touch mode”.
Update: A video of two implementation methods.
Below we will consider standard and custom implementation methods. The disadvantages and advantages of each of them are indicated.
Standard method
Starting with API7, the WallpaperService.Engine class appeared with the onOffsetsChanged method . This method is called every time the desktop changes its position. To use it, it is enough to override it in the own implementation of the WallpaperService.Engine class . The method has the following signature:
onOffsetsChanged(float xOffset, float yOffset, float xOffsetStep, float yOffsetStep, int xPixelOffset, int yPixelOffset)
Of all the transferred parameters, we are interested in xOffset and yOffset , and with respect to live wallpaper, it is enough to use xOffset . This parameter varies from 0 to 1, equal to 0 at one extreme position of the desktop and 1 at the other extreme position of the desktop. If the desktop is in the default position (in the middle), the xOffset parameter is 0.5. For example, for 3 desktops, xOffset will be 0, 0.5, 1, respectively. When moving from one desktop to another, the parameter changes smoothly, and the onOffsetsChanged method is called repeatedly. However, “smoothness” may vary on different devices.
Thus, passing this parameter to the Renderer of your wallpaper, you can shift them in the right direction, realizing the parallax effect. The advantages are obvious: a minimum of code and synchronous work with the desktop.
Everything would be fine if not for the disadvantages of this method:
- Not all devices (shells) call the onOffsetsChanged method when scrolling through desktops. Surprisingly, this often happens with the newest devices (for example, HTC One X).
- Not all devices do this a sufficient number of times, because of which the smoothness of the movement of wallpaper sharply decreases.
- If the desktops in the device are looped, then when switching from the last to the first, a sharp scrolling of the wallpaper occurs.
Native Method, ZTouchMove Class
Because of all these problems, it was decided to make a decision that would run on all devices. For this, the onTouchEvent method of the same WallpaperService.Engine class was found . To use this method, you must first enable its call:
@Override
public void onCreate(SurfaceHolder surfaceHolder) {
setTouchEventsEnabled(true);
}
Further, this method will accept all events related to touching the screen. However, I would like to convert the touch into an already favorite format of displacement from 0 to 1, taking into account inertia, motion animation and other joys. To do this, a touch handler was written, which at the output "gave out" exactly what you need. Below is the code for the resulting handler:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Point;
import android.os.Build;
import android.os.Handler;
import android.view.Display;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.ViewConfiguration;
import android.view.WindowManager;
import android.view.animation.Interpolator;
import android.widget.Scroller;
public class ZTouchMove {
public interface ZTouchMoveListener {
public void onTouchOffsetChanged(float xOffset);
}
private List mListeners = new ArrayList();
public class ZInterpolator implements Interpolator {
public float getInterpolation(float input) {
// f(x) = ax^3 + bx^2 + cx + d
// a = x - 2
// b = 3 - 2x
// c = x
// d = 0
// where x = derivative in point 0
//input = (float)(-Math.cos(10*((double)input/Math.PI)) + 1) / 2;
input = (mVelocity - 2) * (float) Math.pow(input, 3) + (3 - 2 * mVelocity) * (float) Math.pow(input, 2) + mVelocity * input;
return input;
}
}
Handler mHandler = new Handler();
final Runnable mRunnable = new Runnable()
{
public void run()
{
if(onMovingToPosition())
mHandler.postDelayed(this, 20);
}
};
private float mPosition = 0.5f;
private float mPositionDelta = 0;
private float mTouchDownX;
private int xDiff;
private VelocityTracker mVelocityTracker;
private float mVelocity = 0;
private Scroller mScroller;
private final static int TOUCH_STATE_REST = 0;
private final static int TOUCH_STATE_SCROLLING = 1;
private static final int SCROLLING_TIME = 300;
private static final int SNAP_VELOCITY = 350;
private int mTouchSlop;
private int mMaximumVelocity;
private int mTouchState = TOUCH_STATE_REST;
private int mWidth;
private int mNumVirtualScreens = 5;
@SuppressLint("NewApi")
@SuppressWarnings("deprecation")
public void init(Context ctx) {
mScroller = new Scroller(ctx, new ZInterpolator());
final ViewConfiguration configuration = ViewConfiguration.get(ctx);
mTouchSlop = configuration.getScaledTouchSlop();
mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
WindowManager wm = (WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE);
Display display = wm.getDefaultDisplay();
// API Level 13
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) {
Point size = new Point();
display.getSize(size);
mWidth = size.x;
} else {
// API Level <13
mWidth = display.getWidth();
}
}
public void onTouchEvent(MotionEvent e) {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(e);
final float x = e.getX();
final int action = e.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST : TOUCH_STATE_SCROLLING;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
mTouchDownX = x;
break;
case MotionEvent.ACTION_MOVE:
xDiff = (int) (x - mTouchDownX);
if (Math.abs(xDiff) > mTouchSlop && mTouchState != TOUCH_STATE_SCROLLING) {
mTouchState = TOUCH_STATE_SCROLLING;
if(xDiff < 0)
mTouchDownX = mTouchDownX - mTouchSlop;
else
mTouchDownX = mTouchDownX + mTouchSlop;
xDiff = (int) (x - mTouchDownX);
}
if (mTouchState == TOUCH_STATE_SCROLLING) {
mPositionDelta = -(float)xDiff / (mWidth * mNumVirtualScreens);
}
break;
case MotionEvent.ACTION_UP:
if (mTouchState == TOUCH_STATE_SCROLLING) {
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
float velocityX = velocityTracker.getXVelocity() / (float)(mNumVirtualScreens * mWidth);
mPosition = mPosition + mPositionDelta;
mPositionDelta = 0;
if(!returnSpring()) {
mVelocity = Math.min(3, Math.abs(velocityX * mNumVirtualScreens)) ;
// deaccelerate();
// Inertion
if(Math.abs(velocityX) * (float)(mNumVirtualScreens * mWidth) > SNAP_VELOCITY)
moveToPosition(mPosition, mPosition - (velocityX > 0 ? 1 : -1) * 1 / (float) mNumVirtualScreens );
else
moveToPosition(mPosition, mPosition - 0.7f * velocityX * ((float)SCROLLING_TIME / 1000) );
}
}
mTouchState = TOUCH_STATE_REST;
break;
case MotionEvent.ACTION_CANCEL:
mTouchState = TOUCH_STATE_REST;
mPositionDelta = 0;
break;
}
dispatchMoving();
}
private boolean returnSpring() {
mVelocity = 0;
if(mPositionDelta + mPosition > 1 - 0.5 / (float) mNumVirtualScreens)
moveToPosition(mPosition, (float) (1 - 0.5 / (float) mNumVirtualScreens));
else if(mPositionDelta + mPosition < 0.5 / (float) mNumVirtualScreens)
moveToPosition(mPosition, (float) 0.5 / (float) mNumVirtualScreens);
else
return false;
return true;
}
private void moveToPosition(float current_position, float desired_position) {
mScroller.startScroll((int)(current_position * 1000), 0, (int)((desired_position - current_position) * 1000), 0, SCROLLING_TIME);
mHandler.postDelayed(mRunnable, 20);
}
private boolean onMovingToPosition() {
if(mScroller.computeScrollOffset()) {
mPosition = (float)mScroller.getCurrX() / 1000;
dispatchMoving();
return true;
} else {
returnSpring();
return false;
}
}
private float normalizePosition(float xOffset) {
final float springZone = 1 / (float) mNumVirtualScreens;
// Normalized offset is from 0 to 0.5
float xOffsetNormalized = Math.abs(xOffset - 0.5f);
if(xOffsetNormalized + springZone / 2 > 0.5f) {
// Spring formula
// (0.5 - 2 * (1 - (x / (2 * springZone) + 0.5))^2) * springZone
// where x >=0 and <= springZone
// delta y = springZone / 2, y >=0 and y <= springZone / 2
xOffsetNormalized = 0.5f - springZone / 2 +
(0.5f - 2 * (float)Math.pow( (double)(1 - ( (xOffsetNormalized - 0.5f + springZone / 2) / (2 * springZone) + 0.5)), 2 ) ) * springZone;
if(xOffset < 0.5f)
xOffset = 0.5f - xOffsetNormalized;
else
xOffset = 0.5f + xOffsetNormalized;
}
return xOffset;
}
public synchronized void addMovingListener(ZTouchMoveListener listener) {
mListeners.add(listener);
}
private synchronized void dispatchMoving() {
Iterator iterator = mListeners.iterator();
while(iterator.hasNext()) {
((ZTouchMoveListener) iterator.next()).onTouchOffsetChanged(normalizePosition(mPosition + mPositionDelta));
}
}
}
I want to make a reservation right away that the code does not pretend to be super clean and tidy, for me it was important that it carry out its task, there was no time for a hairstyle.
The ZTouchMove class has an onTouchEvent (MotionEvent e) method , like an input that is called from the onTouchEvent of the WallpaperService.Engine class . Next, your renderer should implement the ZTouchMoveListener interface , with the onTouchOffsetChanged (float xOffset) method , which in turn will take the result in the usual format from 0 to 1.
It is also necessary to initialize ZTouchMove by calling the init (Context ctx) methodpassing the application context to it. This is necessary to determine the width of the screen and some other parameters. And also register the renderer as an event listener:
mTouchMove = new ZTouchMove();
mTouchMove.init(ctx);
mTouchMove.addMovingListener(mRenderer);
Since I did not find a way to determine the number of virtual desktops, this parameter was hardcoded in the variable mNumVirtualScreens . If desired, you can add a method to change it and use it at your discretion.
Features of the implementation of animation and inertia of the ZTouchMove class : during slow movements, "inertia" is triggered, during fast movements, the "closer" to the next virtual desktop is triggered. In extreme positions, the "spring" works.
Among the shortcomings of this method, it is worth noting the non-synchronization of the movement of the desktop and wallpaper. That is, it may happen that the desktop has already "rested" in the extreme position, and the wallpaper can still be moved. Or on the desktop at a certain speed, the “closer” to the adjacent screen will work, and the “closer” of the wallpaper may not work. It is not possible to exclude these effects, since, in principle, we do not have information about the current position of the desktop.
Hybrid solution
The user will choose the “parallax” working method in the settings, or you can automatically determine whether the standard method works, and if not, switch to ZTouchMove . Here is the implementation of automatic detection:
if(xOffset != 0 && xOffset != 0.5f && xOffset != 1 || mOffsetChangedEnabled) {
mOffsetChangedEnabled = true;
mXPos = xOffset - 0.5f;
// Устанавливаем положение камеры
setupLookatM();
}
It is based on the fact that xOffset does not accept values other than 0, 0.5, and 1 in the standard implementation, if the standard onOffsetsChanged method of the WallpaperService.Engine class does not work correctly. Accordingly, the mOffsetChangedEnabled flag is false by default , and means that the ZTouchMove class should work .
Personally, I chose a hybrid setting, where automatic detection works by default, and there are two more options: “Desktop mode” and “Touch mode”.
Update: A video of two implementation methods.