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 
17 package com.android.systemui.recents.views;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.ObjectAnimator;
22 import android.animation.TimeInterpolator;
23 import android.animation.ValueAnimator;
24 import android.content.Context;
25 import android.util.FloatProperty;
26 import android.util.Log;
27 import android.util.Property;
28 import android.view.ViewConfiguration;
29 import android.view.ViewDebug;
30 import android.widget.OverScroller;
31 
32 import com.android.systemui.Interpolators;
33 import com.android.systemui.R;
34 import com.android.systemui.recents.LegacyRecentsImpl;
35 import com.android.systemui.recents.utilities.AnimationProps;
36 import com.android.systemui.recents.utilities.Utilities;
37 import com.android.systemui.recents.views.lowram.TaskStackLowRamLayoutAlgorithm;
38 import com.android.systemui.statusbar.FlingAnimationUtils;
39 
40 import java.io.PrintWriter;
41 
42 /* The scrolling logic for a TaskStackView */
43 public class TaskStackViewScroller {
44 
45     private static final String TAG = "TaskStackViewScroller";
46     private static final boolean DEBUG = false;
47 
48     public interface TaskStackViewScrollerCallbacks {
onStackScrollChanged(float prevScroll, float curScroll, AnimationProps animation)49         void onStackScrollChanged(float prevScroll, float curScroll, AnimationProps animation);
50     }
51 
52     /**
53      * A Property wrapper around the <code>stackScroll</code> functionality handled by the
54      * {@link #setStackScroll(float)} and
55      * {@link #getStackScroll()} methods.
56      */
57     private static final Property<TaskStackViewScroller, Float> STACK_SCROLL =
58             new FloatProperty<TaskStackViewScroller>("stackScroll") {
59                 @Override
60                 public void setValue(TaskStackViewScroller object, float value) {
61                     object.setStackScroll(value);
62                 }
63 
64                 @Override
65                 public Float get(TaskStackViewScroller object) {
66                     return object.getStackScroll();
67                 }
68             };
69 
70     Context mContext;
71     TaskStackLayoutAlgorithm mLayoutAlgorithm;
72     TaskStackViewScrollerCallbacks mCb;
73 
74     @ViewDebug.ExportedProperty(category="recents")
75     float mStackScrollP;
76     @ViewDebug.ExportedProperty(category="recents")
77     float mLastDeltaP = 0f;
78     float mFlingDownScrollP;
79     int mFlingDownY;
80 
81     OverScroller mScroller;
82     ObjectAnimator mScrollAnimator;
83     float mFinalAnimatedScroll;
84 
85     final FlingAnimationUtils mFlingAnimationUtils;
86 
TaskStackViewScroller(Context context, TaskStackViewScrollerCallbacks cb, TaskStackLayoutAlgorithm layoutAlgorithm)87     public TaskStackViewScroller(Context context, TaskStackViewScrollerCallbacks cb,
88             TaskStackLayoutAlgorithm layoutAlgorithm) {
89         mContext = context;
90         mCb = cb;
91         mScroller = new OverScroller(context);
92         if (LegacyRecentsImpl.getConfiguration().isLowRamDevice) {
93             mScroller.setFriction(0.06f);
94         }
95         mLayoutAlgorithm = layoutAlgorithm;
96         mFlingAnimationUtils = new FlingAnimationUtils(context, 0.3f);
97     }
98 
99     /** Resets the task scroller. */
reset()100     void reset() {
101         mStackScrollP = 0f;
102         mLastDeltaP = 0f;
103     }
104 
resetDeltaScroll()105     void resetDeltaScroll() {
106         mLastDeltaP = 0f;
107     }
108 
109     /** Gets the current stack scroll */
getStackScroll()110     public float getStackScroll() {
111         return mStackScrollP;
112     }
113 
114     /**
115      * Sets the current stack scroll immediately.
116      */
setStackScroll(float s)117     public void setStackScroll(float s) {
118         setStackScroll(s, AnimationProps.IMMEDIATE);
119     }
120 
121     /**
122      * Sets the current stack scroll immediately, and returns the difference between the target
123      * scroll and the actual scroll after accounting for the effect on the focus state.
124      */
setDeltaStackScroll(float downP, float deltaP)125     public float setDeltaStackScroll(float downP, float deltaP) {
126         float targetScroll = downP + deltaP;
127         float newScroll = mLayoutAlgorithm.updateFocusStateOnScroll(downP + mLastDeltaP, targetScroll,
128                 mStackScrollP);
129         setStackScroll(newScroll, AnimationProps.IMMEDIATE);
130         mLastDeltaP = deltaP;
131         return newScroll - targetScroll;
132     }
133 
134     /**
135      * Sets the current stack scroll, but indicates to the callback the preferred animation to
136      * update to this new scroll.
137      */
setStackScroll(float newScroll, AnimationProps animation)138     public void setStackScroll(float newScroll, AnimationProps animation) {
139         float prevScroll = mStackScrollP;
140         mStackScrollP = newScroll;
141         if (mCb != null) {
142             mCb.onStackScrollChanged(prevScroll, mStackScrollP, animation);
143         }
144     }
145 
146     /**
147      * Sets the current stack scroll to the initial state when you first enter recents.
148      * @return whether the stack progress changed.
149      */
setStackScrollToInitialState()150     public boolean setStackScrollToInitialState() {
151         float prevScroll = mStackScrollP;
152         setStackScroll(mLayoutAlgorithm.mInitialScrollP);
153         return Float.compare(prevScroll, mStackScrollP) != 0;
154     }
155 
156     /**
157      * Starts a fling that is coordinated with the {@link TaskStackViewTouchHandler}.
158      */
fling(float downScrollP, int downY, int y, int velY, int minY, int maxY, int overscroll)159     public void fling(float downScrollP, int downY, int y, int velY, int minY, int maxY,
160             int overscroll) {
161         if (DEBUG) {
162             Log.d(TAG, "fling: " + downScrollP + ", downY: " + downY + ", y: " + y +
163                     ", velY: " + velY + ", minY: " + minY + ", maxY: " + maxY);
164         }
165         mFlingDownScrollP = downScrollP;
166         mFlingDownY = downY;
167         mScroller.fling(0, y, 0, velY, 0, 0, minY, maxY, 0, overscroll);
168     }
169 
170     /** Bounds the current scroll if necessary */
boundScroll()171     public boolean boundScroll() {
172         float curScroll = getStackScroll();
173         float newScroll = getBoundedStackScroll(curScroll);
174         if (Float.compare(newScroll, curScroll) != 0) {
175             setStackScroll(newScroll);
176             return true;
177         }
178         return false;
179     }
180 
181     /** Returns the bounded stack scroll */
getBoundedStackScroll(float scroll)182     float getBoundedStackScroll(float scroll) {
183         return Utilities.clamp(scroll, mLayoutAlgorithm.mMinScrollP, mLayoutAlgorithm.mMaxScrollP);
184     }
185 
186     /** Returns the amount that the absolute value of how much the scroll is out of bounds. */
getScrollAmountOutOfBounds(float scroll)187     float getScrollAmountOutOfBounds(float scroll) {
188         if (scroll < mLayoutAlgorithm.mMinScrollP) {
189             return Math.abs(scroll - mLayoutAlgorithm.mMinScrollP);
190         } else if (scroll > mLayoutAlgorithm.mMaxScrollP) {
191             return Math.abs(scroll - mLayoutAlgorithm.mMaxScrollP);
192         }
193         return 0f;
194     }
195 
196     /** Returns whether the specified scroll is out of bounds */
isScrollOutOfBounds()197     boolean isScrollOutOfBounds() {
198         return Float.compare(getScrollAmountOutOfBounds(mStackScrollP), 0f) != 0;
199     }
200 
201     /**
202      * Scrolls the closest task and snaps into place. Only used in recents for low ram devices.
203      * @param velocity of scroll
204      */
scrollToClosestTask(int velocity)205     void scrollToClosestTask(int velocity) {
206         float stackScroll = getStackScroll();
207 
208         // Skip if not in low ram layout and if the scroll is out of min and max bounds
209         if (!LegacyRecentsImpl.getConfiguration().isLowRamDevice || stackScroll < mLayoutAlgorithm.mMinScrollP
210                 || stackScroll > mLayoutAlgorithm.mMaxScrollP) {
211             return;
212         }
213         TaskStackLowRamLayoutAlgorithm algorithm = mLayoutAlgorithm.mTaskStackLowRamLayoutAlgorithm;
214 
215         float flingThreshold = ViewConfiguration.get(mContext).getScaledMinimumFlingVelocity();
216         if (Math.abs(velocity) > flingThreshold) {
217             int minY = algorithm.percentageToScroll(mLayoutAlgorithm.mMinScrollP);
218             int maxY = algorithm.percentageToScroll(mLayoutAlgorithm.mMaxScrollP);
219 
220             // Calculate the fling and snap to closest task from final y position, computeScroll()
221             // never runs when cancelled with animateScroll() and the overscroll is not calculated
222             // here
223             fling(0 /* downScrollP */, 0 /* downY */, algorithm.percentageToScroll(stackScroll),
224                     -velocity, minY, maxY, 0 /* overscroll */);
225             float pos = algorithm.scrollToPercentage(mScroller.getFinalY());
226 
227             float newScrollP = algorithm.getClosestTaskP(pos, mLayoutAlgorithm.mNumStackTasks,
228                     velocity);
229             ValueAnimator animator = ObjectAnimator.ofFloat(stackScroll, newScrollP);
230             mFlingAnimationUtils.apply(animator, algorithm.percentageToScroll(stackScroll),
231                     algorithm.percentageToScroll(newScrollP), velocity);
232             animateScroll(newScrollP, (int) animator.getDuration(), animator.getInterpolator(),
233                     null /* postRunnable */);
234         } else {
235             float newScrollP = algorithm.getClosestTaskP(stackScroll,
236                     mLayoutAlgorithm.mNumStackTasks, velocity);
237             animateScroll(newScrollP, 300, Interpolators.ACCELERATE_DECELERATE,
238                     null /* postRunnable */);
239         }
240     }
241 
242     /** Animates the stack scroll into bounds */
animateBoundScroll()243     ObjectAnimator animateBoundScroll() {
244         // TODO: Take duration for snap back
245         float curScroll = getStackScroll();
246         float newScroll = getBoundedStackScroll(curScroll);
247         if (Float.compare(newScroll, curScroll) != 0) {
248             // Start a new scroll animation
249             animateScroll(newScroll, null /* postScrollRunnable */);
250         }
251         return mScrollAnimator;
252     }
253 
254     /** Animates the stack scroll */
animateScroll(float newScroll, final Runnable postRunnable)255     void animateScroll(float newScroll, final Runnable postRunnable) {
256         int duration = mContext.getResources().getInteger(
257                 R.integer.recents_animate_task_stack_scroll_duration);
258         animateScroll(newScroll, duration, postRunnable);
259     }
260 
261     /** Animates the stack scroll */
animateScroll(float newScroll, int duration, final Runnable postRunnable)262     void animateScroll(float newScroll, int duration, final Runnable postRunnable) {
263         animateScroll(newScroll, duration, Interpolators.LINEAR_OUT_SLOW_IN, postRunnable);
264     }
265 
266     /** Animates the stack scroll with time interpolator */
animateScroll(float newScroll, int duration, TimeInterpolator interpolator, final Runnable postRunnable)267     void animateScroll(float newScroll, int duration, TimeInterpolator interpolator,
268             final Runnable postRunnable) {
269         ObjectAnimator an = ObjectAnimator.ofFloat(this, STACK_SCROLL, getStackScroll(), newScroll);
270         an.setDuration(duration);
271         an.setInterpolator(interpolator);
272         animateScroll(newScroll, an, postRunnable);
273     }
274 
275     /** Animates the stack scroll with animator */
animateScroll(float newScroll, ObjectAnimator animator, final Runnable postRunnable)276     private void animateScroll(float newScroll, ObjectAnimator animator,
277             final Runnable postRunnable) {
278         // Finish any current scrolling animations
279         if (mScrollAnimator != null && mScrollAnimator.isRunning()) {
280             setStackScroll(mFinalAnimatedScroll);
281             mScroller.forceFinished(true);
282         }
283         stopScroller();
284         stopBoundScrollAnimation();
285 
286         if (Float.compare(mStackScrollP, newScroll) != 0) {
287             mFinalAnimatedScroll = newScroll;
288             mScrollAnimator = animator;
289             mScrollAnimator.addListener(new AnimatorListenerAdapter() {
290                 @Override
291                 public void onAnimationEnd(Animator animation) {
292                     if (postRunnable != null) {
293                         postRunnable.run();
294                     }
295                     mScrollAnimator.removeAllListeners();
296                 }
297             });
298             mScrollAnimator.start();
299         } else {
300             if (postRunnable != null) {
301                 postRunnable.run();
302             }
303         }
304     }
305 
306     /** Aborts any current stack scrolls */
stopBoundScrollAnimation()307     void stopBoundScrollAnimation() {
308         Utilities.cancelAnimationWithoutCallbacks(mScrollAnimator);
309     }
310 
311     /**** OverScroller ****/
312 
313     /** Called from the view draw, computes the next scroll. */
computeScroll()314     boolean computeScroll() {
315         if (mScroller.computeScrollOffset()) {
316             float deltaP = mLayoutAlgorithm.getDeltaPForY(mFlingDownY, mScroller.getCurrY());
317             mFlingDownScrollP += setDeltaStackScroll(mFlingDownScrollP, deltaP);
318             if (DEBUG) {
319                 Log.d(TAG, "computeScroll: " + (mFlingDownScrollP + deltaP));
320             }
321             return true;
322         }
323         return false;
324     }
325 
326     /** Returns whether the overscroller is scrolling. */
isScrolling()327     boolean isScrolling() {
328         return !mScroller.isFinished();
329     }
330 
getScrollVelocity()331     float getScrollVelocity() {
332         return mScroller.getCurrVelocity();
333     }
334 
335     /** Stops the scroller and any current fling. */
stopScroller()336     void stopScroller() {
337         if (!mScroller.isFinished()) {
338             mScroller.abortAnimation();
339         }
340     }
341 
dump(String prefix, PrintWriter writer)342     public void dump(String prefix, PrintWriter writer) {
343         writer.print(prefix); writer.print(TAG);
344         writer.print(" stackScroll:"); writer.print(mStackScrollP);
345         writer.println();
346     }
347 }