Home | History | Annotate | Download | only in phone
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License
     15  */
     16 
     17 package com.android.systemui.statusbar.phone;
     18 
     19 import android.animation.Animator;
     20 import android.animation.AnimatorListenerAdapter;
     21 import android.animation.ValueAnimator;
     22 import android.content.Context;
     23 import android.view.MotionEvent;
     24 import android.view.VelocityTracker;
     25 import android.view.View;
     26 import android.view.ViewConfiguration;
     27 import android.view.animation.AnimationUtils;
     28 import android.view.animation.Interpolator;
     29 
     30 import com.android.systemui.R;
     31 import com.android.systemui.statusbar.FlingAnimationUtils;
     32 import com.android.systemui.statusbar.KeyguardAffordanceView;
     33 
     34 /**
     35  * A touch handler of the keyguard which is responsible for launching phone and camera affordances.
     36  */
     37 public class KeyguardAffordanceHelper {
     38 
     39     public static final float SWIPE_RESTING_ALPHA_AMOUNT = 0.5f;
     40     public static final long HINT_PHASE1_DURATION = 200;
     41     private static final long HINT_PHASE2_DURATION = 350;
     42     private static final float BACKGROUND_RADIUS_SCALE_FACTOR = 0.15f;
     43     private static final int HINT_CIRCLE_OPEN_DURATION = 500;
     44 
     45     private final Context mContext;
     46 
     47     private FlingAnimationUtils mFlingAnimationUtils;
     48     private Callback mCallback;
     49     private VelocityTracker mVelocityTracker;
     50     private boolean mSwipingInProgress;
     51     private float mInitialTouchX;
     52     private float mInitialTouchY;
     53     private float mTranslation;
     54     private float mTranslationOnDown;
     55     private int mTouchSlop;
     56     private int mMinTranslationAmount;
     57     private int mMinFlingVelocity;
     58     private int mHintGrowAmount;
     59     private KeyguardAffordanceView mLeftIcon;
     60     private KeyguardAffordanceView mCenterIcon;
     61     private KeyguardAffordanceView mRightIcon;
     62     private Interpolator mAppearInterpolator;
     63     private Interpolator mDisappearInterpolator;
     64     private Animator mSwipeAnimator;
     65     private int mMinBackgroundRadius;
     66     private boolean mMotionPerformedByUser;
     67     private boolean mMotionCancelled;
     68     private AnimatorListenerAdapter mFlingEndListener = new AnimatorListenerAdapter() {
     69         @Override
     70         public void onAnimationEnd(Animator animation) {
     71             mSwipeAnimator = null;
     72             setSwipingInProgress(false);
     73         }
     74     };
     75     private Runnable mAnimationEndRunnable = new Runnable() {
     76         @Override
     77         public void run() {
     78             mCallback.onAnimationToSideEnded();
     79         }
     80     };
     81 
     82     KeyguardAffordanceHelper(Callback callback, Context context) {
     83         mContext = context;
     84         mCallback = callback;
     85         initIcons();
     86         updateIcon(mLeftIcon, 0.0f, SWIPE_RESTING_ALPHA_AMOUNT, false, false);
     87         updateIcon(mCenterIcon, 0.0f, SWIPE_RESTING_ALPHA_AMOUNT, false, false);
     88         updateIcon(mRightIcon, 0.0f, SWIPE_RESTING_ALPHA_AMOUNT, false, false);
     89         initDimens();
     90     }
     91 
     92     private void initDimens() {
     93         final ViewConfiguration configuration = ViewConfiguration.get(mContext);
     94         mTouchSlop = configuration.getScaledPagingTouchSlop();
     95         mMinFlingVelocity = configuration.getScaledMinimumFlingVelocity();
     96         mMinTranslationAmount = mContext.getResources().getDimensionPixelSize(
     97                 R.dimen.keyguard_min_swipe_amount);
     98         mMinBackgroundRadius = mContext.getResources().getDimensionPixelSize(
     99                 R.dimen.keyguard_affordance_min_background_radius);
    100         mHintGrowAmount =
    101                 mContext.getResources().getDimensionPixelSize(R.dimen.hint_grow_amount_sideways);
    102         mFlingAnimationUtils = new FlingAnimationUtils(mContext, 0.4f);
    103         mAppearInterpolator = AnimationUtils.loadInterpolator(mContext,
    104                 android.R.interpolator.linear_out_slow_in);
    105         mDisappearInterpolator = AnimationUtils.loadInterpolator(mContext,
    106                 android.R.interpolator.fast_out_linear_in);
    107     }
    108 
    109     private void initIcons() {
    110         mLeftIcon = mCallback.getLeftIcon();
    111         mLeftIcon.setIsLeft(true);
    112         mCenterIcon = mCallback.getCenterIcon();
    113         mRightIcon = mCallback.getRightIcon();
    114         mRightIcon.setIsLeft(false);
    115         mLeftIcon.setPreviewView(mCallback.getLeftPreview());
    116         mRightIcon.setPreviewView(mCallback.getRightPreview());
    117     }
    118 
    119     public boolean onTouchEvent(MotionEvent event) {
    120         if (mMotionCancelled && event.getActionMasked() != MotionEvent.ACTION_DOWN) {
    121             return false;
    122         }
    123         final float y = event.getY();
    124         final float x = event.getX();
    125 
    126         boolean isUp = false;
    127         switch (event.getActionMasked()) {
    128             case MotionEvent.ACTION_DOWN:
    129                 if (mSwipingInProgress) {
    130                     cancelAnimation();
    131                 }
    132                 mInitialTouchY = y;
    133                 mInitialTouchX = x;
    134                 mTranslationOnDown = mTranslation;
    135                 initVelocityTracker();
    136                 trackMovement(event);
    137                 mMotionPerformedByUser = false;
    138                 mMotionCancelled = false;
    139                 break;
    140             case MotionEvent.ACTION_POINTER_DOWN:
    141                 mMotionCancelled = true;
    142                 endMotion(event, true /* forceSnapBack */);
    143                 break;
    144             case MotionEvent.ACTION_MOVE:
    145                 final float w = x - mInitialTouchX;
    146                 trackMovement(event);
    147                 if (((leftSwipePossible() && w > mTouchSlop)
    148                         || (rightSwipePossible() && w < -mTouchSlop))
    149                         && Math.abs(w) > Math.abs(y - mInitialTouchY)
    150                         && !mSwipingInProgress) {
    151                     cancelAnimation();
    152                     mInitialTouchY = y;
    153                     mInitialTouchX = x;
    154                     mTranslationOnDown = mTranslation;
    155                     setSwipingInProgress(true);
    156                 }
    157                 if (mSwipingInProgress) {
    158                     setTranslation(mTranslationOnDown + x - mInitialTouchX, false, false);
    159                 }
    160                 break;
    161 
    162             case MotionEvent.ACTION_UP:
    163                 isUp = true;
    164             case MotionEvent.ACTION_CANCEL:
    165                 trackMovement(event);
    166                 endMotion(event, !isUp);
    167                 break;
    168         }
    169         return true;
    170     }
    171 
    172     private void endMotion(MotionEvent event, boolean forceSnapBack) {
    173         if (mSwipingInProgress) {
    174             flingWithCurrentVelocity(forceSnapBack);
    175         }
    176         if (mVelocityTracker != null) {
    177             mVelocityTracker.recycle();
    178             mVelocityTracker = null;
    179         }
    180     }
    181 
    182     private void setSwipingInProgress(boolean inProgress) {
    183         mSwipingInProgress = inProgress;
    184         if (inProgress) {
    185             mCallback.onSwipingStarted();
    186         }
    187     }
    188 
    189     private boolean rightSwipePossible() {
    190         return mRightIcon.getVisibility() == View.VISIBLE;
    191     }
    192 
    193     private boolean leftSwipePossible() {
    194         return mLeftIcon.getVisibility() == View.VISIBLE;
    195     }
    196 
    197     public boolean onInterceptTouchEvent(MotionEvent ev) {
    198         return false;
    199     }
    200 
    201     public void startHintAnimation(boolean right, Runnable onFinishedListener) {
    202 
    203         startHintAnimationPhase1(right, onFinishedListener);
    204     }
    205 
    206     private void startHintAnimationPhase1(final boolean right, final Runnable onFinishedListener) {
    207         final KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon;
    208         targetView.showArrow(true);
    209         ValueAnimator animator = getAnimatorToRadius(right, mHintGrowAmount);
    210         animator.addListener(new AnimatorListenerAdapter() {
    211             private boolean mCancelled;
    212 
    213             @Override
    214             public void onAnimationCancel(Animator animation) {
    215                 mCancelled = true;
    216             }
    217 
    218             @Override
    219             public void onAnimationEnd(Animator animation) {
    220                 if (mCancelled) {
    221                     mSwipeAnimator = null;
    222                     onFinishedListener.run();
    223                     targetView.showArrow(false);
    224                 } else {
    225                     startUnlockHintAnimationPhase2(right, onFinishedListener);
    226                 }
    227             }
    228         });
    229         animator.setInterpolator(mAppearInterpolator);
    230         animator.setDuration(HINT_PHASE1_DURATION);
    231         animator.start();
    232         mSwipeAnimator = animator;
    233     }
    234 
    235     /**
    236      * Phase 2: Move back.
    237      */
    238     private void startUnlockHintAnimationPhase2(boolean right, final Runnable onFinishedListener) {
    239         final KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon;
    240         ValueAnimator animator = getAnimatorToRadius(right, 0);
    241         animator.addListener(new AnimatorListenerAdapter() {
    242             @Override
    243             public void onAnimationEnd(Animator animation) {
    244                 mSwipeAnimator = null;
    245                 targetView.showArrow(false);
    246                 onFinishedListener.run();
    247             }
    248 
    249             @Override
    250             public void onAnimationStart(Animator animation) {
    251                 targetView.showArrow(false);
    252             }
    253         });
    254         animator.setInterpolator(mDisappearInterpolator);
    255         animator.setDuration(HINT_PHASE2_DURATION);
    256         animator.setStartDelay(HINT_CIRCLE_OPEN_DURATION);
    257         animator.start();
    258         mSwipeAnimator = animator;
    259     }
    260 
    261     private ValueAnimator getAnimatorToRadius(final boolean right, int radius) {
    262         final KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon;
    263         ValueAnimator animator = ValueAnimator.ofFloat(targetView.getCircleRadius(), radius);
    264         animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    265             @Override
    266             public void onAnimationUpdate(ValueAnimator animation) {
    267                 float newRadius = (float) animation.getAnimatedValue();
    268                 targetView.setCircleRadiusWithoutAnimation(newRadius);
    269                 float translation = getTranslationFromRadius(newRadius);
    270                 mTranslation = right ? -translation : translation;
    271                 updateIconsFromRadius(targetView, newRadius);
    272             }
    273         });
    274         return animator;
    275     }
    276 
    277     private void cancelAnimation() {
    278         if (mSwipeAnimator != null) {
    279             mSwipeAnimator.cancel();
    280         }
    281     }
    282 
    283     private void flingWithCurrentVelocity(boolean forceSnapBack) {
    284         float vel = getCurrentVelocity();
    285 
    286         // We snap back if the current translation is not far enough
    287         boolean snapBack = isBelowFalsingThreshold();
    288 
    289         // or if the velocity is in the opposite direction.
    290         boolean velIsInWrongDirection = vel * mTranslation < 0;
    291         snapBack |= Math.abs(vel) > mMinFlingVelocity && velIsInWrongDirection;
    292         vel = snapBack ^ velIsInWrongDirection ? 0 : vel;
    293         fling(vel, snapBack || forceSnapBack);
    294     }
    295 
    296     private boolean isBelowFalsingThreshold() {
    297         return Math.abs(mTranslation) < Math.abs(mTranslationOnDown) + getMinTranslationAmount();
    298     }
    299 
    300     private int getMinTranslationAmount() {
    301         float factor = mCallback.getAffordanceFalsingFactor();
    302         return (int) (mMinTranslationAmount * factor);
    303     }
    304 
    305     private void fling(float vel, final boolean snapBack) {
    306         float target = mTranslation < 0 ? -mCallback.getPageWidth() : mCallback.getPageWidth();
    307         target = snapBack ? 0 : target;
    308 
    309         ValueAnimator animator = ValueAnimator.ofFloat(mTranslation, target);
    310         mFlingAnimationUtils.apply(animator, mTranslation, target, vel);
    311         animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    312             @Override
    313             public void onAnimationUpdate(ValueAnimator animation) {
    314                 mTranslation = (float) animation.getAnimatedValue();
    315             }
    316         });
    317         animator.addListener(mFlingEndListener);
    318         if (!snapBack) {
    319             startFinishingCircleAnimation(vel * 0.375f, mAnimationEndRunnable);
    320             mCallback.onAnimationToSideStarted(mTranslation < 0);
    321         } else {
    322             reset(true);
    323         }
    324         animator.start();
    325         mSwipeAnimator = animator;
    326     }
    327 
    328     private void startFinishingCircleAnimation(float velocity, Runnable mAnimationEndRunnable) {
    329         KeyguardAffordanceView targetView = mTranslation > 0 ? mLeftIcon : mRightIcon;
    330         targetView.finishAnimation(velocity, mAnimationEndRunnable);
    331     }
    332 
    333     private void setTranslation(float translation, boolean isReset, boolean animateReset) {
    334         translation = rightSwipePossible() ? translation : Math.max(0, translation);
    335         translation = leftSwipePossible() ? translation : Math.min(0, translation);
    336         float absTranslation = Math.abs(translation);
    337         if (absTranslation > Math.abs(mTranslationOnDown) + getMinTranslationAmount() ||
    338                 mMotionPerformedByUser) {
    339             mMotionPerformedByUser = true;
    340         }
    341         if (translation != mTranslation || isReset) {
    342             KeyguardAffordanceView targetView = translation > 0 ? mLeftIcon : mRightIcon;
    343             KeyguardAffordanceView otherView = translation > 0 ? mRightIcon : mLeftIcon;
    344             float alpha = absTranslation / getMinTranslationAmount();
    345 
    346             // We interpolate the alpha of the other icons to 0
    347             float fadeOutAlpha = SWIPE_RESTING_ALPHA_AMOUNT * (1.0f - alpha);
    348             fadeOutAlpha = Math.max(0.0f, fadeOutAlpha);
    349 
    350             // We interpolate the alpha of the targetView to 1
    351             alpha = fadeOutAlpha + alpha;
    352 
    353             boolean animateIcons = isReset && animateReset;
    354             float radius = getRadiusFromTranslation(absTranslation);
    355             boolean slowAnimation = isReset && isBelowFalsingThreshold();
    356             if (!isReset) {
    357                 updateIcon(targetView, radius, alpha, false, false);
    358             } else {
    359                 updateIcon(targetView, 0.0f, fadeOutAlpha, animateIcons, slowAnimation);
    360             }
    361             updateIcon(otherView, 0.0f, fadeOutAlpha, animateIcons, slowAnimation);
    362             updateIcon(mCenterIcon, 0.0f, fadeOutAlpha, animateIcons, slowAnimation);
    363 
    364             mTranslation = translation;
    365         }
    366     }
    367 
    368     private void updateIconsFromRadius(KeyguardAffordanceView targetView, float newRadius) {
    369         float alpha = newRadius / mMinBackgroundRadius;
    370 
    371         // We interpolate the alpha of the other icons to 0
    372         float fadeOutAlpha = SWIPE_RESTING_ALPHA_AMOUNT * (1.0f - alpha);
    373         fadeOutAlpha = Math.max(0.0f, fadeOutAlpha);
    374 
    375         // We interpolate the alpha of the targetView to 1
    376         alpha = fadeOutAlpha + alpha;
    377         KeyguardAffordanceView otherView = targetView == mRightIcon ? mLeftIcon : mRightIcon;
    378         updateIconAlpha(targetView, alpha, false);
    379         updateIconAlpha(otherView, fadeOutAlpha, false);
    380         updateIconAlpha(mCenterIcon, fadeOutAlpha, false);
    381     }
    382 
    383     private float getTranslationFromRadius(float circleSize) {
    384         float translation = (circleSize - mMinBackgroundRadius) / BACKGROUND_RADIUS_SCALE_FACTOR;
    385         return Math.max(0, translation);
    386     }
    387 
    388     private float getRadiusFromTranslation(float translation) {
    389         return translation * BACKGROUND_RADIUS_SCALE_FACTOR + mMinBackgroundRadius;
    390     }
    391 
    392     public void animateHideLeftRightIcon() {
    393         updateIcon(mRightIcon, 0f, 0f, true, false);
    394         updateIcon(mLeftIcon, 0f, 0f, true, false);
    395     }
    396 
    397     private void updateIcon(KeyguardAffordanceView view, float circleRadius, float alpha,
    398             boolean animate, boolean slowRadiusAnimation) {
    399         if (view.getVisibility() != View.VISIBLE) {
    400             return;
    401         }
    402         view.setCircleRadius(circleRadius, slowRadiusAnimation);
    403         updateIconAlpha(view, alpha, animate);
    404     }
    405 
    406     private void updateIconAlpha(KeyguardAffordanceView view, float alpha, boolean animate) {
    407         float scale = getScale(alpha);
    408         alpha = Math.min(1.0f, alpha);
    409         view.setImageAlpha(alpha, animate);
    410         view.setImageScale(scale, animate);
    411     }
    412 
    413     private float getScale(float alpha) {
    414         float scale = alpha / SWIPE_RESTING_ALPHA_AMOUNT * 0.2f +
    415                 KeyguardAffordanceView.MIN_ICON_SCALE_AMOUNT;
    416         return Math.min(scale, KeyguardAffordanceView.MAX_ICON_SCALE_AMOUNT);
    417     }
    418 
    419     private void trackMovement(MotionEvent event) {
    420         if (mVelocityTracker != null) {
    421             mVelocityTracker.addMovement(event);
    422         }
    423     }
    424 
    425     private void initVelocityTracker() {
    426         if (mVelocityTracker != null) {
    427             mVelocityTracker.recycle();
    428         }
    429         mVelocityTracker = VelocityTracker.obtain();
    430     }
    431 
    432     private float getCurrentVelocity() {
    433         if (mVelocityTracker == null) {
    434             return 0;
    435         }
    436         mVelocityTracker.computeCurrentVelocity(1000);
    437         return mVelocityTracker.getXVelocity();
    438     }
    439 
    440     public void onConfigurationChanged() {
    441         initDimens();
    442         initIcons();
    443     }
    444 
    445     public void reset(boolean animate) {
    446         if (mSwipeAnimator != null) {
    447             mSwipeAnimator.cancel();
    448         }
    449         setTranslation(0.0f, true, animate);
    450         setSwipingInProgress(false);
    451     }
    452 
    453     public interface Callback {
    454 
    455         /**
    456          * Notifies the callback when an animation to a side page was started.
    457          *
    458          * @param rightPage Is the page animated to the right page?
    459          */
    460         void onAnimationToSideStarted(boolean rightPage);
    461 
    462         /**
    463          * Notifies the callback the animation to a side page has ended.
    464          */
    465         void onAnimationToSideEnded();
    466 
    467         float getPageWidth();
    468 
    469         void onSwipingStarted();
    470 
    471         KeyguardAffordanceView getLeftIcon();
    472 
    473         KeyguardAffordanceView getCenterIcon();
    474 
    475         KeyguardAffordanceView getRightIcon();
    476 
    477         View getLeftPreview();
    478 
    479         View getRightPreview();
    480 
    481         /**
    482          * @return The factor the minimum swipe amount should be multiplied with.
    483          */
    484         float getAffordanceFalsingFactor();
    485     }
    486 }
    487