1 package com.android.launcher3.util; 2 3 import static android.content.Intent.ACTION_WALLPAPER_CHANGED; 4 5 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; 6 7 import android.app.WallpaperManager; 8 import android.content.Context; 9 import android.os.Handler; 10 import android.os.IBinder; 11 import android.os.Message; 12 import android.os.SystemClock; 13 import android.util.Log; 14 import android.view.animation.Interpolator; 15 16 import androidx.annotation.AnyThread; 17 18 import com.android.app.animation.Interpolators; 19 import com.android.launcher3.Utilities; 20 import com.android.launcher3.Workspace; 21 22 /** 23 * Utility class to handle wallpaper scrolling along with workspace. 24 */ 25 public class WallpaperOffsetInterpolator { 26 27 private static final int[] sTempInt = new int[2]; 28 private static final String TAG = "WPOffsetInterpolator"; 29 private static final int ANIMATION_DURATION = 250; 30 31 // Don't use all the wallpaper for parallax until you have at least this many pages 32 private static final int MIN_PARALLAX_PAGE_SPAN = 4; 33 34 private final SimpleBroadcastReceiver mWallpaperChangeReceiver = 35 new SimpleBroadcastReceiver(i -> onWallpaperChanged()); 36 private final Workspace<?> mWorkspace; 37 private final boolean mIsRtl; 38 private final Handler mHandler; 39 40 private boolean mRegistered = false; 41 private IBinder mWindowToken; 42 private boolean mWallpaperIsLiveWallpaper; 43 44 private boolean mLockedToDefaultPage; 45 private int mNumScreens; 46 WallpaperOffsetInterpolator(Workspace<?> workspace)47 public WallpaperOffsetInterpolator(Workspace<?> workspace) { 48 mWorkspace = workspace; 49 mIsRtl = Utilities.isRtl(workspace.getResources()); 50 mHandler = new OffsetHandler(workspace.getContext()); 51 } 52 53 /** 54 * Locks the wallpaper offset to the offset in the default state of Launcher. 55 */ setLockToDefaultPage(boolean lockToDefaultPage)56 public void setLockToDefaultPage(boolean lockToDefaultPage) { 57 mLockedToDefaultPage = lockToDefaultPage; 58 } 59 isLockedToDefaultPage()60 public boolean isLockedToDefaultPage() { 61 return mLockedToDefaultPage; 62 } 63 64 /** 65 * Computes the wallpaper offset as an int ratio (out[0] / out[1]) 66 * 67 * TODO: do different behavior if it's a live wallpaper? 68 */ wallpaperOffsetForScroll(int scroll, int numScrollableScreens, final int[] out)69 private void wallpaperOffsetForScroll(int scroll, int numScrollableScreens, final int[] out) { 70 out[1] = 1; 71 72 // To match the default wallpaper behavior in the system, we default to either the left 73 // or right edge on initialization 74 if (mLockedToDefaultPage || numScrollableScreens <= 1) { 75 out[0] = mIsRtl ? 1 : 0; 76 return; 77 } 78 79 // Distribute the wallpaper parallax over a minimum of MIN_PARALLAX_PAGE_SPAN workspace 80 // screens, not including the custom screen, and empty screens (if > MIN_PARALLAX_PAGE_SPAN) 81 int numScreensForWallpaperParallax = mWallpaperIsLiveWallpaper ? numScrollableScreens : 82 Math.max(MIN_PARALLAX_PAGE_SPAN, numScrollableScreens); 83 84 // Offset by the custom screen 85 86 // Don't confuse screens & pages in this function. In a phone UI, we often use screens & 87 // pages interchangeably. However, in a n-panels UI, where n > 1, the screen in this class 88 // means the scrollable screen. Each screen can consist of at most n panels. 89 // Each panel has at most 1 page. Take 5 pages in 2 panels UI as an example, the Workspace 90 // looks as follow: 91 // 92 // S: scrollable screen, P: page, <E>: empty 93 // S0 S1 S2 94 // _______ _______ ________ 95 // |P0|P1| |P2|P3| |P4|<E>| 96 // ¯¯¯¯¯¯¯ ¯¯¯¯¯¯¯ ¯¯¯¯¯¯¯¯ 97 int endIndex = getNumPagesExcludingEmpty() - 1; 98 final int leftPageIndex = mIsRtl ? endIndex : 0; 99 final int rightPageIndex = mIsRtl ? 0 : endIndex; 100 101 // Calculate the scroll range 102 int leftPageScrollX = mWorkspace.getScrollForPage(leftPageIndex); 103 int rightPageScrollX = mWorkspace.getScrollForPage(rightPageIndex); 104 int scrollRange = rightPageScrollX - leftPageScrollX; 105 if (scrollRange <= 0) { 106 out[0] = 0; 107 return; 108 } 109 110 // Sometimes the left parameter of the pages is animated during a layout transition; 111 // this parameter offsets it to keep the wallpaper from animating as well 112 int adjustedScroll = scroll - leftPageScrollX - 113 mWorkspace.getLayoutTransitionOffsetForPage(0); 114 adjustedScroll = Utilities.boundToRange(adjustedScroll, 0, scrollRange); 115 out[1] = (numScreensForWallpaperParallax - 1) * scrollRange; 116 117 // The offset is now distributed 0..1 between the left and right pages that we care about, 118 // so we just map that between the pages that we are using for parallax 119 int rtlOffset = 0; 120 if (mIsRtl) { 121 // In RTL, the pages are right aligned, so adjust the offset from the end 122 rtlOffset = out[1] - (numScrollableScreens - 1) * scrollRange; 123 } 124 out[0] = rtlOffset + adjustedScroll * (numScrollableScreens - 1); 125 } 126 wallpaperOffsetForScroll(int scroll)127 public float wallpaperOffsetForScroll(int scroll) { 128 wallpaperOffsetForScroll(scroll, getNumScrollableScreensExcludingEmpty(), sTempInt); 129 return ((float) sTempInt[0]) / sTempInt[1]; 130 } 131 132 /** 133 * Returns the number of screens that can be scrolled. 134 * 135 * <p>In an usual phone UI, the number of scrollable screens is equal to the number of 136 * CellLayouts because each screen has exactly 1 CellLayout. 137 * 138 * <p>In a n-panels UI, a screen shows n panels. Each panel has at most 1 CellLayout. Take 139 * 2-panels UI as an example: let's say there are 5 CellLayouts in the Workspace. the number of 140 * scrollable screens will be 3 = ⌈5 / 2⌉. 141 */ getNumScrollableScreensExcludingEmpty()142 private int getNumScrollableScreensExcludingEmpty() { 143 float numOfPages = getNumPagesExcludingEmpty(); 144 return (int) Math.ceil(numOfPages / mWorkspace.getPanelCount()); 145 } 146 147 /** 148 * Returns the number of non-empty pages in the Workspace. 149 * 150 * <p>If a user starts dragging on the rightmost (or leftmost in RTL), an empty CellLayout is 151 * added to the Workspace. This empty CellLayout add as a hover-over target for adding a new 152 * page. To avoid janky motion effect, we ignore this empty CellLayout. 153 */ getNumPagesExcludingEmpty()154 private int getNumPagesExcludingEmpty() { 155 int numOfPages = mWorkspace.getChildCount(); 156 if (numOfPages >= MIN_PARALLAX_PAGE_SPAN && mWorkspace.hasExtraEmptyScreens()) { 157 return numOfPages - mWorkspace.getPanelCount(); 158 } else { 159 return numOfPages; 160 } 161 } 162 syncWithScroll()163 public void syncWithScroll() { 164 int numScreens = getNumScrollableScreensExcludingEmpty(); 165 wallpaperOffsetForScroll(mWorkspace.getScrollX(), numScreens, sTempInt); 166 Message msg = Message.obtain(mHandler, MSG_UPDATE_OFFSET, sTempInt[0], sTempInt[1], 167 mWindowToken); 168 if (numScreens != mNumScreens) { 169 if (mNumScreens > 0) { 170 // Don't animate if we're going from 0 screens 171 msg.what = MSG_START_ANIMATION; 172 } 173 mNumScreens = numScreens; 174 updateOffset(); 175 } 176 msg.sendToTarget(); 177 } 178 179 /** Returns the number of pages used for the wallpaper parallax. */ getNumPagesForWallpaperParallax()180 public int getNumPagesForWallpaperParallax() { 181 if (mWallpaperIsLiveWallpaper) { 182 return mNumScreens; 183 } else { 184 return Math.max(MIN_PARALLAX_PAGE_SPAN, mNumScreens); 185 } 186 } 187 188 @AnyThread updateOffset()189 private void updateOffset() { 190 Message.obtain(mHandler, MSG_SET_NUM_PARALLAX, getNumPagesForWallpaperParallax(), 0, 191 mWindowToken).sendToTarget(); 192 } 193 jumpToFinal()194 public void jumpToFinal() { 195 Message.obtain(mHandler, MSG_JUMP_TO_FINAL, mWindowToken).sendToTarget(); 196 } 197 setWindowToken(IBinder token)198 public void setWindowToken(IBinder token) { 199 mWindowToken = token; 200 if (mWindowToken == null && mRegistered) { 201 mWallpaperChangeReceiver.unregisterReceiverSafely(mWorkspace.getContext()); 202 mRegistered = false; 203 } else if (mWindowToken != null && !mRegistered) { 204 mWallpaperChangeReceiver.register(mWorkspace.getContext(), ACTION_WALLPAPER_CHANGED); 205 onWallpaperChanged(); 206 mRegistered = true; 207 } 208 } 209 onWallpaperChanged()210 private void onWallpaperChanged() { 211 UI_HELPER_EXECUTOR.execute(() -> { 212 // Updating the boolean on a background thread is fine as the assignments are atomic 213 mWallpaperIsLiveWallpaper = WallpaperManager.getInstance(mWorkspace.getContext()) 214 .getWallpaperInfo() != null; 215 updateOffset(); 216 }); 217 } 218 219 private static final int MSG_START_ANIMATION = 1; 220 private static final int MSG_UPDATE_OFFSET = 2; 221 private static final int MSG_APPLY_OFFSET = 3; 222 private static final int MSG_SET_NUM_PARALLAX = 4; 223 private static final int MSG_JUMP_TO_FINAL = 5; 224 225 private static class OffsetHandler extends Handler { 226 227 private final Interpolator mInterpolator; 228 private final WallpaperManager mWM; 229 230 private float mCurrentOffset = 0.5f; // to force an initial update 231 private boolean mAnimating; 232 private long mAnimationStartTime; 233 private float mAnimationStartOffset; 234 235 private float mFinalOffset; 236 private float mOffsetX; 237 OffsetHandler(Context context)238 public OffsetHandler(Context context) { 239 super(UI_HELPER_EXECUTOR.getLooper()); 240 mInterpolator = Interpolators.DECELERATE_1_5; 241 mWM = WallpaperManager.getInstance(context); 242 } 243 244 @Override handleMessage(Message msg)245 public void handleMessage(Message msg) { 246 final IBinder token = (IBinder) msg.obj; 247 if (token == null) { 248 return; 249 } 250 251 switch (msg.what) { 252 case MSG_START_ANIMATION: { 253 mAnimating = true; 254 mAnimationStartOffset = mCurrentOffset; 255 mAnimationStartTime = msg.getWhen(); 256 // Follow through 257 } 258 case MSG_UPDATE_OFFSET: 259 mFinalOffset = ((float) msg.arg1) / msg.arg2; 260 // Follow through 261 case MSG_APPLY_OFFSET: { 262 float oldOffset = mCurrentOffset; 263 if (mAnimating) { 264 long durationSinceAnimation = SystemClock.uptimeMillis() 265 - mAnimationStartTime; 266 float t0 = durationSinceAnimation / (float) ANIMATION_DURATION; 267 float t1 = mInterpolator.getInterpolation(t0); 268 mCurrentOffset = mAnimationStartOffset + 269 (mFinalOffset - mAnimationStartOffset) * t1; 270 mAnimating = durationSinceAnimation < ANIMATION_DURATION; 271 } else { 272 mCurrentOffset = mFinalOffset; 273 } 274 275 if (Float.compare(mCurrentOffset, oldOffset) != 0) { 276 setOffsetSafely(token); 277 // Force the wallpaper offset steps to be set again, because another app 278 // might have changed them 279 mWM.setWallpaperOffsetSteps(mOffsetX, 1.0f); 280 } 281 if (mAnimating) { 282 // If we are animating, keep updating the offset 283 Message.obtain(this, MSG_APPLY_OFFSET, token).sendToTarget(); 284 } 285 return; 286 } 287 case MSG_SET_NUM_PARALLAX: { 288 // Set wallpaper offset steps (1 / (number of screens - 1)) 289 mOffsetX = 1.0f / (msg.arg1 - 1); 290 mWM.setWallpaperOffsetSteps(mOffsetX, 1.0f); 291 return; 292 } 293 case MSG_JUMP_TO_FINAL: { 294 if (Float.compare(mCurrentOffset, mFinalOffset) != 0) { 295 mCurrentOffset = mFinalOffset; 296 setOffsetSafely(token); 297 } 298 mAnimating = false; 299 return; 300 } 301 } 302 } 303 304 private void setOffsetSafely(IBinder token) { 305 try { 306 mWM.setWallpaperOffsets(token, mCurrentOffset, 0.5f); 307 } catch (IllegalArgumentException e) { 308 Log.e(TAG, "Error updating wallpaper offset: " + e); 309 } 310 } 311 } 312 }