1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.systemui.recents.views;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ObjectAnimator;
22import android.content.Context;
23import android.util.FloatProperty;
24import android.util.Log;
25import android.util.MutableFloat;
26import android.util.Property;
27import android.view.ViewDebug;
28import android.widget.OverScroller;
29
30import com.android.systemui.Interpolators;
31import com.android.systemui.R;
32import com.android.systemui.recents.misc.Utilities;
33
34import java.io.PrintWriter;
35
36/* The scrolling logic for a TaskStackView */
37public class TaskStackViewScroller {
38
39    private static final String TAG = "TaskStackViewScroller";
40    private static final boolean DEBUG = false;
41
42    public interface TaskStackViewScrollerCallbacks {
43        void onStackScrollChanged(float prevScroll, float curScroll, AnimationProps animation);
44    }
45
46    /**
47     * A Property wrapper around the <code>stackScroll</code> functionality handled by the
48     * {@link #setStackScroll(float)} and
49     * {@link #getStackScroll()} methods.
50     */
51    private static final Property<TaskStackViewScroller, Float> STACK_SCROLL =
52            new FloatProperty<TaskStackViewScroller>("stackScroll") {
53                @Override
54                public void setValue(TaskStackViewScroller object, float value) {
55                    object.setStackScroll(value);
56                }
57
58                @Override
59                public Float get(TaskStackViewScroller object) {
60                    return object.getStackScroll();
61                }
62            };
63
64    Context mContext;
65    TaskStackLayoutAlgorithm mLayoutAlgorithm;
66    TaskStackViewScrollerCallbacks mCb;
67
68    @ViewDebug.ExportedProperty(category="recents")
69    float mStackScrollP;
70    @ViewDebug.ExportedProperty(category="recents")
71    float mLastDeltaP = 0f;
72    float mFlingDownScrollP;
73    int mFlingDownY;
74
75    OverScroller mScroller;
76    ObjectAnimator mScrollAnimator;
77    float mFinalAnimatedScroll;
78
79    public TaskStackViewScroller(Context context, TaskStackViewScrollerCallbacks cb,
80            TaskStackLayoutAlgorithm layoutAlgorithm) {
81        mContext = context;
82        mCb = cb;
83        mScroller = new OverScroller(context);
84        mLayoutAlgorithm = layoutAlgorithm;
85    }
86
87    /** Resets the task scroller. */
88    void reset() {
89        mStackScrollP = 0f;
90        mLastDeltaP = 0f;
91    }
92
93    void resetDeltaScroll() {
94        mLastDeltaP = 0f;
95    }
96
97    /** Gets the current stack scroll */
98    public float getStackScroll() {
99        return mStackScrollP;
100    }
101
102    /**
103     * Sets the current stack scroll immediately.
104     */
105    public void setStackScroll(float s) {
106        setStackScroll(s, AnimationProps.IMMEDIATE);
107    }
108
109    /**
110     * Sets the current stack scroll immediately, and returns the difference between the target
111     * scroll and the actual scroll after accounting for the effect on the focus state.
112     */
113    public float setDeltaStackScroll(float downP, float deltaP) {
114        float targetScroll = downP + deltaP;
115        float newScroll = mLayoutAlgorithm.updateFocusStateOnScroll(downP + mLastDeltaP, targetScroll,
116                mStackScrollP);
117        setStackScroll(newScroll, AnimationProps.IMMEDIATE);
118        mLastDeltaP = deltaP;
119        return newScroll - targetScroll;
120    }
121
122    /**
123     * Sets the current stack scroll, but indicates to the callback the preferred animation to
124     * update to this new scroll.
125     */
126    public void setStackScroll(float newScroll, AnimationProps animation) {
127        float prevScroll = mStackScrollP;
128        mStackScrollP = newScroll;
129        if (mCb != null) {
130            mCb.onStackScrollChanged(prevScroll, mStackScrollP, animation);
131        }
132    }
133
134    /**
135     * Sets the current stack scroll to the initial state when you first enter recents.
136     * @return whether the stack progress changed.
137     */
138    public boolean setStackScrollToInitialState() {
139        float prevScroll = mStackScrollP;
140        setStackScroll(mLayoutAlgorithm.mInitialScrollP);
141        return Float.compare(prevScroll, mStackScrollP) != 0;
142    }
143
144    /**
145     * Starts a fling that is coordinated with the {@link TaskStackViewTouchHandler}.
146     */
147    public void fling(float downScrollP, int downY, int y, int velY, int minY, int maxY,
148            int overscroll) {
149        if (DEBUG) {
150            Log.d(TAG, "fling: " + downScrollP + ", downY: " + downY + ", y: " + y +
151                    ", velY: " + velY + ", minY: " + minY + ", maxY: " + maxY);
152        }
153        mFlingDownScrollP = downScrollP;
154        mFlingDownY = downY;
155        mScroller.fling(0, y, 0, velY, 0, 0, minY, maxY, 0, overscroll);
156    }
157
158    /** Bounds the current scroll if necessary */
159    public boolean boundScroll() {
160        float curScroll = getStackScroll();
161        float newScroll = getBoundedStackScroll(curScroll);
162        if (Float.compare(newScroll, curScroll) != 0) {
163            setStackScroll(newScroll);
164            return true;
165        }
166        return false;
167    }
168
169    /** Returns the bounded stack scroll */
170    float getBoundedStackScroll(float scroll) {
171        return Utilities.clamp(scroll, mLayoutAlgorithm.mMinScrollP, mLayoutAlgorithm.mMaxScrollP);
172    }
173
174    /** Returns the amount that the absolute value of how much the scroll is out of bounds. */
175    float getScrollAmountOutOfBounds(float scroll) {
176        if (scroll < mLayoutAlgorithm.mMinScrollP) {
177            return Math.abs(scroll - mLayoutAlgorithm.mMinScrollP);
178        } else if (scroll > mLayoutAlgorithm.mMaxScrollP) {
179            return Math.abs(scroll - mLayoutAlgorithm.mMaxScrollP);
180        }
181        return 0f;
182    }
183
184    /** Returns whether the specified scroll is out of bounds */
185    boolean isScrollOutOfBounds() {
186        return Float.compare(getScrollAmountOutOfBounds(mStackScrollP), 0f) != 0;
187    }
188
189    /** Animates the stack scroll into bounds */
190    ObjectAnimator animateBoundScroll() {
191        // TODO: Take duration for snap back
192        float curScroll = getStackScroll();
193        float newScroll = getBoundedStackScroll(curScroll);
194        if (Float.compare(newScroll, curScroll) != 0) {
195            // Start a new scroll animation
196            animateScroll(newScroll, null /* postScrollRunnable */);
197        }
198        return mScrollAnimator;
199    }
200
201    /** Animates the stack scroll */
202    void animateScroll(float newScroll, final Runnable postRunnable) {
203        int duration = mContext.getResources().getInteger(
204                R.integer.recents_animate_task_stack_scroll_duration);
205        animateScroll(newScroll, duration, postRunnable);
206    }
207
208    /** Animates the stack scroll */
209    void animateScroll(float newScroll, int duration, final Runnable postRunnable) {
210        // Finish any current scrolling animations
211        if (mScrollAnimator != null && mScrollAnimator.isRunning()) {
212            setStackScroll(mFinalAnimatedScroll);
213            mScroller.forceFinished(true);
214        }
215        stopScroller();
216        stopBoundScrollAnimation();
217
218        if (Float.compare(mStackScrollP, newScroll) != 0) {
219            mFinalAnimatedScroll = newScroll;
220            mScrollAnimator = ObjectAnimator.ofFloat(this, STACK_SCROLL, getStackScroll(), newScroll);
221            mScrollAnimator.setDuration(duration);
222            mScrollAnimator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
223            mScrollAnimator.addListener(new AnimatorListenerAdapter() {
224                @Override
225                public void onAnimationEnd(Animator animation) {
226                    if (postRunnable != null) {
227                        postRunnable.run();
228                    }
229                    mScrollAnimator.removeAllListeners();
230                }
231            });
232            mScrollAnimator.start();
233        } else {
234            if (postRunnable != null) {
235                postRunnable.run();
236            }
237        }
238    }
239
240    /** Aborts any current stack scrolls */
241    void stopBoundScrollAnimation() {
242        Utilities.cancelAnimationWithoutCallbacks(mScrollAnimator);
243    }
244
245    /**** OverScroller ****/
246
247    /** Called from the view draw, computes the next scroll. */
248    boolean computeScroll() {
249        if (mScroller.computeScrollOffset()) {
250            float deltaP = mLayoutAlgorithm.getDeltaPForY(mFlingDownY, mScroller.getCurrY());
251            mFlingDownScrollP += setDeltaStackScroll(mFlingDownScrollP, deltaP);
252            if (DEBUG) {
253                Log.d(TAG, "computeScroll: " + (mFlingDownScrollP + deltaP));
254            }
255            return true;
256        }
257        return false;
258    }
259
260    /** Returns whether the overscroller is scrolling. */
261    boolean isScrolling() {
262        return !mScroller.isFinished();
263    }
264
265    float getScrollVelocity() {
266        return mScroller.getCurrVelocity();
267    }
268
269    /** Stops the scroller and any current fling. */
270    void stopScroller() {
271        if (!mScroller.isFinished()) {
272            mScroller.abortAnimation();
273        }
274    }
275
276    public void dump(String prefix, PrintWriter writer) {
277        writer.print(prefix); writer.print(TAG);
278        writer.print(" stackScroll:"); writer.print(mStackScrollP);
279        writer.println();
280    }
281}