KeyguardAffordanceHelper.java revision 0a8182227249df6d8a76e19886f762b80a046e76
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 int mTrackingPointer;
50    private VelocityTracker mVelocityTracker;
51    private boolean mSwipingInProgress;
52    private float mInitialTouchX;
53    private float mInitialTouchY;
54    private float mTranslation;
55    private float mTranslationOnDown;
56    private int mTouchSlop;
57    private int mMinTranslationAmount;
58    private int mMinFlingVelocity;
59    private int mHintGrowAmount;
60    private KeyguardAffordanceView mLeftIcon;
61    private KeyguardAffordanceView mCenterIcon;
62    private KeyguardAffordanceView mRightIcon;
63    private Interpolator mAppearInterpolator;
64    private Interpolator mDisappearInterpolator;
65    private Animator mSwipeAnimator;
66    private int mMinBackgroundRadius;
67    private boolean mMotionPerformedByUser;
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        int pointerIndex = event.findPointerIndex(mTrackingPointer);
121        if (pointerIndex < 0) {
122            pointerIndex = 0;
123            mTrackingPointer = event.getPointerId(pointerIndex);
124        }
125        final float y = event.getY(pointerIndex);
126        final float x = event.getX(pointerIndex);
127
128        boolean isUp = false;
129        switch (event.getActionMasked()) {
130            case MotionEvent.ACTION_DOWN:
131                if (mSwipingInProgress) {
132                    cancelAnimation();
133                }
134                mInitialTouchY = y;
135                mInitialTouchX = x;
136                mTranslationOnDown = mTranslation;
137                initVelocityTracker();
138                trackMovement(event);
139                mMotionPerformedByUser = false;
140                break;
141
142            case MotionEvent.ACTION_POINTER_UP:
143                final int upPointer = event.getPointerId(event.getActionIndex());
144                if (mTrackingPointer == upPointer) {
145                    // gesture is ongoing, find a new pointer to track
146                    final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
147                    final float newY = event.getY(newIndex);
148                    final float newX = event.getX(newIndex);
149                    mTrackingPointer = event.getPointerId(newIndex);
150                    mInitialTouchY = newY;
151                    mInitialTouchX = newX;
152                    mTranslationOnDown = mTranslation;
153                }
154                break;
155
156            case MotionEvent.ACTION_MOVE:
157                final float w = x - mInitialTouchX;
158                trackMovement(event);
159                if (((leftSwipePossible() && w > mTouchSlop)
160                        || (rightSwipePossible() && w < -mTouchSlop))
161                        && Math.abs(w) > Math.abs(y - mInitialTouchY)
162                        && !mSwipingInProgress) {
163                    cancelAnimation();
164                    mInitialTouchY = y;
165                    mInitialTouchX = x;
166                    mTranslationOnDown = mTranslation;
167                    setSwipingInProgress(true);
168                }
169                if (mSwipingInProgress) {
170                    setTranslation(mTranslationOnDown + x - mInitialTouchX, false, false);
171                }
172                break;
173
174            case MotionEvent.ACTION_UP:
175                isUp = true;
176            case MotionEvent.ACTION_CANCEL:
177                mTrackingPointer = -1;
178                trackMovement(event);
179                if (mSwipingInProgress) {
180                    flingWithCurrentVelocity(!isUp);
181                }
182                if (mVelocityTracker != null) {
183                    mVelocityTracker.recycle();
184                    mVelocityTracker = null;
185                }
186                break;
187        }
188        return true;
189    }
190
191    private void setSwipingInProgress(boolean inProgress) {
192        mSwipingInProgress = inProgress;
193        if (inProgress) {
194            mCallback.onSwipingStarted();
195        }
196    }
197
198    private boolean rightSwipePossible() {
199        return mRightIcon.getVisibility() == View.VISIBLE;
200    }
201
202    private boolean leftSwipePossible() {
203        return mLeftIcon.getVisibility() == View.VISIBLE;
204    }
205
206    public boolean onInterceptTouchEvent(MotionEvent ev) {
207        return false;
208    }
209
210    public void startHintAnimation(boolean right, Runnable onFinishedListener) {
211
212        startHintAnimationPhase1(right, onFinishedListener);
213    }
214
215    private void startHintAnimationPhase1(final boolean right, final Runnable onFinishedListener) {
216        final KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon;
217        targetView.showArrow(true);
218        ValueAnimator animator = getAnimatorToRadius(right, mHintGrowAmount);
219        animator.addListener(new AnimatorListenerAdapter() {
220            private boolean mCancelled;
221
222            @Override
223            public void onAnimationCancel(Animator animation) {
224                mCancelled = true;
225            }
226
227            @Override
228            public void onAnimationEnd(Animator animation) {
229                if (mCancelled) {
230                    mSwipeAnimator = null;
231                    onFinishedListener.run();
232                    targetView.showArrow(false);
233                } else {
234                    startUnlockHintAnimationPhase2(right, onFinishedListener);
235                }
236            }
237        });
238        animator.setInterpolator(mAppearInterpolator);
239        animator.setDuration(HINT_PHASE1_DURATION);
240        animator.start();
241        mSwipeAnimator = animator;
242    }
243
244    /**
245     * Phase 2: Move back.
246     */
247    private void startUnlockHintAnimationPhase2(boolean right, final Runnable onFinishedListener) {
248        final KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon;
249        ValueAnimator animator = getAnimatorToRadius(right, 0);
250        animator.addListener(new AnimatorListenerAdapter() {
251            @Override
252            public void onAnimationEnd(Animator animation) {
253                mSwipeAnimator = null;
254                targetView.showArrow(false);
255                onFinishedListener.run();
256            }
257
258            @Override
259            public void onAnimationStart(Animator animation) {
260                targetView.showArrow(false);
261            }
262        });
263        animator.setInterpolator(mDisappearInterpolator);
264        animator.setDuration(HINT_PHASE2_DURATION);
265        animator.setStartDelay(HINT_CIRCLE_OPEN_DURATION);
266        animator.start();
267        mSwipeAnimator = animator;
268    }
269
270    private ValueAnimator getAnimatorToRadius(final boolean right, int radius) {
271        final KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon;
272        ValueAnimator animator = ValueAnimator.ofFloat(targetView.getCircleRadius(), radius);
273        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
274            @Override
275            public void onAnimationUpdate(ValueAnimator animation) {
276                float newRadius = (float) animation.getAnimatedValue();
277                targetView.setCircleRadiusWithoutAnimation(newRadius);
278                float translation = getTranslationFromRadius(newRadius);
279                mTranslation = right ? -translation : translation;
280                updateIconsFromRadius(targetView, newRadius);
281            }
282        });
283        return animator;
284    }
285
286    private void cancelAnimation() {
287        if (mSwipeAnimator != null) {
288            mSwipeAnimator.cancel();
289        }
290    }
291
292    private void flingWithCurrentVelocity(boolean forceSnapBack) {
293        float vel = getCurrentVelocity();
294
295        // We snap back if the current translation is not far enough
296        boolean snapBack = isBelowFalsingThreshold();
297
298        // or if the velocity is in the opposite direction.
299        boolean velIsInWrongDirection = vel * mTranslation < 0;
300        snapBack |= Math.abs(vel) > mMinFlingVelocity && velIsInWrongDirection;
301        vel = snapBack ^ velIsInWrongDirection ? 0 : vel;
302        fling(vel, snapBack || forceSnapBack);
303    }
304
305    private boolean isBelowFalsingThreshold() {
306        return Math.abs(mTranslation) < Math.abs(mTranslationOnDown) + getMinTranslationAmount();
307    }
308
309    private int getMinTranslationAmount() {
310        float factor = mCallback.getAffordanceFalsingFactor();
311        return (int) (mMinTranslationAmount * factor);
312    }
313
314    private void fling(float vel, final boolean snapBack) {
315        float target = mTranslation < 0 ? -mCallback.getPageWidth() : mCallback.getPageWidth();
316        target = snapBack ? 0 : target;
317
318        ValueAnimator animator = ValueAnimator.ofFloat(mTranslation, target);
319        mFlingAnimationUtils.apply(animator, mTranslation, target, vel);
320        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
321            @Override
322            public void onAnimationUpdate(ValueAnimator animation) {
323                mTranslation = (float) animation.getAnimatedValue();
324            }
325        });
326        animator.addListener(mFlingEndListener);
327        if (!snapBack) {
328            startFinishingCircleAnimation(vel * 0.375f, mAnimationEndRunnable);
329            mCallback.onAnimationToSideStarted(mTranslation < 0);
330        } else {
331            reset(true);
332        }
333        animator.start();
334        mSwipeAnimator = animator;
335    }
336
337    private void startFinishingCircleAnimation(float velocity, Runnable mAnimationEndRunnable) {
338        KeyguardAffordanceView targetView = mTranslation > 0 ? mLeftIcon : mRightIcon;
339        targetView.finishAnimation(velocity, mAnimationEndRunnable);
340    }
341
342    private void setTranslation(float translation, boolean isReset, boolean animateReset) {
343        translation = rightSwipePossible() ? translation : Math.max(0, translation);
344        translation = leftSwipePossible() ? translation : Math.min(0, translation);
345        float absTranslation = Math.abs(translation);
346        if (absTranslation > Math.abs(mTranslationOnDown) + getMinTranslationAmount() ||
347                mMotionPerformedByUser) {
348            mMotionPerformedByUser = true;
349        }
350        if (translation != mTranslation || isReset) {
351            KeyguardAffordanceView targetView = translation > 0 ? mLeftIcon : mRightIcon;
352            KeyguardAffordanceView otherView = translation > 0 ? mRightIcon : mLeftIcon;
353            float alpha = absTranslation / getMinTranslationAmount();
354
355            // We interpolate the alpha of the other icons to 0
356            float fadeOutAlpha = SWIPE_RESTING_ALPHA_AMOUNT * (1.0f - alpha);
357            fadeOutAlpha = Math.max(0.0f, fadeOutAlpha);
358
359            // We interpolate the alpha of the targetView to 1
360            alpha = fadeOutAlpha + alpha;
361
362            boolean animateIcons = isReset && animateReset;
363            float radius = getRadiusFromTranslation(absTranslation);
364            boolean slowAnimation = isReset && isBelowFalsingThreshold();
365            if (!isReset) {
366                updateIcon(targetView, radius, alpha, false, false);
367            } else {
368                updateIcon(targetView, 0.0f, fadeOutAlpha, animateIcons, slowAnimation);
369            }
370            updateIcon(otherView, 0.0f, fadeOutAlpha, animateIcons, slowAnimation);
371            updateIcon(mCenterIcon, 0.0f, fadeOutAlpha, animateIcons, slowAnimation);
372
373            mTranslation = translation;
374        }
375    }
376
377    private void updateIconsFromRadius(KeyguardAffordanceView targetView, float newRadius) {
378        float alpha = newRadius / mMinBackgroundRadius;
379
380        // We interpolate the alpha of the other icons to 0
381        float fadeOutAlpha = SWIPE_RESTING_ALPHA_AMOUNT * (1.0f - alpha);
382        fadeOutAlpha = Math.max(0.0f, fadeOutAlpha);
383
384        // We interpolate the alpha of the targetView to 1
385        alpha = fadeOutAlpha + alpha;
386        KeyguardAffordanceView otherView = targetView == mRightIcon ? mLeftIcon : mRightIcon;
387        updateIconAlpha(targetView, alpha, false);
388        updateIconAlpha(otherView, fadeOutAlpha, false);
389        updateIconAlpha(mCenterIcon, fadeOutAlpha, false);
390    }
391
392    private float getTranslationFromRadius(float circleSize) {
393        float translation = (circleSize - mMinBackgroundRadius) / BACKGROUND_RADIUS_SCALE_FACTOR;
394        return Math.max(0, translation);
395    }
396
397    private float getRadiusFromTranslation(float translation) {
398        return translation * BACKGROUND_RADIUS_SCALE_FACTOR + mMinBackgroundRadius;
399    }
400
401    public void animateHideLeftRightIcon() {
402        updateIcon(mRightIcon, 0f, 0f, true, false);
403        updateIcon(mLeftIcon, 0f, 0f, true, false);
404    }
405
406    private void updateIcon(KeyguardAffordanceView view, float circleRadius, float alpha,
407            boolean animate, boolean slowRadiusAnimation) {
408        if (view.getVisibility() != View.VISIBLE) {
409            return;
410        }
411        view.setCircleRadius(circleRadius, slowRadiusAnimation);
412        updateIconAlpha(view, alpha, animate);
413    }
414
415    private void updateIconAlpha(KeyguardAffordanceView view, float alpha, boolean animate) {
416        float scale = getScale(alpha);
417        alpha = Math.min(1.0f, alpha);
418        view.setImageAlpha(alpha, animate);
419        view.setImageScale(scale, animate);
420    }
421
422    private float getScale(float alpha) {
423        float scale = alpha / SWIPE_RESTING_ALPHA_AMOUNT * 0.2f +
424                KeyguardAffordanceView.MIN_ICON_SCALE_AMOUNT;
425        return Math.min(scale, KeyguardAffordanceView.MAX_ICON_SCALE_AMOUNT);
426    }
427
428    private void trackMovement(MotionEvent event) {
429        if (mVelocityTracker != null) {
430            mVelocityTracker.addMovement(event);
431        }
432    }
433
434    private void initVelocityTracker() {
435        if (mVelocityTracker != null) {
436            mVelocityTracker.recycle();
437        }
438        mVelocityTracker = VelocityTracker.obtain();
439    }
440
441    private float getCurrentVelocity() {
442        if (mVelocityTracker == null) {
443            return 0;
444        }
445        mVelocityTracker.computeCurrentVelocity(1000);
446        return mVelocityTracker.getXVelocity();
447    }
448
449    public void onConfigurationChanged() {
450        initDimens();
451        initIcons();
452    }
453
454    public void onRtlPropertiesChanged() {
455        initIcons();
456    }
457
458    public void reset(boolean animate) {
459        if (mSwipeAnimator != null) {
460            mSwipeAnimator.cancel();
461        }
462        setTranslation(0.0f, true, animate);
463        setSwipingInProgress(false);
464    }
465
466    public interface Callback {
467
468        /**
469         * Notifies the callback when an animation to a side page was started.
470         *
471         * @param rightPage Is the page animated to the right page?
472         */
473        void onAnimationToSideStarted(boolean rightPage);
474
475        /**
476         * Notifies the callback the animation to a side page has ended.
477         */
478        void onAnimationToSideEnded();
479
480        float getPageWidth();
481
482        void onSwipingStarted();
483
484        KeyguardAffordanceView getLeftIcon();
485
486        KeyguardAffordanceView getCenterIcon();
487
488        KeyguardAffordanceView getRightIcon();
489
490        View getLeftPreview();
491
492        View getRightPreview();
493
494        /**
495         * @return The factor the minimum swipe amount should be multiplied with.
496         */
497        float getAffordanceFalsingFactor();
498    }
499}
500