/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.recents.views; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.content.Context; import android.util.FloatProperty; import android.util.Log; import android.util.MutableFloat; import android.util.Property; import android.view.ViewConfiguration; import android.view.ViewDebug; import android.widget.OverScroller; import com.android.systemui.Interpolators; import com.android.systemui.R; import com.android.systemui.recents.Recents; import com.android.systemui.recents.misc.Utilities; import com.android.systemui.recents.views.lowram.TaskStackLowRamLayoutAlgorithm; import com.android.systemui.statusbar.FlingAnimationUtils; import java.io.PrintWriter; /* The scrolling logic for a TaskStackView */ public class TaskStackViewScroller { private static final String TAG = "TaskStackViewScroller"; private static final boolean DEBUG = false; public interface TaskStackViewScrollerCallbacks { void onStackScrollChanged(float prevScroll, float curScroll, AnimationProps animation); } /** * A Property wrapper around the stackScroll functionality handled by the * {@link #setStackScroll(float)} and * {@link #getStackScroll()} methods. */ private static final Property STACK_SCROLL = new FloatProperty("stackScroll") { @Override public void setValue(TaskStackViewScroller object, float value) { object.setStackScroll(value); } @Override public Float get(TaskStackViewScroller object) { return object.getStackScroll(); } }; Context mContext; TaskStackLayoutAlgorithm mLayoutAlgorithm; TaskStackViewScrollerCallbacks mCb; @ViewDebug.ExportedProperty(category="recents") float mStackScrollP; @ViewDebug.ExportedProperty(category="recents") float mLastDeltaP = 0f; float mFlingDownScrollP; int mFlingDownY; OverScroller mScroller; ObjectAnimator mScrollAnimator; float mFinalAnimatedScroll; final FlingAnimationUtils mFlingAnimationUtils; public TaskStackViewScroller(Context context, TaskStackViewScrollerCallbacks cb, TaskStackLayoutAlgorithm layoutAlgorithm) { mContext = context; mCb = cb; mScroller = new OverScroller(context); if (Recents.getConfiguration().isLowRamDevice) { mScroller.setFriction(0.06f); } mLayoutAlgorithm = layoutAlgorithm; mFlingAnimationUtils = new FlingAnimationUtils(context, 0.3f); } /** Resets the task scroller. */ void reset() { mStackScrollP = 0f; mLastDeltaP = 0f; } void resetDeltaScroll() { mLastDeltaP = 0f; } /** Gets the current stack scroll */ public float getStackScroll() { return mStackScrollP; } /** * Sets the current stack scroll immediately. */ public void setStackScroll(float s) { setStackScroll(s, AnimationProps.IMMEDIATE); } /** * Sets the current stack scroll immediately, and returns the difference between the target * scroll and the actual scroll after accounting for the effect on the focus state. */ public float setDeltaStackScroll(float downP, float deltaP) { float targetScroll = downP + deltaP; float newScroll = mLayoutAlgorithm.updateFocusStateOnScroll(downP + mLastDeltaP, targetScroll, mStackScrollP); setStackScroll(newScroll, AnimationProps.IMMEDIATE); mLastDeltaP = deltaP; return newScroll - targetScroll; } /** * Sets the current stack scroll, but indicates to the callback the preferred animation to * update to this new scroll. */ public void setStackScroll(float newScroll, AnimationProps animation) { float prevScroll = mStackScrollP; mStackScrollP = newScroll; if (mCb != null) { mCb.onStackScrollChanged(prevScroll, mStackScrollP, animation); } } /** * Sets the current stack scroll to the initial state when you first enter recents. * @return whether the stack progress changed. */ public boolean setStackScrollToInitialState() { float prevScroll = mStackScrollP; setStackScroll(mLayoutAlgorithm.mInitialScrollP); return Float.compare(prevScroll, mStackScrollP) != 0; } /** * Starts a fling that is coordinated with the {@link TaskStackViewTouchHandler}. */ public void fling(float downScrollP, int downY, int y, int velY, int minY, int maxY, int overscroll) { if (DEBUG) { Log.d(TAG, "fling: " + downScrollP + ", downY: " + downY + ", y: " + y + ", velY: " + velY + ", minY: " + minY + ", maxY: " + maxY); } mFlingDownScrollP = downScrollP; mFlingDownY = downY; mScroller.fling(0, y, 0, velY, 0, 0, minY, maxY, 0, overscroll); } /** Bounds the current scroll if necessary */ public boolean boundScroll() { float curScroll = getStackScroll(); float newScroll = getBoundedStackScroll(curScroll); if (Float.compare(newScroll, curScroll) != 0) { setStackScroll(newScroll); return true; } return false; } /** Returns the bounded stack scroll */ float getBoundedStackScroll(float scroll) { return Utilities.clamp(scroll, mLayoutAlgorithm.mMinScrollP, mLayoutAlgorithm.mMaxScrollP); } /** Returns the amount that the absolute value of how much the scroll is out of bounds. */ float getScrollAmountOutOfBounds(float scroll) { if (scroll < mLayoutAlgorithm.mMinScrollP) { return Math.abs(scroll - mLayoutAlgorithm.mMinScrollP); } else if (scroll > mLayoutAlgorithm.mMaxScrollP) { return Math.abs(scroll - mLayoutAlgorithm.mMaxScrollP); } return 0f; } /** Returns whether the specified scroll is out of bounds */ boolean isScrollOutOfBounds() { return Float.compare(getScrollAmountOutOfBounds(mStackScrollP), 0f) != 0; } /** * Scrolls the closest task and snaps into place. Only used in recents for low ram devices. * @param velocity of scroll */ void scrollToClosestTask(int velocity) { float stackScroll = getStackScroll(); // Skip if not in low ram layout and if the scroll is out of min and max bounds if (!Recents.getConfiguration().isLowRamDevice || stackScroll < mLayoutAlgorithm.mMinScrollP || stackScroll > mLayoutAlgorithm.mMaxScrollP) { return; } TaskStackLowRamLayoutAlgorithm algorithm = mLayoutAlgorithm.mTaskStackLowRamLayoutAlgorithm; float flingThreshold = ViewConfiguration.get(mContext).getScaledMinimumFlingVelocity(); if (Math.abs(velocity) > flingThreshold) { int minY = algorithm.percentageToScroll(mLayoutAlgorithm.mMinScrollP); int maxY = algorithm.percentageToScroll(mLayoutAlgorithm.mMaxScrollP); // Calculate the fling and snap to closest task from final y position, computeScroll() // never runs when cancelled with animateScroll() and the overscroll is not calculated // here fling(0 /* downScrollP */, 0 /* downY */, algorithm.percentageToScroll(stackScroll), -velocity, minY, maxY, 0 /* overscroll */); float pos = algorithm.scrollToPercentage(mScroller.getFinalY()); float newScrollP = algorithm.getClosestTaskP(pos, mLayoutAlgorithm.mNumStackTasks, velocity); ValueAnimator animator = ObjectAnimator.ofFloat(stackScroll, newScrollP); mFlingAnimationUtils.apply(animator, algorithm.percentageToScroll(stackScroll), algorithm.percentageToScroll(newScrollP), velocity); animateScroll(newScrollP, (int) animator.getDuration(), animator.getInterpolator(), null /* postRunnable */); } else { float newScrollP = algorithm.getClosestTaskP(stackScroll, mLayoutAlgorithm.mNumStackTasks, velocity); animateScroll(newScrollP, 300, Interpolators.ACCELERATE_DECELERATE, null /* postRunnable */); } } /** Animates the stack scroll into bounds */ ObjectAnimator animateBoundScroll() { // TODO: Take duration for snap back float curScroll = getStackScroll(); float newScroll = getBoundedStackScroll(curScroll); if (Float.compare(newScroll, curScroll) != 0) { // Start a new scroll animation animateScroll(newScroll, null /* postScrollRunnable */); } return mScrollAnimator; } /** Animates the stack scroll */ void animateScroll(float newScroll, final Runnable postRunnable) { int duration = mContext.getResources().getInteger( R.integer.recents_animate_task_stack_scroll_duration); animateScroll(newScroll, duration, postRunnable); } /** Animates the stack scroll */ void animateScroll(float newScroll, int duration, final Runnable postRunnable) { animateScroll(newScroll, duration, Interpolators.LINEAR_OUT_SLOW_IN, postRunnable); } /** Animates the stack scroll with time interpolator */ void animateScroll(float newScroll, int duration, TimeInterpolator interpolator, final Runnable postRunnable) { ObjectAnimator an = ObjectAnimator.ofFloat(this, STACK_SCROLL, getStackScroll(), newScroll); an.setDuration(duration); an.setInterpolator(interpolator); animateScroll(newScroll, an, postRunnable); } /** Animates the stack scroll with animator */ private void animateScroll(float newScroll, ObjectAnimator animator, final Runnable postRunnable) { // Finish any current scrolling animations if (mScrollAnimator != null && mScrollAnimator.isRunning()) { setStackScroll(mFinalAnimatedScroll); mScroller.forceFinished(true); } stopScroller(); stopBoundScrollAnimation(); if (Float.compare(mStackScrollP, newScroll) != 0) { mFinalAnimatedScroll = newScroll; mScrollAnimator = animator; mScrollAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { if (postRunnable != null) { postRunnable.run(); } mScrollAnimator.removeAllListeners(); } }); mScrollAnimator.start(); } else { if (postRunnable != null) { postRunnable.run(); } } } /** Aborts any current stack scrolls */ void stopBoundScrollAnimation() { Utilities.cancelAnimationWithoutCallbacks(mScrollAnimator); } /**** OverScroller ****/ /** Called from the view draw, computes the next scroll. */ boolean computeScroll() { if (mScroller.computeScrollOffset()) { float deltaP = mLayoutAlgorithm.getDeltaPForY(mFlingDownY, mScroller.getCurrY()); mFlingDownScrollP += setDeltaStackScroll(mFlingDownScrollP, deltaP); if (DEBUG) { Log.d(TAG, "computeScroll: " + (mFlingDownScrollP + deltaP)); } return true; } return false; } /** Returns whether the overscroller is scrolling. */ boolean isScrolling() { return !mScroller.isFinished(); } float getScrollVelocity() { return mScroller.getCurrVelocity(); } /** Stops the scroller and any current fling. */ void stopScroller() { if (!mScroller.isFinished()) { mScroller.abortAnimation(); } } public void dump(String prefix, PrintWriter writer) { writer.print(prefix); writer.print(TAG); writer.print(" stackScroll:"); writer.print(mStackScrollP); writer.println(); } }