mirror of
https://github.com/LawnchairLauncher/lawnchair.git
synced 2026-03-03 09:26:51 +00:00
312 lines
12 KiB
Java
312 lines
12 KiB
Java
package com.android.launcher3.util;
|
|
|
|
import static android.content.Intent.ACTION_WALLPAPER_CHANGED;
|
|
|
|
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
|
|
|
|
import android.app.WallpaperManager;
|
|
import android.content.Context;
|
|
import android.os.Handler;
|
|
import android.os.IBinder;
|
|
import android.os.Message;
|
|
import android.os.SystemClock;
|
|
import android.util.Log;
|
|
import android.view.animation.Interpolator;
|
|
|
|
import androidx.annotation.AnyThread;
|
|
|
|
import com.android.app.animation.Interpolators;
|
|
import com.android.launcher3.Utilities;
|
|
import com.android.launcher3.Workspace;
|
|
|
|
/**
|
|
* Utility class to handle wallpaper scrolling along with workspace.
|
|
*/
|
|
public class WallpaperOffsetInterpolator {
|
|
|
|
private static final int[] sTempInt = new int[2];
|
|
private static final String TAG = "WPOffsetInterpolator";
|
|
private static final int ANIMATION_DURATION = 250;
|
|
|
|
// Don't use all the wallpaper for parallax until you have at least this many pages
|
|
private static final int MIN_PARALLAX_PAGE_SPAN = 4;
|
|
|
|
private final SimpleBroadcastReceiver mWallpaperChangeReceiver =
|
|
new SimpleBroadcastReceiver(i -> onWallpaperChanged());
|
|
private final Workspace<?> mWorkspace;
|
|
private final boolean mIsRtl;
|
|
private final Handler mHandler;
|
|
|
|
private boolean mRegistered = false;
|
|
private IBinder mWindowToken;
|
|
private boolean mWallpaperIsLiveWallpaper;
|
|
|
|
private boolean mLockedToDefaultPage;
|
|
private int mNumScreens;
|
|
|
|
public WallpaperOffsetInterpolator(Workspace<?> workspace) {
|
|
mWorkspace = workspace;
|
|
mIsRtl = Utilities.isRtl(workspace.getResources());
|
|
mHandler = new OffsetHandler(workspace.getContext());
|
|
}
|
|
|
|
/**
|
|
* Locks the wallpaper offset to the offset in the default state of Launcher.
|
|
*/
|
|
public void setLockToDefaultPage(boolean lockToDefaultPage) {
|
|
mLockedToDefaultPage = lockToDefaultPage;
|
|
}
|
|
|
|
public boolean isLockedToDefaultPage() {
|
|
return mLockedToDefaultPage;
|
|
}
|
|
|
|
/**
|
|
* Computes the wallpaper offset as an int ratio (out[0] / out[1])
|
|
*
|
|
* TODO: do different behavior if it's a live wallpaper?
|
|
*/
|
|
private void wallpaperOffsetForScroll(int scroll, int numScrollableScreens, final int[] out) {
|
|
out[1] = 1;
|
|
|
|
// To match the default wallpaper behavior in the system, we default to either the left
|
|
// or right edge on initialization
|
|
if (mLockedToDefaultPage || numScrollableScreens <= 1) {
|
|
out[0] = mIsRtl ? 1 : 0;
|
|
return;
|
|
}
|
|
|
|
// Distribute the wallpaper parallax over a minimum of MIN_PARALLAX_PAGE_SPAN workspace
|
|
// screens, not including the custom screen, and empty screens (if > MIN_PARALLAX_PAGE_SPAN)
|
|
int numScreensForWallpaperParallax = mWallpaperIsLiveWallpaper ? numScrollableScreens :
|
|
Math.max(MIN_PARALLAX_PAGE_SPAN, numScrollableScreens);
|
|
|
|
// Offset by the custom screen
|
|
|
|
// Don't confuse screens & pages in this function. In a phone UI, we often use screens &
|
|
// pages interchangeably. However, in a n-panels UI, where n > 1, the screen in this class
|
|
// means the scrollable screen. Each screen can consist of at most n panels.
|
|
// Each panel has at most 1 page. Take 5 pages in 2 panels UI as an example, the Workspace
|
|
// looks as follow:
|
|
//
|
|
// S: scrollable screen, P: page, <E>: empty
|
|
// S0 S1 S2
|
|
// _______ _______ ________
|
|
// |P0|P1| |P2|P3| |P4|<E>|
|
|
// ¯¯¯¯¯¯¯ ¯¯¯¯¯¯¯ ¯¯¯¯¯¯¯¯
|
|
int endIndex = getNumPagesExcludingEmpty() - 1;
|
|
final int leftPageIndex = mIsRtl ? endIndex : 0;
|
|
final int rightPageIndex = mIsRtl ? 0 : endIndex;
|
|
|
|
// Calculate the scroll range
|
|
int leftPageScrollX = mWorkspace.getScrollForPage(leftPageIndex);
|
|
int rightPageScrollX = mWorkspace.getScrollForPage(rightPageIndex);
|
|
int scrollRange = rightPageScrollX - leftPageScrollX;
|
|
if (scrollRange <= 0) {
|
|
out[0] = 0;
|
|
return;
|
|
}
|
|
|
|
// Sometimes the left parameter of the pages is animated during a layout transition;
|
|
// this parameter offsets it to keep the wallpaper from animating as well
|
|
int adjustedScroll = scroll - leftPageScrollX -
|
|
mWorkspace.getLayoutTransitionOffsetForPage(0);
|
|
adjustedScroll = Utilities.boundToRange(adjustedScroll, 0, scrollRange);
|
|
out[1] = (numScreensForWallpaperParallax - 1) * scrollRange;
|
|
|
|
// The offset is now distributed 0..1 between the left and right pages that we care about,
|
|
// so we just map that between the pages that we are using for parallax
|
|
int rtlOffset = 0;
|
|
if (mIsRtl) {
|
|
// In RTL, the pages are right aligned, so adjust the offset from the end
|
|
rtlOffset = out[1] - (numScrollableScreens - 1) * scrollRange;
|
|
}
|
|
out[0] = rtlOffset + adjustedScroll * (numScrollableScreens - 1);
|
|
}
|
|
|
|
public float wallpaperOffsetForScroll(int scroll) {
|
|
wallpaperOffsetForScroll(scroll, getNumScrollableScreensExcludingEmpty(), sTempInt);
|
|
return ((float) sTempInt[0]) / sTempInt[1];
|
|
}
|
|
|
|
/**
|
|
* Returns the number of screens that can be scrolled.
|
|
*
|
|
* <p>In an usual phone UI, the number of scrollable screens is equal to the number of
|
|
* CellLayouts because each screen has exactly 1 CellLayout.
|
|
*
|
|
* <p>In a n-panels UI, a screen shows n panels. Each panel has at most 1 CellLayout. Take
|
|
* 2-panels UI as an example: let's say there are 5 CellLayouts in the Workspace. the number of
|
|
* scrollable screens will be 3 = ⌈5 / 2⌉.
|
|
*/
|
|
private int getNumScrollableScreensExcludingEmpty() {
|
|
float numOfPages = getNumPagesExcludingEmpty();
|
|
return (int) Math.ceil(numOfPages / mWorkspace.getPanelCount());
|
|
}
|
|
|
|
/**
|
|
* Returns the number of non-empty pages in the Workspace.
|
|
*
|
|
* <p>If a user starts dragging on the rightmost (or leftmost in RTL), an empty CellLayout is
|
|
* added to the Workspace. This empty CellLayout add as a hover-over target for adding a new
|
|
* page. To avoid janky motion effect, we ignore this empty CellLayout.
|
|
*/
|
|
private int getNumPagesExcludingEmpty() {
|
|
int numOfPages = mWorkspace.getChildCount();
|
|
if (numOfPages >= MIN_PARALLAX_PAGE_SPAN && mWorkspace.hasExtraEmptyScreens()) {
|
|
return numOfPages - mWorkspace.getPanelCount();
|
|
} else {
|
|
return numOfPages;
|
|
}
|
|
}
|
|
|
|
public void syncWithScroll() {
|
|
int numScreens = getNumScrollableScreensExcludingEmpty();
|
|
wallpaperOffsetForScroll(mWorkspace.getScrollX(), numScreens, sTempInt);
|
|
Message msg = Message.obtain(mHandler, MSG_UPDATE_OFFSET, sTempInt[0], sTempInt[1],
|
|
mWindowToken);
|
|
if (numScreens != mNumScreens) {
|
|
if (mNumScreens > 0) {
|
|
// Don't animate if we're going from 0 screens
|
|
msg.what = MSG_START_ANIMATION;
|
|
}
|
|
mNumScreens = numScreens;
|
|
updateOffset();
|
|
}
|
|
msg.sendToTarget();
|
|
}
|
|
|
|
/** Returns the number of pages used for the wallpaper parallax. */
|
|
public int getNumPagesForWallpaperParallax() {
|
|
if (mWallpaperIsLiveWallpaper) {
|
|
return mNumScreens;
|
|
} else {
|
|
return Math.max(MIN_PARALLAX_PAGE_SPAN, mNumScreens);
|
|
}
|
|
}
|
|
|
|
@AnyThread
|
|
private void updateOffset() {
|
|
Message.obtain(mHandler, MSG_SET_NUM_PARALLAX, getNumPagesForWallpaperParallax(), 0,
|
|
mWindowToken).sendToTarget();
|
|
}
|
|
|
|
public void jumpToFinal() {
|
|
Message.obtain(mHandler, MSG_JUMP_TO_FINAL, mWindowToken).sendToTarget();
|
|
}
|
|
|
|
public void setWindowToken(IBinder token) {
|
|
mWindowToken = token;
|
|
if (mWindowToken == null && mRegistered) {
|
|
mWallpaperChangeReceiver.unregisterReceiverSafely(mWorkspace.getContext());
|
|
mRegistered = false;
|
|
} else if (mWindowToken != null && !mRegistered) {
|
|
mWallpaperChangeReceiver.register(mWorkspace.getContext(), ACTION_WALLPAPER_CHANGED);
|
|
onWallpaperChanged();
|
|
mRegistered = true;
|
|
}
|
|
}
|
|
|
|
private void onWallpaperChanged() {
|
|
UI_HELPER_EXECUTOR.execute(() -> {
|
|
// Updating the boolean on a background thread is fine as the assignments are atomic
|
|
mWallpaperIsLiveWallpaper = WallpaperManager.getInstance(mWorkspace.getContext())
|
|
.getWallpaperInfo() != null;
|
|
updateOffset();
|
|
});
|
|
}
|
|
|
|
private static final int MSG_START_ANIMATION = 1;
|
|
private static final int MSG_UPDATE_OFFSET = 2;
|
|
private static final int MSG_APPLY_OFFSET = 3;
|
|
private static final int MSG_SET_NUM_PARALLAX = 4;
|
|
private static final int MSG_JUMP_TO_FINAL = 5;
|
|
|
|
private static class OffsetHandler extends Handler {
|
|
|
|
private final Interpolator mInterpolator;
|
|
private final WallpaperManager mWM;
|
|
|
|
private float mCurrentOffset = 0.5f; // to force an initial update
|
|
private boolean mAnimating;
|
|
private long mAnimationStartTime;
|
|
private float mAnimationStartOffset;
|
|
|
|
private float mFinalOffset;
|
|
private float mOffsetX;
|
|
|
|
public OffsetHandler(Context context) {
|
|
super(UI_HELPER_EXECUTOR.getLooper());
|
|
mInterpolator = Interpolators.DECELERATE_1_5;
|
|
mWM = WallpaperManager.getInstance(context);
|
|
}
|
|
|
|
@Override
|
|
public void handleMessage(Message msg) {
|
|
final IBinder token = (IBinder) msg.obj;
|
|
if (token == null) {
|
|
return;
|
|
}
|
|
|
|
switch (msg.what) {
|
|
case MSG_START_ANIMATION: {
|
|
mAnimating = true;
|
|
mAnimationStartOffset = mCurrentOffset;
|
|
mAnimationStartTime = msg.getWhen();
|
|
// Follow through
|
|
}
|
|
case MSG_UPDATE_OFFSET:
|
|
mFinalOffset = ((float) msg.arg1) / msg.arg2;
|
|
// Follow through
|
|
case MSG_APPLY_OFFSET: {
|
|
float oldOffset = mCurrentOffset;
|
|
if (mAnimating) {
|
|
long durationSinceAnimation = SystemClock.uptimeMillis()
|
|
- mAnimationStartTime;
|
|
float t0 = durationSinceAnimation / (float) ANIMATION_DURATION;
|
|
float t1 = mInterpolator.getInterpolation(t0);
|
|
mCurrentOffset = mAnimationStartOffset +
|
|
(mFinalOffset - mAnimationStartOffset) * t1;
|
|
mAnimating = durationSinceAnimation < ANIMATION_DURATION;
|
|
} else {
|
|
mCurrentOffset = mFinalOffset;
|
|
}
|
|
|
|
if (Float.compare(mCurrentOffset, oldOffset) != 0) {
|
|
setOffsetSafely(token);
|
|
// Force the wallpaper offset steps to be set again, because another app
|
|
// might have changed them
|
|
mWM.setWallpaperOffsetSteps(mOffsetX, 1.0f);
|
|
}
|
|
if (mAnimating) {
|
|
// If we are animating, keep updating the offset
|
|
Message.obtain(this, MSG_APPLY_OFFSET, token).sendToTarget();
|
|
}
|
|
return;
|
|
}
|
|
case MSG_SET_NUM_PARALLAX: {
|
|
// Set wallpaper offset steps (1 / (number of screens - 1))
|
|
mOffsetX = 1.0f / (msg.arg1 - 1);
|
|
mWM.setWallpaperOffsetSteps(mOffsetX, 1.0f);
|
|
return;
|
|
}
|
|
case MSG_JUMP_TO_FINAL: {
|
|
if (Float.compare(mCurrentOffset, mFinalOffset) != 0) {
|
|
mCurrentOffset = mFinalOffset;
|
|
setOffsetSafely(token);
|
|
}
|
|
mAnimating = false;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void setOffsetSafely(IBinder token) {
|
|
try {
|
|
mWM.setWallpaperOffsets(token, mCurrentOffset, 0.5f);
|
|
} catch (IllegalArgumentException e) {
|
|
Log.e(TAG, "Error updating wallpaper offset: " + e);
|
|
}
|
|
}
|
|
}
|
|
} |