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 }