package com.android.launcher3.allapps; import android.animation.Animator; import android.animation.AnimatorInflater; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ArgbEvaluator; import android.animation.ObjectAnimator; import android.graphics.Color; import android.support.animation.SpringAnimation; import android.support.v4.graphics.ColorUtils; import android.support.v4.view.animation.FastOutSlowInInterpolator; import android.view.MotionEvent; import android.view.View; import android.view.animation.AccelerateInterpolator; import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.Hotseat; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherAnimUtils; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.Workspace; import com.android.launcher3.anim.SpringAnimationHandler; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.graphics.GradientView; import com.android.launcher3.touch.SwipeDetector; import com.android.launcher3.userevent.nano.LauncherLogProto.Action; import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; import com.android.launcher3.util.SystemUiController; import com.android.launcher3.util.Themes; import com.android.launcher3.util.TouchController; /** * Handles AllApps view transition. * 1) Slides all apps view using direct manipulation * 2) When finger is released, animate to either top or bottom accordingly. *

* Algorithm: * If release velocity > THRES1, snap according to the direction of movement. * If release velocity < THRES1, snap according to either top or bottom depending on whether it's * closer to top or closer to the page indicator. */ public class AllAppsTransitionController implements TouchController, SwipeDetector.Listener, SearchUiManager.OnScrollRangeChangeListener { private static final String TAG = "AllAppsTrans"; private static final boolean DBG = false; private final Interpolator mWorkspaceAccelnterpolator = new AccelerateInterpolator(2f); private final Interpolator mHotseatAccelInterpolator = new AccelerateInterpolator(1.5f); private final Interpolator mDecelInterpolator = new DecelerateInterpolator(3f); private final Interpolator mFastOutSlowInInterpolator = new FastOutSlowInInterpolator(); private final SwipeDetector.ScrollInterpolator mScrollInterpolator = new SwipeDetector.ScrollInterpolator(); private static final float PARALLAX_COEFFICIENT = .125f; private static final int SINGLE_FRAME_MS = 16; private AllAppsContainerView mAppsView; private int mAllAppsBackgroundColor; private Workspace mWorkspace; private Hotseat mHotseat; private int mHotseatBackgroundColor; private AllAppsCaretController mCaretController; private float mStatusBarHeight; private final Launcher mLauncher; private final SwipeDetector mDetector; private final ArgbEvaluator mEvaluator; private final boolean mIsDarkTheme; // Animation in this class is controlled by a single variable {@link mProgress}. // Visually, it represents top y coordinate of the all apps container if multiplied with // {@link mShiftRange}. // When {@link mProgress} is 0, all apps container is pulled up. // When {@link mProgress} is 1, all apps container is pulled down. private float mShiftStart; // [0, mShiftRange] private float mShiftRange; // changes depending on the orientation private float mProgress; // [0, 1], mShiftRange * mProgress = shiftCurrent // Velocity of the container. Unit is in px/ms. private float mContainerVelocity; private static final float DEFAULT_SHIFT_RANGE = 10; private static final float RECATCH_REJECTION_FRACTION = .0875f; private long mAnimationDuration; private AnimatorSet mCurrentAnimation; private boolean mNoIntercept; private boolean mTouchEventStartedOnHotseat; // Used in discovery bounce animation to provide the transition without workspace changing. private boolean mIsTranslateWithoutWorkspace = false; private Animator mDiscoBounceAnimation; private GradientView mGradientView; private SpringAnimation mSearchSpring; private SpringAnimationHandler mSpringAnimationHandler; public AllAppsTransitionController(Launcher l) { mLauncher = l; mDetector = new SwipeDetector(l, this, SwipeDetector.VERTICAL); mShiftRange = DEFAULT_SHIFT_RANGE; mProgress = 1f; mEvaluator = new ArgbEvaluator(); mAllAppsBackgroundColor = Themes.getAttrColor(l, android.R.attr.colorPrimary); mIsDarkTheme = Themes.getAttrBoolean(mLauncher, R.attr.isMainColorDark); } @Override public boolean onControllerInterceptTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { mNoIntercept = false; mTouchEventStartedOnHotseat = mLauncher.getDragLayer().isEventOverHotseat(ev); if (!mLauncher.isAllAppsVisible() && mLauncher.getWorkspace().workspaceInModalState()) { mNoIntercept = true; } else if (mLauncher.isAllAppsVisible() && !mAppsView.shouldContainerScroll(ev)) { mNoIntercept = true; } else if (AbstractFloatingView.getTopOpenView(mLauncher) != null) { mNoIntercept = true; } else { // Now figure out which direction scroll events the controller will start // calling the callbacks. int directionsToDetectScroll = 0; boolean ignoreSlopWhenSettling = false; if (mDetector.isIdleState()) { if (mLauncher.isAllAppsVisible()) { directionsToDetectScroll |= SwipeDetector.DIRECTION_NEGATIVE; } else { directionsToDetectScroll |= SwipeDetector.DIRECTION_POSITIVE; } } else { if (isInDisallowRecatchBottomZone()) { directionsToDetectScroll |= SwipeDetector.DIRECTION_POSITIVE; } else if (isInDisallowRecatchTopZone()) { directionsToDetectScroll |= SwipeDetector.DIRECTION_NEGATIVE; } else { directionsToDetectScroll |= SwipeDetector.DIRECTION_BOTH; ignoreSlopWhenSettling = true; } } mDetector.setDetectableScrollConditions(directionsToDetectScroll, ignoreSlopWhenSettling); } } if (mNoIntercept) { return false; } mDetector.onTouchEvent(ev); if (mDetector.isSettlingState() && (isInDisallowRecatchBottomZone() || isInDisallowRecatchTopZone())) { return false; } return mDetector.isDraggingOrSettling(); } @Override public boolean onControllerTouchEvent(MotionEvent ev) { if (hasSpringAnimationHandler()) { mSpringAnimationHandler.addMovement(ev); } return mDetector.onTouchEvent(ev); } private boolean isInDisallowRecatchTopZone() { return mProgress < RECATCH_REJECTION_FRACTION; } private boolean isInDisallowRecatchBottomZone() { return mProgress > 1 - RECATCH_REJECTION_FRACTION; } @Override public void onDragStart(boolean start) { mCaretController.onDragStart(); cancelAnimation(); mCurrentAnimation = LauncherAnimUtils.createAnimatorSet(); mShiftStart = mAppsView.getTranslationY(); preparePull(start); if (hasSpringAnimationHandler()) { mSpringAnimationHandler.skipToEnd(); } } @Override public boolean onDrag(float displacement, float velocity) { if (mAppsView == null) { return false; // early termination. } mContainerVelocity = velocity; float shift = Math.min(Math.max(0, mShiftStart + displacement), mShiftRange); setProgress(shift / mShiftRange); return true; } @Override public void onDragEnd(float velocity, boolean fling) { if (mAppsView == null) { return; // early termination. } final int containerType = mTouchEventStartedOnHotseat ? ContainerType.HOTSEAT : ContainerType.WORKSPACE; if (fling) { if (velocity < 0) { calculateDuration(velocity, mAppsView.getTranslationY()); if (!mLauncher.isAllAppsVisible()) { mLauncher.getUserEventDispatcher().logActionOnContainer( Action.Touch.FLING, Action.Direction.UP, containerType); } mLauncher.showAppsView(true /* animated */, false /* updatePredictedApps */); if (hasSpringAnimationHandler()) { mSpringAnimationHandler.add(mSearchSpring, true /* setDefaultValues */); // The icons are moving upwards, so we go to 0 from 1. (y-axis 1 is below 0.) mSpringAnimationHandler.animateToFinalPosition(0 /* pos */, 1 /* startValue */); } } else { calculateDuration(velocity, Math.abs(mShiftRange - mAppsView.getTranslationY())); mLauncher.showWorkspace(true); } // snap to top or bottom using the release velocity } else { if (mAppsView.getTranslationY() > mShiftRange / 2) { calculateDuration(velocity, Math.abs(mShiftRange - mAppsView.getTranslationY())); mLauncher.showWorkspace(true); } else { calculateDuration(velocity, Math.abs(mAppsView.getTranslationY())); if (!mLauncher.isAllAppsVisible()) { mLauncher.getUserEventDispatcher().logActionOnContainer( Action.Touch.SWIPE, Action.Direction.UP, containerType); } mLauncher.showAppsView(true, /* animated */ false /* updatePredictedApps */); } } } public boolean isTransitioning() { return mDetector.isDraggingOrSettling(); } /** * @param start {@code true} if start of new drag. */ public void preparePull(boolean start) { if (start) { // Initialize values that should not change until #onDragEnd mStatusBarHeight = mLauncher.getDragLayer().getInsets().top; mHotseat.setVisibility(View.VISIBLE); mHotseatBackgroundColor = mHotseat.getBackgroundDrawableColor(); mHotseat.setBackgroundTransparent(true /* transparent */); if (!mLauncher.isAllAppsVisible()) { mLauncher.tryAndUpdatePredictedApps(); mAppsView.setVisibility(View.VISIBLE); if (!FeatureFlags.LAUNCHER3_GRADIENT_ALL_APPS) { mAppsView.setRevealDrawableColor(mHotseatBackgroundColor); } } } } private void updateLightStatusBar(float shift) { // Do not modify status bar on landscape as all apps is not full bleed. if (!FeatureFlags.LAUNCHER3_GRADIENT_ALL_APPS && mLauncher.getDeviceProfile().isVerticalBarLayout()) { return; } // Use a light system UI (dark icons) if all apps is behind at least half of the status bar. boolean forceChange = FeatureFlags.LAUNCHER3_GRADIENT_ALL_APPS ? shift <= mShiftRange / 4 : shift <= mStatusBarHeight / 2; if (forceChange) { mLauncher.getSystemUiController().updateUiState( SystemUiController.UI_STATE_ALL_APPS, !mIsDarkTheme); } else { mLauncher.getSystemUiController().updateUiState( SystemUiController.UI_STATE_ALL_APPS, 0); } } private void updateAllAppsBg(float progress) { // gradient if (mGradientView == null) { mGradientView = (GradientView) mLauncher.findViewById(R.id.gradient_bg); mGradientView.setVisibility(View.VISIBLE); } mGradientView.setProgress(progress); } /** * @param progress value between 0 and 1, 0 shows all apps and 1 shows workspace */ public void setProgress(float progress) { float shiftPrevious = mProgress * mShiftRange; mProgress = progress; float shiftCurrent = progress * mShiftRange; float workspaceHotseatAlpha = Utilities.boundToRange(progress, 0f, 1f); float alpha = 1 - workspaceHotseatAlpha; float workspaceAlpha = mWorkspaceAccelnterpolator.getInterpolation(workspaceHotseatAlpha); float hotseatAlpha = mHotseatAccelInterpolator.getInterpolation(workspaceHotseatAlpha); int color = (Integer) mEvaluator.evaluate(mDecelInterpolator.getInterpolation(alpha), mHotseatBackgroundColor, mAllAppsBackgroundColor); int bgAlpha = Color.alpha((int) mEvaluator.evaluate(alpha, mHotseatBackgroundColor, mAllAppsBackgroundColor)); if (FeatureFlags.LAUNCHER3_GRADIENT_ALL_APPS) { updateAllAppsBg(alpha); } else { mAppsView.setRevealDrawableColor(ColorUtils.setAlphaComponent(color, bgAlpha)); } mAppsView.getContentView().setAlpha(alpha); mAppsView.setTranslationY(shiftCurrent); if (!mLauncher.getDeviceProfile().isVerticalBarLayout()) { mWorkspace.setHotseatTranslationAndAlpha(Workspace.Direction.Y, -mShiftRange + shiftCurrent, hotseatAlpha); } else { mWorkspace.setHotseatTranslationAndAlpha(Workspace.Direction.Y, PARALLAX_COEFFICIENT * (-mShiftRange + shiftCurrent), hotseatAlpha); } if (mIsTranslateWithoutWorkspace) { return; } mWorkspace.setWorkspaceYTranslationAndAlpha( PARALLAX_COEFFICIENT * (-mShiftRange + shiftCurrent), workspaceAlpha); if (!mDetector.isDraggingState()) { mContainerVelocity = mDetector.computeVelocity(shiftCurrent - shiftPrevious, System.currentTimeMillis()); } mCaretController.updateCaret(progress, mContainerVelocity, mDetector.isDraggingState()); updateLightStatusBar(shiftCurrent); } public float getProgress() { return mProgress; } private void calculateDuration(float velocity, float disp) { mAnimationDuration = SwipeDetector.calculateDuration(velocity, disp / mShiftRange); } public boolean animateToAllApps(AnimatorSet animationOut, long duration) { boolean shouldPost = true; if (animationOut == null) { return shouldPost; } Interpolator interpolator; if (mDetector.isIdleState()) { preparePull(true); mAnimationDuration = duration; mShiftStart = mAppsView.getTranslationY(); interpolator = mFastOutSlowInInterpolator; } else { mScrollInterpolator.setVelocityAtZero(Math.abs(mContainerVelocity)); interpolator = mScrollInterpolator; float nextFrameProgress = mProgress + mContainerVelocity * SINGLE_FRAME_MS / mShiftRange; if (nextFrameProgress >= 0f) { mProgress = nextFrameProgress; } shouldPost = false; } ObjectAnimator driftAndAlpha = ObjectAnimator.ofFloat(this, "progress", mProgress, 0f); driftAndAlpha.setDuration(mAnimationDuration); driftAndAlpha.setInterpolator(interpolator); animationOut.play(driftAndAlpha); animationOut.addListener(new AnimatorListenerAdapter() { boolean canceled = false; @Override public void onAnimationCancel(Animator animation) { canceled = true; } @Override public void onAnimationEnd(Animator animation) { if (canceled) { return; } else { finishPullUp(); cleanUpAnimation(); mDetector.finishedScrolling(); } } }); mCurrentAnimation = animationOut; return shouldPost; } public void showDiscoveryBounce() { // cancel existing animation in case user locked and unlocked at a super human speed. cancelDiscoveryAnimation(); // assumption is that this variable is always null mDiscoBounceAnimation = AnimatorInflater.loadAnimator(mLauncher, R.animator.discovery_bounce); mDiscoBounceAnimation.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animator) { mIsTranslateWithoutWorkspace = true; preparePull(true); } @Override public void onAnimationEnd(Animator animator) { finishPullDown(); mDiscoBounceAnimation = null; mIsTranslateWithoutWorkspace = false; } }); mDiscoBounceAnimation.setTarget(this); mAppsView.post(new Runnable() { @Override public void run() { if (mDiscoBounceAnimation == null) { return; } mDiscoBounceAnimation.start(); } }); } public boolean animateToWorkspace(AnimatorSet animationOut, long duration) { boolean shouldPost = true; if (animationOut == null) { return shouldPost; } Interpolator interpolator; if (mDetector.isIdleState()) { preparePull(true); mAnimationDuration = duration; mShiftStart = mAppsView.getTranslationY(); interpolator = mFastOutSlowInInterpolator; } else { mScrollInterpolator.setVelocityAtZero(Math.abs(mContainerVelocity)); interpolator = mScrollInterpolator; float nextFrameProgress = mProgress + mContainerVelocity * SINGLE_FRAME_MS / mShiftRange; if (nextFrameProgress <= 1f) { mProgress = nextFrameProgress; } shouldPost = false; } ObjectAnimator driftAndAlpha = ObjectAnimator.ofFloat(this, "progress", mProgress, 1f); driftAndAlpha.setDuration(mAnimationDuration); driftAndAlpha.setInterpolator(interpolator); animationOut.play(driftAndAlpha); animationOut.addListener(new AnimatorListenerAdapter() { boolean canceled = false; @Override public void onAnimationCancel(Animator animation) { canceled = true; } @Override public void onAnimationEnd(Animator animation) { if (canceled) { return; } else { finishPullDown(); cleanUpAnimation(); mDetector.finishedScrolling(); } } }); mCurrentAnimation = animationOut; return shouldPost; } public void finishPullUp() { mHotseat.setVisibility(View.INVISIBLE); if (hasSpringAnimationHandler()) { mSpringAnimationHandler.remove(mSearchSpring); mSpringAnimationHandler.reset(); } setProgress(0f); } public void finishPullDown() { mAppsView.setVisibility(View.INVISIBLE); mHotseat.setBackgroundTransparent(false /* transparent */); mHotseat.setVisibility(View.VISIBLE); mAppsView.reset(); if (hasSpringAnimationHandler()) { mSpringAnimationHandler.reset(); } setProgress(1f); } private void cancelAnimation() { if (mCurrentAnimation != null) { mCurrentAnimation.cancel(); mCurrentAnimation = null; } cancelDiscoveryAnimation(); } public void cancelDiscoveryAnimation() { if (mDiscoBounceAnimation == null) { return; } mDiscoBounceAnimation.cancel(); mDiscoBounceAnimation = null; } private void cleanUpAnimation() { mCurrentAnimation = null; } public void setupViews(AllAppsContainerView appsView, Hotseat hotseat, Workspace workspace) { mAppsView = appsView; mHotseat = hotseat; mWorkspace = workspace; mHotseat.bringToFront(); mCaretController = new AllAppsCaretController( mWorkspace.getPageIndicator().getCaretDrawable(), mLauncher); mAppsView.getSearchUiManager().addOnScrollRangeChangeListener(this); mSpringAnimationHandler = mAppsView.getSpringAnimationHandler(); mSearchSpring = mAppsView.getSearchUiManager().getSpringForFling(); } private boolean hasSpringAnimationHandler() { return FeatureFlags.LAUNCHER3_PHYSICS && mSpringAnimationHandler != null; } @Override public void onScrollRangeChanged(int scrollRange) { mShiftRange = scrollRange; setProgress(mProgress); } }