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.Recents; 35 import com.android.systemui.shared.recents.utilities.AnimationProps; 36 import com.android.systemui.shared.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 { 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 87 public TaskStackViewScroller(Context context, TaskStackViewScrollerCallbacks cb, 88 TaskStackLayoutAlgorithm layoutAlgorithm) { 89 mContext = context; 90 mCb = cb; 91 mScroller = new OverScroller(context); 92 if (Recents.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. */ 100 void reset() { 101 mStackScrollP = 0f; 102 mLastDeltaP = 0f; 103 } 104 105 void resetDeltaScroll() { 106 mLastDeltaP = 0f; 107 } 108 109 /** Gets the current stack scroll */ 110 public float getStackScroll() { 111 return mStackScrollP; 112 } 113 114 /** 115 * Sets the current stack scroll immediately. 116 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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. */ 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 */ 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 */ 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 (!Recents.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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 307 void stopBoundScrollAnimation() { 308 Utilities.cancelAnimationWithoutCallbacks(mScrollAnimator); 309 } 310 311 /**** OverScroller ****/ 312 313 /** Called from the view draw, computes the next scroll. */ 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. */ 327 boolean isScrolling() { 328 return !mScroller.isFinished(); 329 } 330 331 float getScrollVelocity() { 332 return mScroller.getCurrVelocity(); 333 } 334 335 /** Stops the scroller and any current fling. */ 336 void stopScroller() { 337 if (!mScroller.isFinished()) { 338 mScroller.abortAnimation(); 339 } 340 } 341 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 }