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