📜 ⬆️ ⬇️

Parallax effect for live wallpaper on Android

Everyone who tried to set themselves live wallpaper, noticed the parallax effect when moving between desktops. It looks very entertaining, but its implementation has problems, which will be covered in this article. It will be about the implementation of the parallax effect for Android live wallpaper.


Below will be considered the standard and custom implementation methods. The disadvantages and advantages of each of them are indicated.

Standard method


Starting from 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 class WallpaperService.Engine . The method has the following signature:
')
onOffsetsChanged(float xOffset, float yOffset, float xOffsetStep, float yOffsetStep, int xPixelOffset, int yPixelOffset) 


Of all the parameters passed, we are interested in xOffset and yOffset , and in relation to live wallpaper, it suffices to use xOffset . This parameter varies from 0 to 1, is equal to 0 at one extreme position of the desktop and 1 at another extreme position of the desktop. If the desktop is in the default position (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, the “smoothness” may differ on different devices.

Thus, by passing this parameter to the Renderer of your wallpaper, you can shift them in the right direction by implementing the parallax effect. The advantages are obvious: minimum code and synchronous work with the desktop.

Everything would be fine if it were not for the disadvantages of this method:


Own method, class ZTouchMove


Because of all these problems, it was decided to make his own decision, which would be carried out on all devices. For this, the onTouchEvent method of the same class WallpaperService.Engine 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 associated with the touch screen. However, I would like to transform the touch into an already favorite format of displacement from 0 to 1, taking into account inertia, motion animation and other joys. For this, a touch handler was written, which at the output "gave out" just what was needed. 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<ZTouchMoveListener> mListeners = new ArrayList<ZTouchMoveListener>(); 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<ZTouchMoveListener> iterator = mListeners.iterator(); while(iterator.hasNext()) { ((ZTouchMoveListener) iterator.next()).onTouchOffsetChanged(normalizePosition(mPosition + mPositionDelta)); } } } 

I just want to make a reservation that the code does not pretend to be super clean and tidy, for me it was mainly that he carried out his task, there was no time for a haircut.

The ZTouchMove class has an onTouchEvent (MotionEvent e) method, like an input that is called from the WallpaperService.Engine onTouchEvent class. Next, your renderer should implement the ZTouchMoveListener interface, with the onTouchOffsetChanged (float xOffset) method , which in turn will receive the result in the usual format from 0 to 1.

It is also necessary to perform the initial initialization of ZTouchMove by calling the init method (Context ctx) , passing the application context to it. This is necessary to determine the width of the screen and some other parameters. As well as 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 coded 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 class ZTouchMove : during slow movements, "inertia" is triggered, during fast movements, the "closer" is triggered to the next virtual desktop. At the extreme positions of the "spring".

Among the shortcomings of this method, it is worth noting the asynchronous work of moving the desktop and wallpaper. That is, it may happen that the desktop has already “rested” in an extreme position, and the wallpaper can still be moved. Or on the desktop at a certain speed, the "closer" to the next screen will work, and the "closer" of the wallpaper may not work. These effects are not possible to exclude, as we basically do not have information about the current position of the desktop.

Hybrid solution


The user himself will choose the method of “parallax” in the settings, or you can automatically determine whether the standard method works, and if not, switch to ZTouchMove . Here is an 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, with the standard implementation, xOffset does not accept values ​​other than 0, 0.5 and 1, if the standard onOffsetsChanged method of the WallpaperService.Engine class does not work correctly. Accordingly, the mOffsetChangedEnabled flag defaults to false , 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: Video of two methods of implementation.

Source: https://habr.com/ru/post/164367/


All Articles