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 }