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