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.content.Context;
23 import android.util.FloatProperty;
24 import android.util.Log;
25 import android.util.MutableFloat;
26 import android.util.Property;
27 import android.view.ViewDebug;
28 import android.widget.OverScroller;
29 
30 import com.android.systemui.Interpolators;
31 import com.android.systemui.R;
32 import com.android.systemui.recents.misc.Utilities;
33 
34 import java.io.PrintWriter;
35 
36 /* The scrolling logic for a TaskStackView */
37 public class TaskStackViewScroller {
38 
39     private static final String TAG = "TaskStackViewScroller";
40     private static final boolean DEBUG = false;
41 
42     public interface TaskStackViewScrollerCallbacks {
onStackScrollChanged(float prevScroll, float curScroll, AnimationProps animation)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 
TaskStackViewScroller(Context context, TaskStackViewScrollerCallbacks cb, TaskStackLayoutAlgorithm layoutAlgorithm)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. */
reset()88     void reset() {
89         mStackScrollP = 0f;
90         mLastDeltaP = 0f;
91     }
92 
resetDeltaScroll()93     void resetDeltaScroll() {
94         mLastDeltaP = 0f;
95     }
96 
97     /** Gets the current stack scroll */
getStackScroll()98     public float getStackScroll() {
99         return mStackScrollP;
100     }
101 
102     /**
103      * Sets the current stack scroll immediately.
104      */
setStackScroll(float s)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      */
setDeltaStackScroll(float downP, float deltaP)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      */
setStackScroll(float newScroll, AnimationProps animation)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      */
setStackScrollToInitialState()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      */
fling(float downScrollP, int downY, int y, int velY, int minY, int maxY, int overscroll)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 */
boundScroll()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 */
getBoundedStackScroll(float 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. */
getScrollAmountOutOfBounds(float scroll)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 */
isScrollOutOfBounds()185     boolean isScrollOutOfBounds() {
186         return Float.compare(getScrollAmountOutOfBounds(mStackScrollP), 0f) != 0;
187     }
188 
189     /** Animates the stack scroll into bounds */
animateBoundScroll()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 */
animateScroll(float newScroll, final Runnable postRunnable)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 */
animateScroll(float newScroll, int duration, final Runnable postRunnable)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 */
stopBoundScrollAnimation()241     void stopBoundScrollAnimation() {
242         Utilities.cancelAnimationWithoutCallbacks(mScrollAnimator);
243     }
244 
245     /**** OverScroller ****/
246 
247     /** Called from the view draw, computes the next scroll. */
computeScroll()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. */
isScrolling()261     boolean isScrolling() {
262         return !mScroller.isFinished();
263     }
264 
265     /** Stops the scroller and any current fling. */
stopScroller()266     void stopScroller() {
267         if (!mScroller.isFinished()) {
268             mScroller.abortAnimation();
269         }
270     }
271 
dump(String prefix, PrintWriter writer)272     public void dump(String prefix, PrintWriter writer) {
273         writer.print(prefix); writer.print(TAG);
274         writer.print(" stackScroll:"); writer.print(mStackScrollP);
275         writer.println();
276     }
277 }