1package com.android.launcher3.util;
2
3import android.app.WallpaperManager;
4import android.os.IBinder;
5import android.util.Log;
6import android.view.Choreographer;
7import android.view.animation.DecelerateInterpolator;
8import android.view.animation.Interpolator;
9
10import com.android.launcher3.Utilities;
11import com.android.launcher3.Workspace;
12
13/**
14 * Utility class to handle wallpaper scrolling along with workspace.
15 */
16public 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
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
54    public void doFrame(long frameTimeNanos) {
55        updateOffset(false);
56    }
57
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     */
75    public void setLockToDefaultPage(boolean lockToDefaultPage) {
76        mLockedToDefaultPage = lockToDefaultPage;
77    }
78
79    public boolean isLockedToDefaultPage() {
80        return mLockedToDefaultPage;
81    }
82
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     */
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
161    private float wallpaperOffsetForCurrentScroll() {
162        return wallpaperOffsetForScroll(mWorkspace.getScrollX());
163    }
164
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
174    private int getNumScreensExcludingEmptyAndCustom() {
175        return mWorkspace.getChildCount() - numEmptyScreensToIgnore() - mWorkspace.numCustomPages();
176    }
177
178    public void syncWithScroll() {
179        float offset = wallpaperOffsetForCurrentScroll();
180        setFinalX(offset);
181        updateOffset(true);
182    }
183
184    public float getCurrX() {
185        return mCurrentOffset;
186    }
187
188    public float getFinalX() {
189        return mFinalOffset;
190    }
191
192    private void animateToFinal() {
193        mAnimating = true;
194        mAnimationStartOffset = mCurrentOffset;
195        mAnimationStartTime = System.currentTimeMillis();
196    }
197
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
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
220    private void scheduleUpdate() {
221        if (!mWaitingForUpdate) {
222            mChoreographer.postFrameCallback(this);
223            mWaitingForUpdate = true;
224        }
225    }
226
227    public void jumpToFinal() {
228        mCurrentOffset = mFinalOffset;
229    }
230
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
238    public void setWindowToken(IBinder token) {
239        mWindowToken = token;
240    }
241}