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 updateOffset()148 private void updateOffset() { 149 int numPagesForWallpaperParallax; 150 if (mWallpaperIsLiveWallpaper) { 151 numPagesForWallpaperParallax = mNumScreens; 152 } else { 153 numPagesForWallpaperParallax = Math.max(MIN_PARALLAX_PAGE_SPAN, mNumScreens); 154 } 155 Message.obtain(mHandler, MSG_SET_NUM_PARALLAX, numPagesForWallpaperParallax, 0, 156 mWindowToken).sendToTarget(); 157 } 158 jumpToFinal()159 public void jumpToFinal() { 160 Message.obtain(mHandler, MSG_JUMP_TO_FINAL, mWindowToken).sendToTarget(); 161 } 162 setWindowToken(IBinder token)163 public void setWindowToken(IBinder token) { 164 mWindowToken = token; 165 if (mWindowToken == null && mRegistered) { 166 mWorkspace.getContext().unregisterReceiver(this); 167 mRegistered = false; 168 } else if (mWindowToken != null && !mRegistered) { 169 mWorkspace.getContext() 170 .registerReceiver(this, new IntentFilter(Intent.ACTION_WALLPAPER_CHANGED)); 171 onReceive(mWorkspace.getContext(), null); 172 mRegistered = true; 173 } 174 } 175 176 @Override onReceive(Context context, Intent intent)177 public void onReceive(Context context, Intent intent) { 178 mWallpaperIsLiveWallpaper = 179 WallpaperManager.getInstance(mWorkspace.getContext()).getWallpaperInfo() != null; 180 updateOffset(); 181 } 182 183 private static final int MSG_START_ANIMATION = 1; 184 private static final int MSG_UPDATE_OFFSET = 2; 185 private static final int MSG_APPLY_OFFSET = 3; 186 private static final int MSG_SET_NUM_PARALLAX = 4; 187 private static final int MSG_JUMP_TO_FINAL = 5; 188 189 private static class OffsetHandler extends Handler { 190 191 private final Interpolator mInterpolator; 192 private final WallpaperManager mWM; 193 194 private float mCurrentOffset = 0.5f; // to force an initial update 195 private boolean mAnimating; 196 private long mAnimationStartTime; 197 private float mAnimationStartOffset; 198 199 private float mFinalOffset; 200 private float mOffsetX; 201 OffsetHandler(Context context)202 public OffsetHandler(Context context) { 203 super(UI_HELPER_EXECUTOR.getLooper()); 204 mInterpolator = Interpolators.DEACCEL_1_5; 205 mWM = WallpaperManager.getInstance(context); 206 } 207 208 @Override handleMessage(Message msg)209 public void handleMessage(Message msg) { 210 final IBinder token = (IBinder) msg.obj; 211 if (token == null) { 212 return; 213 } 214 215 switch (msg.what) { 216 case MSG_START_ANIMATION: { 217 mAnimating = true; 218 mAnimationStartOffset = mCurrentOffset; 219 mAnimationStartTime = msg.getWhen(); 220 // Follow through 221 } 222 case MSG_UPDATE_OFFSET: 223 mFinalOffset = ((float) msg.arg1) / msg.arg2; 224 // Follow through 225 case MSG_APPLY_OFFSET: { 226 float oldOffset = mCurrentOffset; 227 if (mAnimating) { 228 long durationSinceAnimation = SystemClock.uptimeMillis() 229 - mAnimationStartTime; 230 float t0 = durationSinceAnimation / (float) ANIMATION_DURATION; 231 float t1 = mInterpolator.getInterpolation(t0); 232 mCurrentOffset = mAnimationStartOffset + 233 (mFinalOffset - mAnimationStartOffset) * t1; 234 mAnimating = durationSinceAnimation < ANIMATION_DURATION; 235 } else { 236 mCurrentOffset = mFinalOffset; 237 } 238 239 if (Float.compare(mCurrentOffset, oldOffset) != 0) { 240 setOffsetSafely(token); 241 // Force the wallpaper offset steps to be set again, because another app 242 // might have changed them 243 mWM.setWallpaperOffsetSteps(mOffsetX, 1.0f); 244 } 245 if (mAnimating) { 246 // If we are animating, keep updating the offset 247 Message.obtain(this, MSG_APPLY_OFFSET, token).sendToTarget(); 248 } 249 return; 250 } 251 case MSG_SET_NUM_PARALLAX: { 252 // Set wallpaper offset steps (1 / (number of screens - 1)) 253 mOffsetX = 1.0f / (msg.arg1 - 1); 254 mWM.setWallpaperOffsetSteps(mOffsetX, 1.0f); 255 return; 256 } 257 case MSG_JUMP_TO_FINAL: { 258 if (Float.compare(mCurrentOffset, mFinalOffset) != 0) { 259 mCurrentOffset = mFinalOffset; 260 setOffsetSafely(token); 261 } 262 mAnimating = false; 263 return; 264 } 265 } 266 } 267 268 private void setOffsetSafely(IBinder token) { 269 try { 270 mWM.setWallpaperOffsets(token, mCurrentOffset, 0.5f); 271 } catch (IllegalArgumentException e) { 272 Log.e(TAG, "Error updating wallpaper offset: " + e); 273 } 274 } 275 } 276 }