1 package com.android.launcher3.util;
2 
3 import android.app.WallpaperManager;
4 import android.os.IBinder;
5 import android.util.Log;
6 import android.view.Choreographer;
7 import android.view.animation.DecelerateInterpolator;
8 import android.view.animation.Interpolator;
9 
10 import com.android.launcher3.Utilities;
11 import com.android.launcher3.Workspace;
12 
13 /**
14  * Utility class to handle wallpaper scrolling along with workspace.
15  */
16 public class WallpaperOffsetInterpolator implements Choreographer.FrameCallback {
17     private static final String TAG = "WPOffsetInterpolator";
18     private static final int ANIMATION_DURATION = 250;
19 
20     // Don't use all the wallpaper for parallax until you have at least this many pages
21     private static final int MIN_PARALLAX_PAGE_SPAN = 4;
22 
23     private final Choreographer mChoreographer;
24     private final Interpolator mInterpolator;
25     private final WallpaperManager mWallpaperManager;
26     private final Workspace mWorkspace;
27     private final boolean mIsRtl;
28 
29     private IBinder mWindowToken;
30     private boolean mWallpaperIsLiveWallpaper;
31     private float mLastSetWallpaperOffsetSteps = 0;
32 
33     private float mFinalOffset = 0.0f;
34     private float mCurrentOffset = 0.5f; // to force an initial update
35     private boolean mWaitingForUpdate;
36     private boolean mLockedToDefaultPage;
37 
38     private boolean mAnimating;
39     private long mAnimationStartTime;
40     private float mAnimationStartOffset;
41     int mNumScreens;
42     int mNumPagesForWallpaperParallax;
43 
WallpaperOffsetInterpolator(Workspace workspace)44     public WallpaperOffsetInterpolator(Workspace workspace) {
45         mChoreographer = Choreographer.getInstance();
46         mInterpolator = new DecelerateInterpolator(1.5f);
47 
48         mWorkspace = workspace;
49         mWallpaperManager = WallpaperManager.getInstance(workspace.getContext());
50         mIsRtl = Utilities.isRtl(workspace.getResources());
51     }
52 
53     @Override
doFrame(long frameTimeNanos)54     public void doFrame(long frameTimeNanos) {
55         updateOffset(false);
56     }
57 
updateOffset(boolean force)58     private void updateOffset(boolean force) {
59         if (mWaitingForUpdate || force) {
60             mWaitingForUpdate = false;
61             if (computeScrollOffset() && mWindowToken != null) {
62                 try {
63                     mWallpaperManager.setWallpaperOffsets(mWindowToken, getCurrX(), 0.5f);
64                     setWallpaperOffsetSteps();
65                 } catch (IllegalArgumentException e) {
66                     Log.e(TAG, "Error updating wallpaper offset: " + e);
67                 }
68             }
69         }
70     }
71 
72     /**
73      * Locks the wallpaper offset to the offset in the default state of Launcher.
74      */
setLockToDefaultPage(boolean lockToDefaultPage)75     public void setLockToDefaultPage(boolean lockToDefaultPage) {
76         mLockedToDefaultPage = lockToDefaultPage;
77     }
78 
isLockedToDefaultPage()79     public boolean isLockedToDefaultPage() {
80         return mLockedToDefaultPage;
81     }
82 
computeScrollOffset()83     public boolean computeScrollOffset() {
84         final float oldOffset = mCurrentOffset;
85         if (mAnimating) {
86             long durationSinceAnimation = System.currentTimeMillis() - mAnimationStartTime;
87             float t0 = durationSinceAnimation / (float) ANIMATION_DURATION;
88             float t1 = mInterpolator.getInterpolation(t0);
89             mCurrentOffset = mAnimationStartOffset +
90                     (mFinalOffset - mAnimationStartOffset) * t1;
91             mAnimating = durationSinceAnimation < ANIMATION_DURATION;
92         } else {
93             mCurrentOffset = mFinalOffset;
94         }
95 
96         if (Math.abs(mCurrentOffset - mFinalOffset) > 0.0000001f) {
97             scheduleUpdate();
98         }
99         if (Math.abs(oldOffset - mCurrentOffset) > 0.0000001f) {
100             return true;
101         }
102         return false;
103     }
104 
105     /**
106      * TODO: do different behavior if it's  a live wallpaper?
107      */
wallpaperOffsetForScroll(int scroll)108     public float wallpaperOffsetForScroll(int scroll) {
109         // To match the default wallpaper behavior in the system, we default to either the left
110         // or right edge on initialization
111         int numScrollingPages = getNumScreensExcludingEmptyAndCustom();
112         if (mLockedToDefaultPage || numScrollingPages <= 1) {
113             return mIsRtl ? 1f : 0f;
114         }
115 
116         // Distribute the wallpaper parallax over a minimum of MIN_PARALLAX_PAGE_SPAN workspace
117         // screens, not including the custom screen, and empty screens (if > MIN_PARALLAX_PAGE_SPAN)
118         if (mWallpaperIsLiveWallpaper) {
119             mNumPagesForWallpaperParallax = numScrollingPages;
120         } else {
121             mNumPagesForWallpaperParallax = Math.max(MIN_PARALLAX_PAGE_SPAN, numScrollingPages);
122         }
123 
124         // Offset by the custom screen
125         int leftPageIndex;
126         int rightPageIndex;
127         if (mIsRtl) {
128             rightPageIndex = mWorkspace.numCustomPages();
129             leftPageIndex = rightPageIndex + numScrollingPages - 1;
130         } else {
131             leftPageIndex = mWorkspace.numCustomPages();
132             rightPageIndex = leftPageIndex + numScrollingPages - 1;
133         }
134 
135         // Calculate the scroll range
136         int leftPageScrollX = mWorkspace.getScrollForPage(leftPageIndex);
137         int rightPageScrollX = mWorkspace.getScrollForPage(rightPageIndex);
138         int scrollRange = rightPageScrollX - leftPageScrollX;
139         if (scrollRange == 0) {
140             return 0f;
141         }
142 
143         // Sometimes the left parameter of the pages is animated during a layout transition;
144         // this parameter offsets it to keep the wallpaper from animating as well
145         int adjustedScroll = scroll - leftPageScrollX -
146                 mWorkspace.getLayoutTransitionOffsetForPage(0);
147         float offset = Utilities.boundToRange((float) adjustedScroll / scrollRange, 0f, 1f);
148 
149         // The offset is now distributed 0..1 between the left and right pages that we care about,
150         // so we just map that between the pages that we are using for parallax
151         float rtlOffset = 0;
152         if (mIsRtl) {
153             // In RTL, the pages are right aligned, so adjust the offset from the end
154             rtlOffset = (float) ((mNumPagesForWallpaperParallax - 1) - (numScrollingPages - 1)) /
155                     (mNumPagesForWallpaperParallax - 1);
156         }
157         return rtlOffset + offset *
158                 ((float) (numScrollingPages - 1) / (mNumPagesForWallpaperParallax - 1));
159     }
160 
wallpaperOffsetForCurrentScroll()161     private float wallpaperOffsetForCurrentScroll() {
162         return wallpaperOffsetForScroll(mWorkspace.getScrollX());
163     }
164 
numEmptyScreensToIgnore()165     private int numEmptyScreensToIgnore() {
166         int numScrollingPages = mWorkspace.getChildCount() - mWorkspace.numCustomPages();
167         if (numScrollingPages >= MIN_PARALLAX_PAGE_SPAN && mWorkspace.hasExtraEmptyScreen()) {
168             return 1;
169         } else {
170             return 0;
171         }
172     }
173 
getNumScreensExcludingEmptyAndCustom()174     private int getNumScreensExcludingEmptyAndCustom() {
175         return mWorkspace.getChildCount() - numEmptyScreensToIgnore() - mWorkspace.numCustomPages();
176     }
177 
syncWithScroll()178     public void syncWithScroll() {
179         float offset = wallpaperOffsetForCurrentScroll();
180         setFinalX(offset);
181         updateOffset(true);
182     }
183 
getCurrX()184     public float getCurrX() {
185         return mCurrentOffset;
186     }
187 
getFinalX()188     public float getFinalX() {
189         return mFinalOffset;
190     }
191 
animateToFinal()192     private void animateToFinal() {
193         mAnimating = true;
194         mAnimationStartOffset = mCurrentOffset;
195         mAnimationStartTime = System.currentTimeMillis();
196     }
197 
setWallpaperOffsetSteps()198     private void setWallpaperOffsetSteps() {
199         // Set wallpaper offset steps (1 / (number of screens - 1))
200         float xOffset = 1.0f / (mNumPagesForWallpaperParallax - 1);
201         if (xOffset != mLastSetWallpaperOffsetSteps) {
202             mWallpaperManager.setWallpaperOffsetSteps(xOffset, 1.0f);
203             mLastSetWallpaperOffsetSteps = xOffset;
204         }
205     }
206 
setFinalX(float x)207     public void setFinalX(float x) {
208         scheduleUpdate();
209         mFinalOffset = Math.max(0f, Math.min(x, 1f));
210         if (getNumScreensExcludingEmptyAndCustom() != mNumScreens) {
211             if (mNumScreens > 0 && Float.compare(mCurrentOffset, mFinalOffset) != 0) {
212                 // Don't animate if we're going from 0 screens, or if the final offset is the same
213                 // as the current offset
214                 animateToFinal();
215             }
216             mNumScreens = getNumScreensExcludingEmptyAndCustom();
217         }
218     }
219 
scheduleUpdate()220     private void scheduleUpdate() {
221         if (!mWaitingForUpdate) {
222             mChoreographer.postFrameCallback(this);
223             mWaitingForUpdate = true;
224         }
225     }
226 
jumpToFinal()227     public void jumpToFinal() {
228         mCurrentOffset = mFinalOffset;
229     }
230 
onResume()231     public void onResume() {
232         mWallpaperIsLiveWallpaper = mWallpaperManager.getWallpaperInfo() != null;
233         // Force the wallpaper offset steps to be set again, because another app might have changed
234         // them
235         mLastSetWallpaperOffsetSteps = 0f;
236     }
237 
setWindowToken(IBinder token)238     public void setWindowToken(IBinder token) {
239         mWindowToken = token;
240     }
241 }