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 
KeyguardAffordanceHelper(Callback callback, Context context)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 
initDimens()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 
initIcons()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 
onTouchEvent(MotionEvent event)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 
endMotion(MotionEvent event, boolean forceSnapBack)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 
setSwipingInProgress(boolean inProgress)182     private void setSwipingInProgress(boolean inProgress) {
183         mSwipingInProgress = inProgress;
184         if (inProgress) {
185             mCallback.onSwipingStarted();
186         }
187     }
188 
rightSwipePossible()189     private boolean rightSwipePossible() {
190         return mRightIcon.getVisibility() == View.VISIBLE;
191     }
192 
leftSwipePossible()193     private boolean leftSwipePossible() {
194         return mLeftIcon.getVisibility() == View.VISIBLE;
195     }
196 
onInterceptTouchEvent(MotionEvent ev)197     public boolean onInterceptTouchEvent(MotionEvent ev) {
198         return false;
199     }
200 
startHintAnimation(boolean right, Runnable onFinishedListener)201     public void startHintAnimation(boolean right, Runnable onFinishedListener) {
202 
203         startHintAnimationPhase1(right, onFinishedListener);
204     }
205 
startHintAnimationPhase1(final boolean right, final Runnable onFinishedListener)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      */
startUnlockHintAnimationPhase2(boolean right, final Runnable onFinishedListener)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 
getAnimatorToRadius(final boolean right, int radius)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 
cancelAnimation()277     private void cancelAnimation() {
278         if (mSwipeAnimator != null) {
279             mSwipeAnimator.cancel();
280         }
281     }
282 
flingWithCurrentVelocity(boolean forceSnapBack)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 
isBelowFalsingThreshold()296     private boolean isBelowFalsingThreshold() {
297         return Math.abs(mTranslation) < Math.abs(mTranslationOnDown) + getMinTranslationAmount();
298     }
299 
getMinTranslationAmount()300     private int getMinTranslationAmount() {
301         float factor = mCallback.getAffordanceFalsingFactor();
302         return (int) (mMinTranslationAmount * factor);
303     }
304 
fling(float vel, final boolean snapBack)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, mTranslation, vel);
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 onRtlPropertiesChanged() {
446         initIcons();
447     }
448 
449     public void reset(boolean animate) {
450         if (mSwipeAnimator != null) {
451             mSwipeAnimator.cancel();
452         }
453         setTranslation(0.0f, true, animate);
454         setSwipingInProgress(false);
455     }
456 
457     public interface Callback {
458 
459         /**
460          * Notifies the callback when an animation to a side page was started.
461          *
462          * @param rightPage Is the page animated to the right page?
463          */
464         void onAnimationToSideStarted(boolean rightPage, float translation, float vel);
465 
466         /**
467          * Notifies the callback the animation to a side page has ended.
468          */
469         void onAnimationToSideEnded();
470 
471         float getPageWidth();
472 
473         void onSwipingStarted();
474 
475         KeyguardAffordanceView getLeftIcon();
476 
477         KeyguardAffordanceView getCenterIcon();
478 
479         KeyguardAffordanceView getRightIcon();
480 
481         View getLeftPreview();
482 
483         View getRightPreview();
484 
485         /**
486          * @return The factor the minimum swipe amount should be multiplied with.
487          */
488         float getAffordanceFalsingFactor();
489     }
490 }
491