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
17package com.android.systemui.statusbar.phone;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ValueAnimator;
22import android.content.Context;
23import android.view.MotionEvent;
24import android.view.VelocityTracker;
25import android.view.View;
26import android.view.ViewConfiguration;
27import android.view.animation.AnimationUtils;
28import android.view.animation.Interpolator;
29
30import com.android.systemui.R;
31import com.android.systemui.statusbar.FlingAnimationUtils;
32import com.android.systemui.statusbar.KeyguardAffordanceView;
33
34/**
35 * A touch handler of the keyguard which is responsible for launching phone and camera affordances.
36 */
37public 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, 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