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