Home | History | Annotate | Download | only in allapps
      1 package com.android.launcher3.allapps;
      2 
      3 import android.animation.Animator;
      4 import android.animation.AnimatorInflater;
      5 import android.animation.AnimatorListenerAdapter;
      6 import android.animation.AnimatorSet;
      7 import android.animation.ArgbEvaluator;
      8 import android.animation.ObjectAnimator;
      9 import android.graphics.Color;
     10 import android.support.animation.SpringAnimation;
     11 import android.support.v4.graphics.ColorUtils;
     12 import android.support.v4.view.animation.FastOutSlowInInterpolator;
     13 import android.view.MotionEvent;
     14 import android.view.View;
     15 import android.view.animation.AccelerateInterpolator;
     16 import android.view.animation.DecelerateInterpolator;
     17 import android.view.animation.Interpolator;
     18 
     19 import com.android.launcher3.AbstractFloatingView;
     20 import com.android.launcher3.Hotseat;
     21 import com.android.launcher3.Launcher;
     22 import com.android.launcher3.LauncherAnimUtils;
     23 import com.android.launcher3.R;
     24 import com.android.launcher3.Utilities;
     25 import com.android.launcher3.Workspace;
     26 import com.android.launcher3.anim.SpringAnimationHandler;
     27 import com.android.launcher3.config.FeatureFlags;
     28 import com.android.launcher3.graphics.GradientView;
     29 import com.android.launcher3.touch.SwipeDetector;
     30 import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
     31 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
     32 import com.android.launcher3.util.SystemUiController;
     33 import com.android.launcher3.util.Themes;
     34 import com.android.launcher3.util.TouchController;
     35 
     36 /**
     37  * Handles AllApps view transition.
     38  * 1) Slides all apps view using direct manipulation
     39  * 2) When finger is released, animate to either top or bottom accordingly.
     40  * <p/>
     41  * Algorithm:
     42  * If release velocity > THRES1, snap according to the direction of movement.
     43  * If release velocity < THRES1, snap according to either top or bottom depending on whether it's
     44  * closer to top or closer to the page indicator.
     45  */
     46 public class AllAppsTransitionController implements TouchController, SwipeDetector.Listener,
     47          SearchUiManager.OnScrollRangeChangeListener {
     48 
     49     private static final String TAG = "AllAppsTrans";
     50     private static final boolean DBG = false;
     51 
     52     private final Interpolator mWorkspaceAccelnterpolator = new AccelerateInterpolator(2f);
     53     private final Interpolator mHotseatAccelInterpolator = new AccelerateInterpolator(1.5f);
     54     private final Interpolator mDecelInterpolator = new DecelerateInterpolator(3f);
     55     private final Interpolator mFastOutSlowInInterpolator = new FastOutSlowInInterpolator();
     56     private final SwipeDetector.ScrollInterpolator mScrollInterpolator
     57             = new SwipeDetector.ScrollInterpolator();
     58 
     59     private static final float PARALLAX_COEFFICIENT = .125f;
     60     private static final int SINGLE_FRAME_MS = 16;
     61 
     62     private AllAppsContainerView mAppsView;
     63     private int mAllAppsBackgroundColor;
     64     private Workspace mWorkspace;
     65     private Hotseat mHotseat;
     66     private int mHotseatBackgroundColor;
     67 
     68     private AllAppsCaretController mCaretController;
     69 
     70     private float mStatusBarHeight;
     71 
     72     private final Launcher mLauncher;
     73     private final SwipeDetector mDetector;
     74     private final ArgbEvaluator mEvaluator;
     75     private final boolean mIsDarkTheme;
     76 
     77     // Animation in this class is controlled by a single variable {@link mProgress}.
     78     // Visually, it represents top y coordinate of the all apps container if multiplied with
     79     // {@link mShiftRange}.
     80 
     81     // When {@link mProgress} is 0, all apps container is pulled up.
     82     // When {@link mProgress} is 1, all apps container is pulled down.
     83     private float mShiftStart;      // [0, mShiftRange]
     84     private float mShiftRange;      // changes depending on the orientation
     85     private float mProgress;        // [0, 1], mShiftRange * mProgress = shiftCurrent
     86 
     87     // Velocity of the container. Unit is in px/ms.
     88     private float mContainerVelocity;
     89 
     90     private static final float DEFAULT_SHIFT_RANGE = 10;
     91 
     92     private static final float RECATCH_REJECTION_FRACTION = .0875f;
     93 
     94     private long mAnimationDuration;
     95 
     96     private AnimatorSet mCurrentAnimation;
     97     private boolean mNoIntercept;
     98     private boolean mTouchEventStartedOnHotseat;
     99 
    100     // Used in discovery bounce animation to provide the transition without workspace changing.
    101     private boolean mIsTranslateWithoutWorkspace = false;
    102     private Animator mDiscoBounceAnimation;
    103     private GradientView mGradientView;
    104 
    105     private SpringAnimation mSearchSpring;
    106     private SpringAnimationHandler mSpringAnimationHandler;
    107 
    108     public AllAppsTransitionController(Launcher l) {
    109         mLauncher = l;
    110         mDetector = new SwipeDetector(l, this, SwipeDetector.VERTICAL);
    111         mShiftRange = DEFAULT_SHIFT_RANGE;
    112         mProgress = 1f;
    113 
    114         mEvaluator = new ArgbEvaluator();
    115         mAllAppsBackgroundColor = Themes.getAttrColor(l, android.R.attr.colorPrimary);
    116         mIsDarkTheme = Themes.getAttrBoolean(mLauncher, R.attr.isMainColorDark);
    117     }
    118 
    119     @Override
    120     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
    121         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
    122             mNoIntercept = false;
    123             mTouchEventStartedOnHotseat = mLauncher.getDragLayer().isEventOverHotseat(ev);
    124             if (!mLauncher.isAllAppsVisible() && mLauncher.getWorkspace().workspaceInModalState()) {
    125                 mNoIntercept = true;
    126             } else if (mLauncher.isAllAppsVisible() &&
    127                     !mAppsView.shouldContainerScroll(ev)) {
    128                 mNoIntercept = true;
    129             } else if (AbstractFloatingView.getTopOpenView(mLauncher) != null) {
    130                 mNoIntercept = true;
    131             } else {
    132                 // Now figure out which direction scroll events the controller will start
    133                 // calling the callbacks.
    134                 int directionsToDetectScroll = 0;
    135                 boolean ignoreSlopWhenSettling = false;
    136 
    137                 if (mDetector.isIdleState()) {
    138                     if (mLauncher.isAllAppsVisible()) {
    139                         directionsToDetectScroll |= SwipeDetector.DIRECTION_NEGATIVE;
    140                     } else {
    141                         directionsToDetectScroll |= SwipeDetector.DIRECTION_POSITIVE;
    142                     }
    143                 } else {
    144                     if (isInDisallowRecatchBottomZone()) {
    145                         directionsToDetectScroll |= SwipeDetector.DIRECTION_POSITIVE;
    146                     } else if (isInDisallowRecatchTopZone()) {
    147                         directionsToDetectScroll |= SwipeDetector.DIRECTION_NEGATIVE;
    148                     } else {
    149                         directionsToDetectScroll |= SwipeDetector.DIRECTION_BOTH;
    150                         ignoreSlopWhenSettling = true;
    151                     }
    152                 }
    153                 mDetector.setDetectableScrollConditions(directionsToDetectScroll,
    154                         ignoreSlopWhenSettling);
    155             }
    156         }
    157 
    158         if (mNoIntercept) {
    159             return false;
    160         }
    161         mDetector.onTouchEvent(ev);
    162         if (mDetector.isSettlingState() && (isInDisallowRecatchBottomZone() || isInDisallowRecatchTopZone())) {
    163             return false;
    164         }
    165         return mDetector.isDraggingOrSettling();
    166     }
    167 
    168     @Override
    169     public boolean onControllerTouchEvent(MotionEvent ev) {
    170         if (hasSpringAnimationHandler()) {
    171             mSpringAnimationHandler.addMovement(ev);
    172         }
    173         return mDetector.onTouchEvent(ev);
    174     }
    175 
    176     private boolean isInDisallowRecatchTopZone() {
    177         return mProgress < RECATCH_REJECTION_FRACTION;
    178     }
    179 
    180     private boolean isInDisallowRecatchBottomZone() {
    181         return mProgress > 1 - RECATCH_REJECTION_FRACTION;
    182     }
    183 
    184     @Override
    185     public void onDragStart(boolean start) {
    186         mCaretController.onDragStart();
    187         cancelAnimation();
    188         mCurrentAnimation = LauncherAnimUtils.createAnimatorSet();
    189         mShiftStart = mAppsView.getTranslationY();
    190         preparePull(start);
    191         if (hasSpringAnimationHandler()) {
    192             mSpringAnimationHandler.skipToEnd();
    193         }
    194     }
    195 
    196     @Override
    197     public boolean onDrag(float displacement, float velocity) {
    198         if (mAppsView == null) {
    199             return false;   // early termination.
    200         }
    201 
    202         mContainerVelocity = velocity;
    203 
    204         float shift = Math.min(Math.max(0, mShiftStart + displacement), mShiftRange);
    205         setProgress(shift / mShiftRange);
    206 
    207         return true;
    208     }
    209 
    210     @Override
    211     public void onDragEnd(float velocity, boolean fling) {
    212         if (mAppsView == null) {
    213             return; // early termination.
    214         }
    215 
    216         final int containerType = mTouchEventStartedOnHotseat
    217                 ? ContainerType.HOTSEAT : ContainerType.WORKSPACE;
    218 
    219         if (fling) {
    220             if (velocity < 0) {
    221                 calculateDuration(velocity, mAppsView.getTranslationY());
    222 
    223                 if (!mLauncher.isAllAppsVisible()) {
    224                     mLauncher.getUserEventDispatcher().logActionOnContainer(
    225                             Action.Touch.FLING,
    226                             Action.Direction.UP,
    227                             containerType);
    228                 }
    229                 mLauncher.showAppsView(true /* animated */, false /* updatePredictedApps */);
    230                 if (hasSpringAnimationHandler()) {
    231                     mSpringAnimationHandler.add(mSearchSpring, true /* setDefaultValues */);
    232                     // The icons are moving upwards, so we go to 0 from 1. (y-axis 1 is below 0.)
    233                     mSpringAnimationHandler.animateToFinalPosition(0 /* pos */, 1 /* startValue */);
    234                 }
    235             } else {
    236                 calculateDuration(velocity, Math.abs(mShiftRange - mAppsView.getTranslationY()));
    237                 mLauncher.showWorkspace(true);
    238             }
    239             // snap to top or bottom using the release velocity
    240         } else {
    241             if (mAppsView.getTranslationY() > mShiftRange / 2) {
    242                 calculateDuration(velocity, Math.abs(mShiftRange - mAppsView.getTranslationY()));
    243                 mLauncher.showWorkspace(true);
    244             } else {
    245                 calculateDuration(velocity, Math.abs(mAppsView.getTranslationY()));
    246                 if (!mLauncher.isAllAppsVisible()) {
    247                     mLauncher.getUserEventDispatcher().logActionOnContainer(
    248                             Action.Touch.SWIPE,
    249                             Action.Direction.UP,
    250                             containerType);
    251                 }
    252                 mLauncher.showAppsView(true, /* animated */ false /* updatePredictedApps */);
    253             }
    254         }
    255     }
    256 
    257     public boolean isTransitioning() {
    258         return mDetector.isDraggingOrSettling();
    259     }
    260 
    261     /**
    262      * @param start {@code true} if start of new drag.
    263      */
    264     public void preparePull(boolean start) {
    265         if (start) {
    266             // Initialize values that should not change until #onDragEnd
    267             mStatusBarHeight = mLauncher.getDragLayer().getInsets().top;
    268             mHotseat.setVisibility(View.VISIBLE);
    269             mHotseatBackgroundColor = mHotseat.getBackgroundDrawableColor();
    270             mHotseat.setBackgroundTransparent(true /* transparent */);
    271             if (!mLauncher.isAllAppsVisible()) {
    272                 mLauncher.tryAndUpdatePredictedApps();
    273                 mAppsView.setVisibility(View.VISIBLE);
    274                 if (!FeatureFlags.LAUNCHER3_GRADIENT_ALL_APPS) {
    275                     mAppsView.setRevealDrawableColor(mHotseatBackgroundColor);
    276                 }
    277             }
    278         }
    279     }
    280 
    281     private void updateLightStatusBar(float shift) {
    282         // Do not modify status bar on landscape as all apps is not full bleed.
    283         if (!FeatureFlags.LAUNCHER3_GRADIENT_ALL_APPS
    284                 && mLauncher.getDeviceProfile().isVerticalBarLayout()) {
    285             return;
    286         }
    287 
    288         // Use a light system UI (dark icons) if all apps is behind at least half of the status bar.
    289         boolean forceChange = FeatureFlags.LAUNCHER3_GRADIENT_ALL_APPS ?
    290                 shift <= mShiftRange / 4 :
    291                 shift <= mStatusBarHeight / 2;
    292         if (forceChange) {
    293             mLauncher.getSystemUiController().updateUiState(
    294                     SystemUiController.UI_STATE_ALL_APPS, !mIsDarkTheme);
    295         } else {
    296             mLauncher.getSystemUiController().updateUiState(
    297                     SystemUiController.UI_STATE_ALL_APPS, 0);
    298         }
    299     }
    300 
    301     private void updateAllAppsBg(float progress) {
    302         // gradient
    303         if (mGradientView == null) {
    304             mGradientView = (GradientView) mLauncher.findViewById(R.id.gradient_bg);
    305             mGradientView.setVisibility(View.VISIBLE);
    306         }
    307         mGradientView.setProgress(progress);
    308     }
    309 
    310     /**
    311      * @param progress       value between 0 and 1, 0 shows all apps and 1 shows workspace
    312      */
    313     public void setProgress(float progress) {
    314         float shiftPrevious = mProgress * mShiftRange;
    315         mProgress = progress;
    316         float shiftCurrent = progress * mShiftRange;
    317 
    318         float workspaceHotseatAlpha = Utilities.boundToRange(progress, 0f, 1f);
    319         float alpha = 1 - workspaceHotseatAlpha;
    320         float workspaceAlpha = mWorkspaceAccelnterpolator.getInterpolation(workspaceHotseatAlpha);
    321         float hotseatAlpha = mHotseatAccelInterpolator.getInterpolation(workspaceHotseatAlpha);
    322 
    323         int color = (Integer) mEvaluator.evaluate(mDecelInterpolator.getInterpolation(alpha),
    324                 mHotseatBackgroundColor, mAllAppsBackgroundColor);
    325         int bgAlpha = Color.alpha((int) mEvaluator.evaluate(alpha,
    326                 mHotseatBackgroundColor, mAllAppsBackgroundColor));
    327 
    328         if (FeatureFlags.LAUNCHER3_GRADIENT_ALL_APPS) {
    329             updateAllAppsBg(alpha);
    330         } else {
    331             mAppsView.setRevealDrawableColor(ColorUtils.setAlphaComponent(color, bgAlpha));
    332         }
    333 
    334         mAppsView.getContentView().setAlpha(alpha);
    335         mAppsView.setTranslationY(shiftCurrent);
    336 
    337         if (!mLauncher.getDeviceProfile().isVerticalBarLayout()) {
    338             mWorkspace.setHotseatTranslationAndAlpha(Workspace.Direction.Y, -mShiftRange + shiftCurrent,
    339                     hotseatAlpha);
    340         } else {
    341             mWorkspace.setHotseatTranslationAndAlpha(Workspace.Direction.Y,
    342                     PARALLAX_COEFFICIENT * (-mShiftRange + shiftCurrent),
    343                     hotseatAlpha);
    344         }
    345 
    346         if (mIsTranslateWithoutWorkspace) {
    347             return;
    348         }
    349         mWorkspace.setWorkspaceYTranslationAndAlpha(
    350                 PARALLAX_COEFFICIENT * (-mShiftRange + shiftCurrent), workspaceAlpha);
    351 
    352         if (!mDetector.isDraggingState()) {
    353             mContainerVelocity = mDetector.computeVelocity(shiftCurrent - shiftPrevious,
    354                     System.currentTimeMillis());
    355         }
    356 
    357         mCaretController.updateCaret(progress, mContainerVelocity, mDetector.isDraggingState());
    358         updateLightStatusBar(shiftCurrent);
    359     }
    360 
    361     public float getProgress() {
    362         return mProgress;
    363     }
    364 
    365     private void calculateDuration(float velocity, float disp) {
    366         mAnimationDuration = SwipeDetector.calculateDuration(velocity, disp / mShiftRange);
    367     }
    368 
    369     public boolean animateToAllApps(AnimatorSet animationOut, long duration) {
    370         boolean shouldPost = true;
    371         if (animationOut == null) {
    372             return shouldPost;
    373         }
    374         Interpolator interpolator;
    375         if (mDetector.isIdleState()) {
    376             preparePull(true);
    377             mAnimationDuration = duration;
    378             mShiftStart = mAppsView.getTranslationY();
    379             interpolator = mFastOutSlowInInterpolator;
    380         } else {
    381             mScrollInterpolator.setVelocityAtZero(Math.abs(mContainerVelocity));
    382             interpolator = mScrollInterpolator;
    383             float nextFrameProgress = mProgress + mContainerVelocity * SINGLE_FRAME_MS / mShiftRange;
    384             if (nextFrameProgress >= 0f) {
    385                 mProgress = nextFrameProgress;
    386             }
    387             shouldPost = false;
    388         }
    389 
    390         ObjectAnimator driftAndAlpha = ObjectAnimator.ofFloat(this, "progress",
    391                 mProgress, 0f);
    392         driftAndAlpha.setDuration(mAnimationDuration);
    393         driftAndAlpha.setInterpolator(interpolator);
    394         animationOut.play(driftAndAlpha);
    395 
    396         animationOut.addListener(new AnimatorListenerAdapter() {
    397             boolean canceled = false;
    398 
    399             @Override
    400             public void onAnimationCancel(Animator animation) {
    401                 canceled = true;
    402             }
    403 
    404             @Override
    405             public void onAnimationEnd(Animator animation) {
    406                 if (canceled) {
    407                     return;
    408                 } else {
    409                     finishPullUp();
    410                     cleanUpAnimation();
    411                     mDetector.finishedScrolling();
    412                 }
    413             }
    414         });
    415         mCurrentAnimation = animationOut;
    416         return shouldPost;
    417     }
    418 
    419     public void showDiscoveryBounce() {
    420         // cancel existing animation in case user locked and unlocked at a super human speed.
    421         cancelDiscoveryAnimation();
    422 
    423         // assumption is that this variable is always null
    424         mDiscoBounceAnimation = AnimatorInflater.loadAnimator(mLauncher,
    425                 R.animator.discovery_bounce);
    426         mDiscoBounceAnimation.addListener(new AnimatorListenerAdapter() {
    427             @Override
    428             public void onAnimationStart(Animator animator) {
    429                 mIsTranslateWithoutWorkspace = true;
    430                 preparePull(true);
    431             }
    432 
    433             @Override
    434             public void onAnimationEnd(Animator animator) {
    435                 finishPullDown();
    436                 mDiscoBounceAnimation = null;
    437                 mIsTranslateWithoutWorkspace = false;
    438             }
    439         });
    440         mDiscoBounceAnimation.setTarget(this);
    441         mAppsView.post(new Runnable() {
    442             @Override
    443             public void run() {
    444                 if (mDiscoBounceAnimation == null) {
    445                     return;
    446                 }
    447                 mDiscoBounceAnimation.start();
    448             }
    449         });
    450     }
    451 
    452     public boolean animateToWorkspace(AnimatorSet animationOut, long duration) {
    453         boolean shouldPost = true;
    454         if (animationOut == null) {
    455             return shouldPost;
    456         }
    457         Interpolator interpolator;
    458         if (mDetector.isIdleState()) {
    459             preparePull(true);
    460             mAnimationDuration = duration;
    461             mShiftStart = mAppsView.getTranslationY();
    462             interpolator = mFastOutSlowInInterpolator;
    463         } else {
    464             mScrollInterpolator.setVelocityAtZero(Math.abs(mContainerVelocity));
    465             interpolator = mScrollInterpolator;
    466             float nextFrameProgress = mProgress + mContainerVelocity * SINGLE_FRAME_MS / mShiftRange;
    467             if (nextFrameProgress <= 1f) {
    468                 mProgress = nextFrameProgress;
    469             }
    470             shouldPost = false;
    471         }
    472 
    473         ObjectAnimator driftAndAlpha = ObjectAnimator.ofFloat(this, "progress",
    474                 mProgress, 1f);
    475         driftAndAlpha.setDuration(mAnimationDuration);
    476         driftAndAlpha.setInterpolator(interpolator);
    477         animationOut.play(driftAndAlpha);
    478 
    479         animationOut.addListener(new AnimatorListenerAdapter() {
    480             boolean canceled = false;
    481 
    482             @Override
    483             public void onAnimationCancel(Animator animation) {
    484                 canceled = true;
    485             }
    486 
    487             @Override
    488             public void onAnimationEnd(Animator animation) {
    489                 if (canceled) {
    490                     return;
    491                 } else {
    492                     finishPullDown();
    493                     cleanUpAnimation();
    494                     mDetector.finishedScrolling();
    495                 }
    496             }
    497         });
    498         mCurrentAnimation = animationOut;
    499         return shouldPost;
    500     }
    501 
    502     public void finishPullUp() {
    503         mHotseat.setVisibility(View.INVISIBLE);
    504         if (hasSpringAnimationHandler()) {
    505             mSpringAnimationHandler.remove(mSearchSpring);
    506             mSpringAnimationHandler.reset();
    507         }
    508         setProgress(0f);
    509     }
    510 
    511     public void finishPullDown() {
    512         mAppsView.setVisibility(View.INVISIBLE);
    513         mHotseat.setBackgroundTransparent(false /* transparent */);
    514         mHotseat.setVisibility(View.VISIBLE);
    515         mAppsView.reset();
    516         if (hasSpringAnimationHandler()) {
    517             mSpringAnimationHandler.reset();
    518         }
    519         setProgress(1f);
    520     }
    521 
    522     private void cancelAnimation() {
    523         if (mCurrentAnimation != null) {
    524             mCurrentAnimation.cancel();
    525             mCurrentAnimation = null;
    526         }
    527         cancelDiscoveryAnimation();
    528     }
    529 
    530     public void cancelDiscoveryAnimation() {
    531         if (mDiscoBounceAnimation == null) {
    532             return;
    533         }
    534         mDiscoBounceAnimation.cancel();
    535         mDiscoBounceAnimation = null;
    536     }
    537 
    538     private void cleanUpAnimation() {
    539         mCurrentAnimation = null;
    540     }
    541 
    542     public void setupViews(AllAppsContainerView appsView, Hotseat hotseat, Workspace workspace) {
    543         mAppsView = appsView;
    544         mHotseat = hotseat;
    545         mWorkspace = workspace;
    546         mHotseat.bringToFront();
    547         mCaretController = new AllAppsCaretController(
    548                 mWorkspace.getPageIndicator().getCaretDrawable(), mLauncher);
    549         mAppsView.getSearchUiManager().addOnScrollRangeChangeListener(this);
    550         mSpringAnimationHandler = mAppsView.getSpringAnimationHandler();
    551         mSearchSpring = mAppsView.getSearchUiManager().getSpringForFling();
    552     }
    553 
    554     private boolean hasSpringAnimationHandler() {
    555         return FeatureFlags.LAUNCHER3_PHYSICS && mSpringAnimationHandler != null;
    556     }
    557 
    558     @Override
    559     public void onScrollRangeChanged(int scrollRange) {
    560         mShiftRange = scrollRange;
    561         setProgress(mProgress);
    562     }
    563 }
    564