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.25f;
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 mMotionCancelled;
67     private int mTouchTargetSize;
68     private View mTargetedView;
69     private boolean mTouchSlopExeeded;
70     private AnimatorListenerAdapter mFlingEndListener = new AnimatorListenerAdapter() {
71         @Override
72         public void onAnimationEnd(Animator animation) {
73             mSwipeAnimator = null;
74             mSwipingInProgress = false;
75             mTargetedView = null;
76         }
77     };
78     private Runnable mAnimationEndRunnable = new Runnable() {
79         @Override
80         public void run() {
81             mCallback.onAnimationToSideEnded();
82         }
83     };
84 
KeyguardAffordanceHelper(Callback callback, Context context)85     KeyguardAffordanceHelper(Callback callback, Context context) {
86         mContext = context;
87         mCallback = callback;
88         initIcons();
89         updateIcon(mLeftIcon, 0.0f, mLeftIcon.getRestingAlpha(), false, false, true);
90         updateIcon(mCenterIcon, 0.0f, mCenterIcon.getRestingAlpha(), false, false, true);
91         updateIcon(mRightIcon, 0.0f, mRightIcon.getRestingAlpha(), false, false, true);
92         initDimens();
93     }
94 
initDimens()95     private void initDimens() {
96         final ViewConfiguration configuration = ViewConfiguration.get(mContext);
97         mTouchSlop = configuration.getScaledPagingTouchSlop();
98         mMinFlingVelocity = configuration.getScaledMinimumFlingVelocity();
99         mMinTranslationAmount = mContext.getResources().getDimensionPixelSize(
100                 R.dimen.keyguard_min_swipe_amount);
101         mMinBackgroundRadius = mContext.getResources().getDimensionPixelSize(
102                 R.dimen.keyguard_affordance_min_background_radius);
103         mTouchTargetSize = mContext.getResources().getDimensionPixelSize(
104                 R.dimen.keyguard_affordance_touch_target_size);
105         mHintGrowAmount =
106                 mContext.getResources().getDimensionPixelSize(R.dimen.hint_grow_amount_sideways);
107         mFlingAnimationUtils = new FlingAnimationUtils(mContext, 0.4f);
108         mAppearInterpolator = AnimationUtils.loadInterpolator(mContext,
109                 android.R.interpolator.linear_out_slow_in);
110         mDisappearInterpolator = AnimationUtils.loadInterpolator(mContext,
111                 android.R.interpolator.fast_out_linear_in);
112     }
113 
initIcons()114     private void initIcons() {
115         mLeftIcon = mCallback.getLeftIcon();
116         mCenterIcon = mCallback.getCenterIcon();
117         mRightIcon = mCallback.getRightIcon();
118         updatePreviews();
119     }
120 
updatePreviews()121     public void updatePreviews() {
122         mLeftIcon.setPreviewView(mCallback.getLeftPreview());
123         mRightIcon.setPreviewView(mCallback.getRightPreview());
124     }
125 
onTouchEvent(MotionEvent event)126     public boolean onTouchEvent(MotionEvent event) {
127         int action = event.getActionMasked();
128         if (mMotionCancelled && action != MotionEvent.ACTION_DOWN) {
129             return false;
130         }
131         final float y = event.getY();
132         final float x = event.getX();
133 
134         boolean isUp = false;
135         switch (action) {
136             case MotionEvent.ACTION_DOWN:
137                 View targetView = getIconAtPosition(x, y);
138                 if (targetView == null || (mTargetedView != null && mTargetedView != targetView)) {
139                     mMotionCancelled = true;
140                     return false;
141                 }
142                 if (mTargetedView != null) {
143                     cancelAnimation();
144                 } else {
145                     mTouchSlopExeeded = false;
146                 }
147                 mCallback.onSwipingStarted(targetView == mRightIcon);
148                 mSwipingInProgress = true;
149                 mTargetedView = targetView;
150                 mInitialTouchX = x;
151                 mInitialTouchY = y;
152                 mTranslationOnDown = mTranslation;
153                 initVelocityTracker();
154                 trackMovement(event);
155                 mMotionCancelled = false;
156                 break;
157             case MotionEvent.ACTION_POINTER_DOWN:
158                 mMotionCancelled = true;
159                 endMotion(true /* forceSnapBack */, x, y);
160                 break;
161             case MotionEvent.ACTION_MOVE:
162                 trackMovement(event);
163                 float xDist = x - mInitialTouchX;
164                 float yDist = y - mInitialTouchY;
165                 float distance = (float) Math.hypot(xDist, yDist);
166                 if (!mTouchSlopExeeded && distance > mTouchSlop) {
167                     mTouchSlopExeeded = true;
168                 }
169                 if (mSwipingInProgress) {
170                     if (mTargetedView == mRightIcon) {
171                         distance = mTranslationOnDown - distance;
172                         distance = Math.min(0, distance);
173                     } else {
174                         distance = mTranslationOnDown + distance;
175                         distance = Math.max(0, distance);
176                     }
177                     setTranslation(distance, false /* isReset */, false /* animateReset */);
178                 }
179                 break;
180 
181             case MotionEvent.ACTION_UP:
182                 isUp = true;
183             case MotionEvent.ACTION_CANCEL:
184                 boolean hintOnTheRight = mTargetedView == mRightIcon;
185                 trackMovement(event);
186                 endMotion(!isUp, x, y);
187                 if (!mTouchSlopExeeded && isUp) {
188                     mCallback.onIconClicked(hintOnTheRight);
189                 }
190                 break;
191         }
192         return true;
193     }
194 
getIconAtPosition(float x, float y)195     private View getIconAtPosition(float x, float y) {
196         if (leftSwipePossible() && isOnIcon(mLeftIcon, x, y)) {
197             return mLeftIcon;
198         }
199         if (rightSwipePossible() && isOnIcon(mRightIcon, x, y)) {
200             return mRightIcon;
201         }
202         return null;
203     }
204 
isOnAffordanceIcon(float x, float y)205     public boolean isOnAffordanceIcon(float x, float y) {
206         return isOnIcon(mLeftIcon, x, y) || isOnIcon(mRightIcon, x, y);
207     }
208 
isOnIcon(View icon, float x, float y)209     private boolean isOnIcon(View icon, float x, float y) {
210         float iconX = icon.getX() + icon.getWidth() / 2.0f;
211         float iconY = icon.getY() + icon.getHeight() / 2.0f;
212         double distance = Math.hypot(x - iconX, y - iconY);
213         return distance <= mTouchTargetSize / 2;
214     }
215 
endMotion(boolean forceSnapBack, float lastX, float lastY)216     private void endMotion(boolean forceSnapBack, float lastX, float lastY) {
217         if (mSwipingInProgress) {
218             flingWithCurrentVelocity(forceSnapBack, lastX, lastY);
219         } else {
220             mTargetedView = null;
221         }
222         if (mVelocityTracker != null) {
223             mVelocityTracker.recycle();
224             mVelocityTracker = null;
225         }
226     }
227 
rightSwipePossible()228     private boolean rightSwipePossible() {
229         return mRightIcon.getVisibility() == View.VISIBLE;
230     }
231 
leftSwipePossible()232     private boolean leftSwipePossible() {
233         return mLeftIcon.getVisibility() == View.VISIBLE;
234     }
235 
onInterceptTouchEvent(MotionEvent ev)236     public boolean onInterceptTouchEvent(MotionEvent ev) {
237         return false;
238     }
239 
startHintAnimation(boolean right, Runnable onFinishedListener)240     public void startHintAnimation(boolean right,
241             Runnable onFinishedListener) {
242         cancelAnimation();
243         startHintAnimationPhase1(right, onFinishedListener);
244     }
245 
startHintAnimationPhase1(final boolean right, final Runnable onFinishedListener)246     private void startHintAnimationPhase1(final boolean right, final Runnable onFinishedListener) {
247         final KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon;
248         ValueAnimator animator = getAnimatorToRadius(right, mHintGrowAmount);
249         animator.addListener(new AnimatorListenerAdapter() {
250             private boolean mCancelled;
251 
252             @Override
253             public void onAnimationCancel(Animator animation) {
254                 mCancelled = true;
255             }
256 
257             @Override
258             public void onAnimationEnd(Animator animation) {
259                 if (mCancelled) {
260                     mSwipeAnimator = null;
261                     mTargetedView = null;
262                     onFinishedListener.run();
263                 } else {
264                     startUnlockHintAnimationPhase2(right, onFinishedListener);
265                 }
266             }
267         });
268         animator.setInterpolator(mAppearInterpolator);
269         animator.setDuration(HINT_PHASE1_DURATION);
270         animator.start();
271         mSwipeAnimator = animator;
272         mTargetedView = targetView;
273     }
274 
275     /**
276      * Phase 2: Move back.
277      */
startUnlockHintAnimationPhase2(boolean right, final Runnable onFinishedListener)278     private void startUnlockHintAnimationPhase2(boolean right, final Runnable onFinishedListener) {
279         ValueAnimator animator = getAnimatorToRadius(right, 0);
280         animator.addListener(new AnimatorListenerAdapter() {
281             @Override
282             public void onAnimationEnd(Animator animation) {
283                 mSwipeAnimator = null;
284                 mTargetedView = null;
285                 onFinishedListener.run();
286             }
287         });
288         animator.setInterpolator(mDisappearInterpolator);
289         animator.setDuration(HINT_PHASE2_DURATION);
290         animator.setStartDelay(HINT_CIRCLE_OPEN_DURATION);
291         animator.start();
292         mSwipeAnimator = animator;
293     }
294 
getAnimatorToRadius(final boolean right, int radius)295     private ValueAnimator getAnimatorToRadius(final boolean right, int radius) {
296         final KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon;
297         ValueAnimator animator = ValueAnimator.ofFloat(targetView.getCircleRadius(), radius);
298         animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
299             @Override
300             public void onAnimationUpdate(ValueAnimator animation) {
301                 float newRadius = (float) animation.getAnimatedValue();
302                 targetView.setCircleRadiusWithoutAnimation(newRadius);
303                 float translation = getTranslationFromRadius(newRadius);
304                 mTranslation = right ? -translation : translation;
305                 updateIconsFromTranslation(targetView);
306             }
307         });
308         return animator;
309     }
310 
cancelAnimation()311     private void cancelAnimation() {
312         if (mSwipeAnimator != null) {
313             mSwipeAnimator.cancel();
314         }
315     }
316 
flingWithCurrentVelocity(boolean forceSnapBack, float lastX, float lastY)317     private void flingWithCurrentVelocity(boolean forceSnapBack, float lastX, float lastY) {
318         float vel = getCurrentVelocity(lastX, lastY);
319 
320         // We snap back if the current translation is not far enough
321         boolean snapBack = isBelowFalsingThreshold();
322 
323         // or if the velocity is in the opposite direction.
324         boolean velIsInWrongDirection = vel * mTranslation < 0;
325         snapBack |= Math.abs(vel) > mMinFlingVelocity && velIsInWrongDirection;
326         vel = snapBack ^ velIsInWrongDirection ? 0 : vel;
327         fling(vel, snapBack || forceSnapBack);
328     }
329 
isBelowFalsingThreshold()330     private boolean isBelowFalsingThreshold() {
331         return Math.abs(mTranslation) < Math.abs(mTranslationOnDown) + getMinTranslationAmount();
332     }
333 
getMinTranslationAmount()334     private int getMinTranslationAmount() {
335         float factor = mCallback.getAffordanceFalsingFactor();
336         return (int) (mMinTranslationAmount * factor);
337     }
338 
fling(float vel, final boolean snapBack)339     private void fling(float vel, final boolean snapBack) {
340         float target = mTranslation < 0
341                 ? -mCallback.getMaxTranslationDistance()
342                 : mCallback.getMaxTranslationDistance();
343         target = snapBack ? 0 : target;
344 
345         ValueAnimator animator = ValueAnimator.ofFloat(mTranslation, target);
346         mFlingAnimationUtils.apply(animator, mTranslation, target, vel);
347         animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
348             @Override
349             public void onAnimationUpdate(ValueAnimator animation) {
350                 mTranslation = (float) animation.getAnimatedValue();
351             }
352         });
353         animator.addListener(mFlingEndListener);
354         if (!snapBack) {
355             startFinishingCircleAnimation(vel * 0.375f, mAnimationEndRunnable);
356             mCallback.onAnimationToSideStarted(mTranslation < 0, mTranslation, vel);
357         } else {
358             reset(true);
359         }
360         animator.start();
361         mSwipeAnimator = animator;
362         if (snapBack) {
363             mCallback.onSwipingAborted();
364         }
365     }
366 
367     private void startFinishingCircleAnimation(float velocity, Runnable mAnimationEndRunnable) {
368         KeyguardAffordanceView targetView = mTranslation > 0 ? mLeftIcon : mRightIcon;
369         targetView.finishAnimation(velocity, mAnimationEndRunnable);
370     }
371 
372     private void setTranslation(float translation, boolean isReset, boolean animateReset) {
373         translation = rightSwipePossible() ? translation : Math.max(0, translation);
374         translation = leftSwipePossible() ? translation : Math.min(0, translation);
375         float absTranslation = Math.abs(translation);
376         if (translation != mTranslation || isReset) {
377             KeyguardAffordanceView targetView = translation > 0 ? mLeftIcon : mRightIcon;
378             KeyguardAffordanceView otherView = translation > 0 ? mRightIcon : mLeftIcon;
379             float alpha = absTranslation / getMinTranslationAmount();
380 
381             // We interpolate the alpha of the other icons to 0
382             float fadeOutAlpha = 1.0f - alpha;
383             fadeOutAlpha = Math.max(fadeOutAlpha, 0.0f);
384 
385             boolean animateIcons = isReset && animateReset;
386             float radius = getRadiusFromTranslation(absTranslation);
387             boolean slowAnimation = isReset && isBelowFalsingThreshold();
388             if (!isReset) {
389                 updateIcon(targetView, radius, alpha + fadeOutAlpha * targetView.getRestingAlpha(),
390                         false, false, false);
391             } else {
392                 updateIcon(targetView, 0.0f, fadeOutAlpha * targetView.getRestingAlpha(),
393                         animateIcons, slowAnimation, false);
394             }
395             updateIcon(otherView, 0.0f, fadeOutAlpha * otherView.getRestingAlpha(),
396                     animateIcons, slowAnimation, false);
397             updateIcon(mCenterIcon, 0.0f, fadeOutAlpha * mCenterIcon.getRestingAlpha(),
398                     animateIcons, slowAnimation, false);
399 
400             mTranslation = translation;
401         }
402     }
403 
updateIconsFromTranslation(KeyguardAffordanceView targetView)404     private void updateIconsFromTranslation(KeyguardAffordanceView targetView) {
405         float absTranslation = Math.abs(mTranslation);
406         float alpha = absTranslation / getMinTranslationAmount();
407 
408         // We interpolate the alpha of the other icons to 0
409         float fadeOutAlpha =  1.0f - alpha;
410         fadeOutAlpha = Math.max(0.0f, fadeOutAlpha);
411 
412         // We interpolate the alpha of the targetView to 1
413         KeyguardAffordanceView otherView = targetView == mRightIcon ? mLeftIcon : mRightIcon;
414         updateIconAlpha(targetView, alpha + fadeOutAlpha * targetView.getRestingAlpha(), false);
415         updateIconAlpha(otherView, fadeOutAlpha * otherView.getRestingAlpha(), false);
416         updateIconAlpha(mCenterIcon, fadeOutAlpha * mCenterIcon.getRestingAlpha(), false);
417     }
418 
getTranslationFromRadius(float circleSize)419     private float getTranslationFromRadius(float circleSize) {
420         float translation = (circleSize - mMinBackgroundRadius)
421                 / BACKGROUND_RADIUS_SCALE_FACTOR;
422         return translation > 0.0f ? translation + mTouchSlop : 0.0f;
423     }
424 
getRadiusFromTranslation(float translation)425     private float getRadiusFromTranslation(float translation) {
426         if (translation <= mTouchSlop) {
427             return 0.0f;
428         }
429         return (translation - mTouchSlop)  * BACKGROUND_RADIUS_SCALE_FACTOR + mMinBackgroundRadius;
430     }
431 
animateHideLeftRightIcon()432     public void animateHideLeftRightIcon() {
433         cancelAnimation();
434         updateIcon(mRightIcon, 0f, 0f, true, false, false);
435         updateIcon(mLeftIcon, 0f, 0f, true, false, false);
436     }
437 
updateIcon(KeyguardAffordanceView view, float circleRadius, float alpha, boolean animate, boolean slowRadiusAnimation, boolean force)438     private void updateIcon(KeyguardAffordanceView view, float circleRadius, float alpha,
439             boolean animate, boolean slowRadiusAnimation, boolean force) {
440         if (view.getVisibility() != View.VISIBLE && !force) {
441             return;
442         }
443         view.setCircleRadius(circleRadius, slowRadiusAnimation);
444         updateIconAlpha(view, alpha, animate);
445     }
446 
updateIconAlpha(KeyguardAffordanceView view, float alpha, boolean animate)447     private void updateIconAlpha(KeyguardAffordanceView view, float alpha, boolean animate) {
448         float scale = getScale(alpha, view);
449         alpha = Math.min(1.0f, alpha);
450         view.setImageAlpha(alpha, animate);
451         view.setImageScale(scale, animate);
452     }
453 
getScale(float alpha, KeyguardAffordanceView icon)454     private float getScale(float alpha, KeyguardAffordanceView icon) {
455         float scale = alpha / icon.getRestingAlpha() * 0.2f +
456                 KeyguardAffordanceView.MIN_ICON_SCALE_AMOUNT;
457         return Math.min(scale, KeyguardAffordanceView.MAX_ICON_SCALE_AMOUNT);
458     }
459 
trackMovement(MotionEvent event)460     private void trackMovement(MotionEvent event) {
461         if (mVelocityTracker != null) {
462             mVelocityTracker.addMovement(event);
463         }
464     }
465 
initVelocityTracker()466     private void initVelocityTracker() {
467         if (mVelocityTracker != null) {
468             mVelocityTracker.recycle();
469         }
470         mVelocityTracker = VelocityTracker.obtain();
471     }
472 
getCurrentVelocity(float lastX, float lastY)473     private float getCurrentVelocity(float lastX, float lastY) {
474         if (mVelocityTracker == null) {
475             return 0;
476         }
477         mVelocityTracker.computeCurrentVelocity(1000);
478         float aX = mVelocityTracker.getXVelocity();
479         float aY = mVelocityTracker.getYVelocity();
480         float bX = lastX - mInitialTouchX;
481         float bY = lastY - mInitialTouchY;
482         float bLen = (float) Math.hypot(bX, bY);
483         // Project the velocity onto the distance vector: a * b / |b|
484         float projectedVelocity = (aX * bX + aY * bY) / bLen;
485         if (mTargetedView == mRightIcon) {
486             projectedVelocity = -projectedVelocity;
487         }
488         return projectedVelocity;
489     }
490 
onConfigurationChanged()491     public void onConfigurationChanged() {
492         initDimens();
493         initIcons();
494     }
495 
onRtlPropertiesChanged()496     public void onRtlPropertiesChanged() {
497         initIcons();
498     }
499 
reset(boolean animate)500     public void reset(boolean animate) {
501         cancelAnimation();
502         setTranslation(0.0f, true, animate);
503         mMotionCancelled = true;
504         if (mSwipingInProgress) {
505             mCallback.onSwipingAborted();
506         }
507         mSwipingInProgress = false;
508     }
509 
510     public interface Callback {
511 
512         /**
513          * Notifies the callback when an animation to a side page was started.
514          *
515          * @param rightPage Is the page animated to the right page?
516          */
onAnimationToSideStarted(boolean rightPage, float translation, float vel)517         void onAnimationToSideStarted(boolean rightPage, float translation, float vel);
518 
519         /**
520          * Notifies the callback the animation to a side page has ended.
521          */
onAnimationToSideEnded()522         void onAnimationToSideEnded();
523 
getMaxTranslationDistance()524         float getMaxTranslationDistance();
525 
onSwipingStarted(boolean rightIcon)526         void onSwipingStarted(boolean rightIcon);
527 
onSwipingAborted()528         void onSwipingAborted();
529 
onIconClicked(boolean rightIcon)530         void onIconClicked(boolean rightIcon);
531 
getLeftIcon()532         KeyguardAffordanceView getLeftIcon();
533 
getCenterIcon()534         KeyguardAffordanceView getCenterIcon();
535 
getRightIcon()536         KeyguardAffordanceView getRightIcon();
537 
getLeftPreview()538         View getLeftPreview();
539 
getRightPreview()540         View getRightPreview();
541 
542         /**
543          * @return The factor the minimum swipe amount should be multiplied with.
544          */
getAffordanceFalsingFactor()545         float getAffordanceFalsingFactor();
546     }
547 }
548