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 numScrollingPages, final int[] out)66 private void wallpaperOffsetForScroll(int scroll, int numScrollingPages, 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 || numScrollingPages <= 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 numPagesForWallpaperParallax = mWallpaperIsLiveWallpaper ? numScrollingPages : 79 Math.max(MIN_PARALLAX_PAGE_SPAN, numScrollingPages); 80 81 // Offset by the custom screen 82 int leftPageIndex; 83 int rightPageIndex; 84 if (mIsRtl) { 85 rightPageIndex = 0; 86 leftPageIndex = rightPageIndex + numScrollingPages - 1; 87 } else { 88 leftPageIndex = 0; 89 rightPageIndex = leftPageIndex + numScrollingPages - 1; 90 } 91 92 // Calculate the scroll range 93 int leftPageScrollX = mWorkspace.getScrollForPage(leftPageIndex); 94 int rightPageScrollX = mWorkspace.getScrollForPage(rightPageIndex); 95 int scrollRange = rightPageScrollX - leftPageScrollX; 96 if (scrollRange <= 0) { 97 out[0] = 0; 98 return; 99 } 100 101 // Sometimes the left parameter of the pages is animated during a layout transition; 102 // this parameter offsets it to keep the wallpaper from animating as well 103 int adjustedScroll = scroll - leftPageScrollX - 104 mWorkspace.getLayoutTransitionOffsetForPage(0); 105 adjustedScroll = Utilities.boundToRange(adjustedScroll, 0, scrollRange); 106 out[1] = (numPagesForWallpaperParallax - 1) * scrollRange; 107 108 // The offset is now distributed 0..1 between the left and right pages that we care about, 109 // so we just map that between the pages that we are using for parallax 110 int rtlOffset = 0; 111 if (mIsRtl) { 112 // In RTL, the pages are right aligned, so adjust the offset from the end 113 rtlOffset = out[1] - (numScrollingPages - 1) * scrollRange; 114 } 115 out[0] = rtlOffset + adjustedScroll * (numScrollingPages - 1); 116 } 117 wallpaperOffsetForScroll(int scroll)118 public float wallpaperOffsetForScroll(int scroll) { 119 wallpaperOffsetForScroll(scroll, getNumScreensExcludingEmpty(), sTempInt); 120 return ((float) sTempInt[0]) / sTempInt[1]; 121 } 122 getNumScreensExcludingEmpty()123 private int getNumScreensExcludingEmpty() { 124 int numScrollingPages = mWorkspace.getChildCount(); 125 if (numScrollingPages >= MIN_PARALLAX_PAGE_SPAN && mWorkspace.hasExtraEmptyScreen()) { 126 return numScrollingPages - 1; 127 } else { 128 return numScrollingPages; 129 } 130 } 131 syncWithScroll()132 public void syncWithScroll() { 133 int numScreens = getNumScreensExcludingEmpty(); 134 wallpaperOffsetForScroll(mWorkspace.getScrollX(), numScreens, sTempInt); 135 Message msg = Message.obtain(mHandler, MSG_UPDATE_OFFSET, sTempInt[0], sTempInt[1], 136 mWindowToken); 137 if (numScreens != mNumScreens) { 138 if (mNumScreens > 0) { 139 // Don't animate if we're going from 0 screens 140 msg.what = MSG_START_ANIMATION; 141 } 142 mNumScreens = numScreens; 143 updateOffset(); 144 } 145 msg.sendToTarget(); 146 } 147 148 /** Returns the number of pages used for the wallpaper parallax. */ getNumPagesForWallpaperParallax()149 public int getNumPagesForWallpaperParallax() { 150 if (mWallpaperIsLiveWallpaper) { 151 return mNumScreens; 152 } else { 153 return Math.max(MIN_PARALLAX_PAGE_SPAN, mNumScreens); 154 } 155 } 156 updateOffset()157 private void updateOffset() { 158 Message.obtain(mHandler, MSG_SET_NUM_PARALLAX, getNumPagesForWallpaperParallax(), 0, 159 mWindowToken).sendToTarget(); 160 } 161 jumpToFinal()162 public void jumpToFinal() { 163 Message.obtain(mHandler, MSG_JUMP_TO_FINAL, mWindowToken).sendToTarget(); 164 } 165 setWindowToken(IBinder token)166 public void setWindowToken(IBinder token) { 167 mWindowToken = token; 168 if (mWindowToken == null && mRegistered) { 169 mWorkspace.getContext().unregisterReceiver(this); 170 mRegistered = false; 171 } else if (mWindowToken != null && !mRegistered) { 172 mWorkspace.getContext() 173 .registerReceiver(this, new IntentFilter(Intent.ACTION_WALLPAPER_CHANGED)); 174 onReceive(mWorkspace.getContext(), null); 175 mRegistered = true; 176 } 177 } 178 179 @Override onReceive(Context context, Intent intent)180 public void onReceive(Context context, Intent intent) { 181 mWallpaperIsLiveWallpaper = 182 WallpaperManager.getInstance(mWorkspace.getContext()).getWallpaperInfo() != null; 183 updateOffset(); 184 } 185 186 private static final int MSG_START_ANIMATION = 1; 187 private static final int MSG_UPDATE_OFFSET = 2; 188 private static final int MSG_APPLY_OFFSET = 3; 189 private static final int MSG_SET_NUM_PARALLAX = 4; 190 private static final int MSG_JUMP_TO_FINAL = 5; 191 192 private static class OffsetHandler extends Handler { 193 194 private final Interpolator mInterpolator; 195 private final WallpaperManager mWM; 196 197 private float mCurrentOffset = 0.5f; // to force an initial update 198 private boolean mAnimating; 199 private long mAnimationStartTime; 200 private float mAnimationStartOffset; 201 202 private float mFinalOffset; 203 private float mOffsetX; 204 OffsetHandler(Context context)205 public OffsetHandler(Context context) { 206 super(UI_HELPER_EXECUTOR.getLooper()); 207 mInterpolator = Interpolators.DEACCEL_1_5; 208 mWM = WallpaperManager.getInstance(context); 209 } 210 211 @Override handleMessage(Message msg)212 public void handleMessage(Message msg) { 213 final IBinder token = (IBinder) msg.obj; 214 if (token == null) { 215 return; 216 } 217 218 switch (msg.what) { 219 case MSG_START_ANIMATION: { 220 mAnimating = true; 221 mAnimationStartOffset = mCurrentOffset; 222 mAnimationStartTime = msg.getWhen(); 223 // Follow through 224 } 225 case MSG_UPDATE_OFFSET: 226 mFinalOffset = ((float) msg.arg1) / msg.arg2; 227 // Follow through 228 case MSG_APPLY_OFFSET: { 229 float oldOffset = mCurrentOffset; 230 if (mAnimating) { 231 long durationSinceAnimation = SystemClock.uptimeMillis() 232 - mAnimationStartTime; 233 float t0 = durationSinceAnimation / (float) ANIMATION_DURATION; 234 float t1 = mInterpolator.getInterpolation(t0); 235 mCurrentOffset = mAnimationStartOffset + 236 (mFinalOffset - mAnimationStartOffset) * t1; 237 mAnimating = durationSinceAnimation < ANIMATION_DURATION; 238 } else { 239 mCurrentOffset = mFinalOffset; 240 } 241 242 if (Float.compare(mCurrentOffset, oldOffset) != 0) { 243 setOffsetSafely(token); 244 // Force the wallpaper offset steps to be set again, because another app 245 // might have changed them 246 mWM.setWallpaperOffsetSteps(mOffsetX, 1.0f); 247 } 248 if (mAnimating) { 249 // If we are animating, keep updating the offset 250 Message.obtain(this, MSG_APPLY_OFFSET, token).sendToTarget(); 251 } 252 return; 253 } 254 case MSG_SET_NUM_PARALLAX: { 255 // Set wallpaper offset steps (1 / (number of screens - 1)) 256 mOffsetX = 1.0f / (msg.arg1 - 1); 257 mWM.setWallpaperOffsetSteps(mOffsetX, 1.0f); 258 return; 259 } 260 case MSG_JUMP_TO_FINAL: { 261 if (Float.compare(mCurrentOffset, mFinalOffset) != 0) { 262 mCurrentOffset = mFinalOffset; 263 setOffsetSafely(token); 264 } 265 mAnimating = false; 266 return; 267 } 268 } 269 } 270 271 private void setOffsetSafely(IBinder token) { 272 try { 273 mWM.setWallpaperOffsets(token, mCurrentOffset, 0.5f); 274 } catch (IllegalArgumentException e) { 275 Log.e(TAG, "Error updating wallpaper offset: " + e); 276 } 277 } 278 } 279 }