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