• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package com.android.launcher3.util;
2 
3 import static android.content.Intent.ACTION_WALLPAPER_CHANGED;
4 
5 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
6 
7 import android.app.WallpaperManager;
8 import android.content.Context;
9 import android.os.Handler;
10 import android.os.IBinder;
11 import android.os.Message;
12 import android.os.SystemClock;
13 import android.util.Log;
14 import android.view.animation.Interpolator;
15 
16 import androidx.annotation.AnyThread;
17 
18 import com.android.app.animation.Interpolators;
19 import com.android.launcher3.Utilities;
20 import com.android.launcher3.Workspace;
21 
22 /**
23  * Utility class to handle wallpaper scrolling along with workspace.
24  */
25 public class WallpaperOffsetInterpolator {
26 
27     private static final int[] sTempInt = new int[2];
28     private static final String TAG = "WPOffsetInterpolator";
29     private static final int ANIMATION_DURATION = 250;
30 
31     // Don't use all the wallpaper for parallax until you have at least this many pages
32     private static final int MIN_PARALLAX_PAGE_SPAN = 4;
33 
34     private final SimpleBroadcastReceiver mWallpaperChangeReceiver;
35     private final Workspace<?> mWorkspace;
36     private final boolean mIsRtl;
37     private final Handler mHandler;
38 
39     private boolean mRegistered = false;
40     private IBinder mWindowToken;
41     private boolean mWallpaperIsLiveWallpaper;
42 
43     private boolean mLockedToDefaultPage;
44     private int mNumScreens;
45 
WallpaperOffsetInterpolator(Workspace<?> workspace)46     public WallpaperOffsetInterpolator(Workspace<?> workspace) {
47         mWorkspace = workspace;
48         mWallpaperChangeReceiver = new SimpleBroadcastReceiver(
49                 workspace.getContext(), UI_HELPER_EXECUTOR, i -> onWallpaperChanged());
50         mIsRtl = Utilities.isRtl(workspace.getResources());
51         mHandler = new OffsetHandler(workspace.getContext());
52     }
53 
54     /**
55      * Locks the wallpaper offset to the offset in the default state of Launcher.
56      */
setLockToDefaultPage(boolean lockToDefaultPage)57     public void setLockToDefaultPage(boolean lockToDefaultPage) {
58         mLockedToDefaultPage = lockToDefaultPage;
59     }
60 
isLockedToDefaultPage()61     public boolean isLockedToDefaultPage() {
62         return mLockedToDefaultPage;
63     }
64 
65     /**
66      * Computes the wallpaper offset as an int ratio (out[0] / out[1])
67      *
68      * TODO: do different behavior if it's  a live wallpaper?
69      */
wallpaperOffsetForScroll(int scroll, int numScrollableScreens, final int[] out)70     private void wallpaperOffsetForScroll(int scroll, int numScrollableScreens, final int[] out) {
71         out[1] = 1;
72 
73         // To match the default wallpaper behavior in the system, we default to either the left
74         // or right edge on initialization
75         if (mLockedToDefaultPage || numScrollableScreens <= 1) {
76             out[0] =  mIsRtl ? 1 : 0;
77             return;
78         }
79 
80         // Distribute the wallpaper parallax over a minimum of MIN_PARALLAX_PAGE_SPAN workspace
81         // screens, not including the custom screen, and empty screens (if > MIN_PARALLAX_PAGE_SPAN)
82         int numScreensForWallpaperParallax = mWallpaperIsLiveWallpaper ? numScrollableScreens :
83                         Math.max(MIN_PARALLAX_PAGE_SPAN, numScrollableScreens);
84 
85         // Offset by the custom screen
86 
87         // Don't confuse screens & pages in this function. In a phone UI, we often use screens &
88         // pages interchangeably. However, in a n-panels UI, where n > 1, the screen in this class
89         // means the scrollable screen. Each screen can consist of at most n panels.
90         // Each panel has at most 1 page. Take 5 pages in 2 panels UI as an example, the Workspace
91         // looks as follow:
92         //
93         // S: scrollable screen, P: page, <E>: empty
94         //   S0        S1         S2
95         // _______   _______   ________
96         // |P0|P1|   |P2|P3|   |P4|<E>|
97         // ¯¯¯¯¯¯¯   ¯¯¯¯¯¯¯   ¯¯¯¯¯¯¯¯
98         int endIndex = getNumPagesExcludingEmpty() - 1;
99         final int leftPageIndex = mIsRtl ? endIndex : 0;
100         final int rightPageIndex = mIsRtl ? 0 : endIndex;
101 
102         // Calculate the scroll range
103         int leftPageScrollX = mWorkspace.getScrollForPage(leftPageIndex);
104         int rightPageScrollX = mWorkspace.getScrollForPage(rightPageIndex);
105         int scrollRange = rightPageScrollX - leftPageScrollX;
106         if (scrollRange <= 0) {
107             out[0] = 0;
108             return;
109         }
110 
111         // Sometimes the left parameter of the pages is animated during a layout transition;
112         // this parameter offsets it to keep the wallpaper from animating as well
113         int adjustedScroll = scroll - leftPageScrollX -
114                 mWorkspace.getLayoutTransitionOffsetForPage(0);
115         adjustedScroll = Utilities.boundToRange(adjustedScroll, 0, scrollRange);
116         out[1] = (numScreensForWallpaperParallax - 1) * scrollRange;
117 
118         // The offset is now distributed 0..1 between the left and right pages that we care about,
119         // so we just map that between the pages that we are using for parallax
120         int rtlOffset = 0;
121         if (mIsRtl) {
122             // In RTL, the pages are right aligned, so adjust the offset from the end
123             rtlOffset = out[1] - (numScrollableScreens - 1) * scrollRange;
124         }
125         out[0] = rtlOffset + adjustedScroll * (numScrollableScreens - 1);
126     }
127 
wallpaperOffsetForScroll(int scroll)128     public float wallpaperOffsetForScroll(int scroll) {
129         wallpaperOffsetForScroll(scroll, getNumScrollableScreensExcludingEmpty(), sTempInt);
130         return ((float) sTempInt[0]) / sTempInt[1];
131     }
132 
133     /**
134      * Returns the number of screens that can be scrolled.
135      *
136      * <p>In an usual phone UI, the number of scrollable screens is equal to the number of
137      * CellLayouts because each screen has exactly 1 CellLayout.
138      *
139      * <p>In a n-panels UI, a screen shows n panels. Each panel has at most 1 CellLayout. Take
140      * 2-panels UI as an example: let's say there are 5 CellLayouts in the Workspace. the number of
141      * scrollable screens will be 3 = ⌈5 / 2⌉.
142      */
getNumScrollableScreensExcludingEmpty()143     private int getNumScrollableScreensExcludingEmpty() {
144         float numOfPages = getNumPagesExcludingEmpty();
145         return (int) Math.ceil(numOfPages / mWorkspace.getPanelCount());
146     }
147 
148     /**
149      * Returns the number of non-empty pages in the Workspace.
150      *
151      * <p>If a user starts dragging on the rightmost (or leftmost in RTL), an empty CellLayout is
152      * added to the Workspace. This empty CellLayout add as a hover-over target for adding a new
153      * page. To avoid janky motion effect, we ignore this empty CellLayout.
154      */
getNumPagesExcludingEmpty()155     private int getNumPagesExcludingEmpty() {
156         int numOfPages = mWorkspace.getChildCount();
157         if (numOfPages >= MIN_PARALLAX_PAGE_SPAN && mWorkspace.hasExtraEmptyScreens()) {
158             return numOfPages - mWorkspace.getPanelCount();
159         } else {
160             return numOfPages;
161         }
162     }
163 
syncWithScroll()164     public void syncWithScroll() {
165         int numScreens = getNumScrollableScreensExcludingEmpty();
166         wallpaperOffsetForScroll(mWorkspace.getScrollX(), numScreens, sTempInt);
167         Message msg = Message.obtain(mHandler, MSG_UPDATE_OFFSET, sTempInt[0], sTempInt[1],
168                 mWindowToken);
169         if (numScreens != mNumScreens) {
170             if (mNumScreens > 0) {
171                 // Don't animate if we're going from 0 screens
172                 msg.what = MSG_START_ANIMATION;
173             }
174             mNumScreens = numScreens;
175             updateOffset();
176         }
177         msg.sendToTarget();
178     }
179 
180     /** Returns the number of pages used for the wallpaper parallax. */
getNumPagesForWallpaperParallax()181     public int getNumPagesForWallpaperParallax() {
182         if (mWallpaperIsLiveWallpaper) {
183             return mNumScreens;
184         } else {
185             return Math.max(MIN_PARALLAX_PAGE_SPAN, mNumScreens);
186         }
187     }
188 
189     @AnyThread
updateOffset()190     private void updateOffset() {
191         Message.obtain(mHandler, MSG_SET_NUM_PARALLAX, getNumPagesForWallpaperParallax(), 0,
192                 mWindowToken).sendToTarget();
193     }
194 
jumpToFinal()195     public void jumpToFinal() {
196         Message.obtain(mHandler, MSG_JUMP_TO_FINAL, mWindowToken).sendToTarget();
197     }
198 
setWindowToken(IBinder token)199     public void setWindowToken(IBinder token) {
200         mWindowToken = token;
201         if (mWindowToken == null && mRegistered) {
202             mWallpaperChangeReceiver.unregisterReceiverSafely();
203             mRegistered = false;
204         } else if (mWindowToken != null && !mRegistered) {
205             mWallpaperChangeReceiver.register(ACTION_WALLPAPER_CHANGED);
206             onWallpaperChanged();
207             mRegistered = true;
208         }
209     }
210 
onWallpaperChanged()211     private void onWallpaperChanged() {
212         UI_HELPER_EXECUTOR.execute(() -> {
213             // Updating the boolean on a background thread is fine as the assignments are atomic
214             mWallpaperIsLiveWallpaper = WallpaperManager.getInstance(mWorkspace.getContext())
215                     .getWallpaperInfo() != null;
216             updateOffset();
217         });
218     }
219 
220     private static final int MSG_START_ANIMATION = 1;
221     private static final int MSG_UPDATE_OFFSET = 2;
222     private static final int MSG_APPLY_OFFSET = 3;
223     private static final int MSG_SET_NUM_PARALLAX = 4;
224     private static final int MSG_JUMP_TO_FINAL = 5;
225 
226     private static class OffsetHandler extends Handler {
227 
228         private final Interpolator mInterpolator;
229         private final WallpaperManager mWM;
230 
231         private float mCurrentOffset = 0.5f; // to force an initial update
232         private boolean mAnimating;
233         private long mAnimationStartTime;
234         private float mAnimationStartOffset;
235 
236         private float mFinalOffset;
237         private float mOffsetX;
238 
OffsetHandler(Context context)239         public OffsetHandler(Context context) {
240             super(UI_HELPER_EXECUTOR.getLooper());
241             mInterpolator = Interpolators.DECELERATE_1_5;
242             mWM = WallpaperManager.getInstance(context);
243         }
244 
245         @Override
handleMessage(Message msg)246         public void handleMessage(Message msg) {
247             final IBinder token = (IBinder) msg.obj;
248             if (token == null) {
249                 return;
250             }
251 
252             switch (msg.what) {
253                 case MSG_START_ANIMATION: {
254                     mAnimating = true;
255                     mAnimationStartOffset = mCurrentOffset;
256                     mAnimationStartTime = msg.getWhen();
257                     // Follow through
258                 }
259                 case MSG_UPDATE_OFFSET:
260                     mFinalOffset = ((float) msg.arg1) / msg.arg2;
261                     // Follow through
262                 case MSG_APPLY_OFFSET: {
263                     float oldOffset = mCurrentOffset;
264                     if (mAnimating) {
265                         long durationSinceAnimation = SystemClock.uptimeMillis()
266                                 - mAnimationStartTime;
267                         float t0 = durationSinceAnimation / (float) ANIMATION_DURATION;
268                         float t1 = mInterpolator.getInterpolation(t0);
269                         mCurrentOffset = mAnimationStartOffset +
270                                 (mFinalOffset - mAnimationStartOffset) * t1;
271                         mAnimating = durationSinceAnimation < ANIMATION_DURATION;
272                     } else {
273                         mCurrentOffset = mFinalOffset;
274                     }
275 
276                     if (Float.compare(mCurrentOffset, oldOffset) != 0) {
277                         setOffsetSafely(token);
278                         // Force the wallpaper offset steps to be set again, because another app
279                         // might have changed them
280                         mWM.setWallpaperOffsetSteps(mOffsetX, 1.0f);
281                     }
282                     if (mAnimating) {
283                         // If we are animating, keep updating the offset
284                         Message.obtain(this, MSG_APPLY_OFFSET, token).sendToTarget();
285                     }
286                     return;
287                 }
288                 case MSG_SET_NUM_PARALLAX: {
289                     // Set wallpaper offset steps (1 / (number of screens - 1))
290                     mOffsetX = 1.0f / (msg.arg1 - 1);
291                     mWM.setWallpaperOffsetSteps(mOffsetX, 1.0f);
292                     return;
293                 }
294                 case MSG_JUMP_TO_FINAL: {
295                     if (Float.compare(mCurrentOffset, mFinalOffset) != 0) {
296                         mCurrentOffset = mFinalOffset;
297                         setOffsetSafely(token);
298                     }
299                     mAnimating = false;
300                     return;
301                 }
302             }
303         }
304 
305         private void setOffsetSafely(IBinder token) {
306             try {
307                 mWM.setWallpaperOffsets(token, mCurrentOffset, 0.5f);
308             } catch (IllegalArgumentException e) {
309                 Log.e(TAG, "Error updating wallpaper offset: " + e);
310             }
311         }
312     }
313 }