• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 }