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