/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.systemui.statusbar.phone; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.content.Context; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import com.android.systemui.Interpolators; import com.android.systemui.R; import com.android.systemui.classifier.FalsingManager; import com.android.systemui.statusbar.FlingAnimationUtils; import com.android.systemui.statusbar.KeyguardAffordanceView; /** * A touch handler of the keyguard which is responsible for launching phone and camera affordances. */ public class KeyguardAffordanceHelper { public static final float SWIPE_RESTING_ALPHA_AMOUNT = 0.5f; public static final long HINT_PHASE1_DURATION = 200; private static final long HINT_PHASE2_DURATION = 350; private static final float BACKGROUND_RADIUS_SCALE_FACTOR = 0.25f; private static final int HINT_CIRCLE_OPEN_DURATION = 500; private final Context mContext; private final Callback mCallback; private FlingAnimationUtils mFlingAnimationUtils; private VelocityTracker mVelocityTracker; private boolean mSwipingInProgress; private float mInitialTouchX; private float mInitialTouchY; private float mTranslation; private float mTranslationOnDown; private int mTouchSlop; private int mMinTranslationAmount; private int mMinFlingVelocity; private int mHintGrowAmount; private KeyguardAffordanceView mLeftIcon; private KeyguardAffordanceView mCenterIcon; private KeyguardAffordanceView mRightIcon; private Animator mSwipeAnimator; private FalsingManager mFalsingManager; private int mMinBackgroundRadius; private boolean mMotionCancelled; private int mTouchTargetSize; private View mTargetedView; private boolean mTouchSlopExeeded; private AnimatorListenerAdapter mFlingEndListener = new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mSwipeAnimator = null; mSwipingInProgress = false; mTargetedView = null; } }; private Runnable mAnimationEndRunnable = new Runnable() { @Override public void run() { mCallback.onAnimationToSideEnded(); } }; KeyguardAffordanceHelper(Callback callback, Context context) { mContext = context; mCallback = callback; initIcons(); updateIcon(mLeftIcon, 0.0f, mLeftIcon.getRestingAlpha(), false, false, true, false); updateIcon(mCenterIcon, 0.0f, mCenterIcon.getRestingAlpha(), false, false, true, false); updateIcon(mRightIcon, 0.0f, mRightIcon.getRestingAlpha(), false, false, true, false); initDimens(); } private void initDimens() { final ViewConfiguration configuration = ViewConfiguration.get(mContext); mTouchSlop = configuration.getScaledPagingTouchSlop(); mMinFlingVelocity = configuration.getScaledMinimumFlingVelocity(); mMinTranslationAmount = mContext.getResources().getDimensionPixelSize( R.dimen.keyguard_min_swipe_amount); mMinBackgroundRadius = mContext.getResources().getDimensionPixelSize( R.dimen.keyguard_affordance_min_background_radius); mTouchTargetSize = mContext.getResources().getDimensionPixelSize( R.dimen.keyguard_affordance_touch_target_size); mHintGrowAmount = mContext.getResources().getDimensionPixelSize(R.dimen.hint_grow_amount_sideways); mFlingAnimationUtils = new FlingAnimationUtils(mContext, 0.4f); mFalsingManager = FalsingManager.getInstance(mContext); } private void initIcons() { mLeftIcon = mCallback.getLeftIcon(); mCenterIcon = mCallback.getCenterIcon(); mRightIcon = mCallback.getRightIcon(); updatePreviews(); } public void updatePreviews() { mLeftIcon.setPreviewView(mCallback.getLeftPreview()); mRightIcon.setPreviewView(mCallback.getRightPreview()); } public boolean onTouchEvent(MotionEvent event) { int action = event.getActionMasked(); if (mMotionCancelled && action != MotionEvent.ACTION_DOWN) { return false; } final float y = event.getY(); final float x = event.getX(); boolean isUp = false; switch (action) { case MotionEvent.ACTION_DOWN: View targetView = getIconAtPosition(x, y); if (targetView == null || (mTargetedView != null && mTargetedView != targetView)) { mMotionCancelled = true; return false; } if (mTargetedView != null) { cancelAnimation(); } else { mTouchSlopExeeded = false; } startSwiping(targetView); mInitialTouchX = x; mInitialTouchY = y; mTranslationOnDown = mTranslation; initVelocityTracker(); trackMovement(event); mMotionCancelled = false; break; case MotionEvent.ACTION_POINTER_DOWN: mMotionCancelled = true; endMotion(true /* forceSnapBack */, x, y); break; case MotionEvent.ACTION_MOVE: trackMovement(event); float xDist = x - mInitialTouchX; float yDist = y - mInitialTouchY; float distance = (float) Math.hypot(xDist, yDist); if (!mTouchSlopExeeded && distance > mTouchSlop) { mTouchSlopExeeded = true; } if (mSwipingInProgress) { if (mTargetedView == mRightIcon) { distance = mTranslationOnDown - distance; distance = Math.min(0, distance); } else { distance = mTranslationOnDown + distance; distance = Math.max(0, distance); } setTranslation(distance, false /* isReset */, false /* animateReset */); } break; case MotionEvent.ACTION_UP: isUp = true; case MotionEvent.ACTION_CANCEL: boolean hintOnTheRight = mTargetedView == mRightIcon; trackMovement(event); endMotion(!isUp, x, y); if (!mTouchSlopExeeded && isUp) { mCallback.onIconClicked(hintOnTheRight); } break; } return true; } private void startSwiping(View targetView) { mCallback.onSwipingStarted(targetView == mRightIcon); mSwipingInProgress = true; mTargetedView = targetView; } private View getIconAtPosition(float x, float y) { if (leftSwipePossible() && isOnIcon(mLeftIcon, x, y)) { return mLeftIcon; } if (rightSwipePossible() && isOnIcon(mRightIcon, x, y)) { return mRightIcon; } return null; } public boolean isOnAffordanceIcon(float x, float y) { return isOnIcon(mLeftIcon, x, y) || isOnIcon(mRightIcon, x, y); } private boolean isOnIcon(View icon, float x, float y) { float iconX = icon.getX() + icon.getWidth() / 2.0f; float iconY = icon.getY() + icon.getHeight() / 2.0f; double distance = Math.hypot(x - iconX, y - iconY); return distance <= mTouchTargetSize / 2; } private void endMotion(boolean forceSnapBack, float lastX, float lastY) { if (mSwipingInProgress) { flingWithCurrentVelocity(forceSnapBack, lastX, lastY); } else { mTargetedView = null; } if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } } private boolean rightSwipePossible() { return mRightIcon.getVisibility() == View.VISIBLE; } private boolean leftSwipePossible() { return mLeftIcon.getVisibility() == View.VISIBLE; } public boolean onInterceptTouchEvent(MotionEvent ev) { return false; } public void startHintAnimation(boolean right, Runnable onFinishedListener) { cancelAnimation(); startHintAnimationPhase1(right, onFinishedListener); } private void startHintAnimationPhase1(final boolean right, final Runnable onFinishedListener) { final KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon; ValueAnimator animator = getAnimatorToRadius(right, mHintGrowAmount); animator.addListener(new AnimatorListenerAdapter() { private boolean mCancelled; @Override public void onAnimationCancel(Animator animation) { mCancelled = true; } @Override public void onAnimationEnd(Animator animation) { if (mCancelled) { mSwipeAnimator = null; mTargetedView = null; onFinishedListener.run(); } else { startUnlockHintAnimationPhase2(right, onFinishedListener); } } }); animator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN); animator.setDuration(HINT_PHASE1_DURATION); animator.start(); mSwipeAnimator = animator; mTargetedView = targetView; } /** * Phase 2: Move back. */ private void startUnlockHintAnimationPhase2(boolean right, final Runnable onFinishedListener) { ValueAnimator animator = getAnimatorToRadius(right, 0); animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mSwipeAnimator = null; mTargetedView = null; onFinishedListener.run(); } }); animator.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN); animator.setDuration(HINT_PHASE2_DURATION); animator.setStartDelay(HINT_CIRCLE_OPEN_DURATION); animator.start(); mSwipeAnimator = animator; } private ValueAnimator getAnimatorToRadius(final boolean right, int radius) { final KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon; ValueAnimator animator = ValueAnimator.ofFloat(targetView.getCircleRadius(), radius); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float newRadius = (float) animation.getAnimatedValue(); targetView.setCircleRadiusWithoutAnimation(newRadius); float translation = getTranslationFromRadius(newRadius); mTranslation = right ? -translation : translation; updateIconsFromTranslation(targetView); } }); return animator; } private void cancelAnimation() { if (mSwipeAnimator != null) { mSwipeAnimator.cancel(); } } private void flingWithCurrentVelocity(boolean forceSnapBack, float lastX, float lastY) { float vel = getCurrentVelocity(lastX, lastY); // We snap back if the current translation is not far enough boolean snapBack = false; if (mCallback.needsAntiFalsing()) { snapBack = snapBack || mFalsingManager.isFalseTouch(); } snapBack = snapBack || isBelowFalsingThreshold(); // or if the velocity is in the opposite direction. boolean velIsInWrongDirection = vel * mTranslation < 0; snapBack |= Math.abs(vel) > mMinFlingVelocity && velIsInWrongDirection; vel = snapBack ^ velIsInWrongDirection ? 0 : vel; fling(vel, snapBack || forceSnapBack, mTranslation < 0); } private boolean isBelowFalsingThreshold() { return Math.abs(mTranslation) < Math.abs(mTranslationOnDown) + getMinTranslationAmount(); } private int getMinTranslationAmount() { float factor = mCallback.getAffordanceFalsingFactor(); return (int) (mMinTranslationAmount * factor); } private void fling(float vel, final boolean snapBack, boolean right) { float target = right ? -mCallback.getMaxTranslationDistance() : mCallback.getMaxTranslationDistance(); target = snapBack ? 0 : target; ValueAnimator animator = ValueAnimator.ofFloat(mTranslation, target); mFlingAnimationUtils.apply(animator, mTranslation, target, vel); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mTranslation = (float) animation.getAnimatedValue(); } }); animator.addListener(mFlingEndListener); if (!snapBack) { startFinishingCircleAnimation(vel * 0.375f, mAnimationEndRunnable, right); mCallback.onAnimationToSideStarted(right, mTranslation, vel); } else { reset(true); } animator.start(); mSwipeAnimator = animator; if (snapBack) { mCallback.onSwipingAborted(); } } private void startFinishingCircleAnimation(float velocity, Runnable mAnimationEndRunnable, boolean right) { KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon; targetView.finishAnimation(velocity, mAnimationEndRunnable); } private void setTranslation(float translation, boolean isReset, boolean animateReset) { translation = rightSwipePossible() ? translation : Math.max(0, translation); translation = leftSwipePossible() ? translation : Math.min(0, translation); float absTranslation = Math.abs(translation); if (translation != mTranslation || isReset) { KeyguardAffordanceView targetView = translation > 0 ? mLeftIcon : mRightIcon; KeyguardAffordanceView otherView = translation > 0 ? mRightIcon : mLeftIcon; float alpha = absTranslation / getMinTranslationAmount(); // We interpolate the alpha of the other icons to 0 float fadeOutAlpha = 1.0f - alpha; fadeOutAlpha = Math.max(fadeOutAlpha, 0.0f); boolean animateIcons = isReset && animateReset; boolean forceNoCircleAnimation = isReset && !animateReset; float radius = getRadiusFromTranslation(absTranslation); boolean slowAnimation = isReset && isBelowFalsingThreshold(); if (!isReset) { updateIcon(targetView, radius, alpha + fadeOutAlpha * targetView.getRestingAlpha(), false, false, false, false); } else { updateIcon(targetView, 0.0f, fadeOutAlpha * targetView.getRestingAlpha(), animateIcons, slowAnimation, true /* isReset */, forceNoCircleAnimation); } updateIcon(otherView, 0.0f, fadeOutAlpha * otherView.getRestingAlpha(), animateIcons, slowAnimation, isReset, forceNoCircleAnimation); updateIcon(mCenterIcon, 0.0f, fadeOutAlpha * mCenterIcon.getRestingAlpha(), animateIcons, slowAnimation, isReset, forceNoCircleAnimation); mTranslation = translation; } } private void updateIconsFromTranslation(KeyguardAffordanceView targetView) { float absTranslation = Math.abs(mTranslation); float alpha = absTranslation / getMinTranslationAmount(); // We interpolate the alpha of the other icons to 0 float fadeOutAlpha = 1.0f - alpha; fadeOutAlpha = Math.max(0.0f, fadeOutAlpha); // We interpolate the alpha of the targetView to 1 KeyguardAffordanceView otherView = targetView == mRightIcon ? mLeftIcon : mRightIcon; updateIconAlpha(targetView, alpha + fadeOutAlpha * targetView.getRestingAlpha(), false); updateIconAlpha(otherView, fadeOutAlpha * otherView.getRestingAlpha(), false); updateIconAlpha(mCenterIcon, fadeOutAlpha * mCenterIcon.getRestingAlpha(), false); } private float getTranslationFromRadius(float circleSize) { float translation = (circleSize - mMinBackgroundRadius) / BACKGROUND_RADIUS_SCALE_FACTOR; return translation > 0.0f ? translation + mTouchSlop : 0.0f; } private float getRadiusFromTranslation(float translation) { if (translation <= mTouchSlop) { return 0.0f; } return (translation - mTouchSlop) * BACKGROUND_RADIUS_SCALE_FACTOR + mMinBackgroundRadius; } public void animateHideLeftRightIcon() { cancelAnimation(); updateIcon(mRightIcon, 0f, 0f, true, false, false, false); updateIcon(mLeftIcon, 0f, 0f, true, false, false, false); } private void updateIcon(KeyguardAffordanceView view, float circleRadius, float alpha, boolean animate, boolean slowRadiusAnimation, boolean force, boolean forceNoCircleAnimation) { if (view.getVisibility() != View.VISIBLE && !force) { return; } if (forceNoCircleAnimation) { view.setCircleRadiusWithoutAnimation(circleRadius); } else { view.setCircleRadius(circleRadius, slowRadiusAnimation); } updateIconAlpha(view, alpha, animate); } private void updateIconAlpha(KeyguardAffordanceView view, float alpha, boolean animate) { float scale = getScale(alpha, view); alpha = Math.min(1.0f, alpha); view.setImageAlpha(alpha, animate); view.setImageScale(scale, animate); } private float getScale(float alpha, KeyguardAffordanceView icon) { float scale = alpha / icon.getRestingAlpha() * 0.2f + KeyguardAffordanceView.MIN_ICON_SCALE_AMOUNT; return Math.min(scale, KeyguardAffordanceView.MAX_ICON_SCALE_AMOUNT); } private void trackMovement(MotionEvent event) { if (mVelocityTracker != null) { mVelocityTracker.addMovement(event); } } private void initVelocityTracker() { if (mVelocityTracker != null) { mVelocityTracker.recycle(); } mVelocityTracker = VelocityTracker.obtain(); } private float getCurrentVelocity(float lastX, float lastY) { if (mVelocityTracker == null) { return 0; } mVelocityTracker.computeCurrentVelocity(1000); float aX = mVelocityTracker.getXVelocity(); float aY = mVelocityTracker.getYVelocity(); float bX = lastX - mInitialTouchX; float bY = lastY - mInitialTouchY; float bLen = (float) Math.hypot(bX, bY); // Project the velocity onto the distance vector: a * b / |b| float projectedVelocity = (aX * bX + aY * bY) / bLen; if (mTargetedView == mRightIcon) { projectedVelocity = -projectedVelocity; } return projectedVelocity; } public void onConfigurationChanged() { initDimens(); initIcons(); } public void onRtlPropertiesChanged() { initIcons(); } public void reset(boolean animate) { cancelAnimation(); setTranslation(0.0f, true /* isReset */, animate); mMotionCancelled = true; if (mSwipingInProgress) { mCallback.onSwipingAborted(); mSwipingInProgress = false; } } public boolean isSwipingInProgress() { return mSwipingInProgress; } public void launchAffordance(boolean animate, boolean left) { if (mSwipingInProgress) { // We don't want to mess with the state if the user is actually swiping already. return; } KeyguardAffordanceView targetView = left ? mLeftIcon : mRightIcon; KeyguardAffordanceView otherView = left ? mRightIcon : mLeftIcon; startSwiping(targetView); if (animate) { fling(0, false, !left); updateIcon(otherView, 0.0f, 0, true, false, true, false); updateIcon(mCenterIcon, 0.0f, 0, true, false, true, false); } else { mCallback.onAnimationToSideStarted(!left, mTranslation, 0); mTranslation = left ? mCallback.getMaxTranslationDistance() : mCallback.getMaxTranslationDistance(); updateIcon(mCenterIcon, 0.0f, 0.0f, false, false, true, false); updateIcon(otherView, 0.0f, 0.0f, false, false, true, false); targetView.instantFinishAnimation(); mFlingEndListener.onAnimationEnd(null); mAnimationEndRunnable.run(); } } public interface Callback { /** * Notifies the callback when an animation to a side page was started. * * @param rightPage Is the page animated to the right page? */ void onAnimationToSideStarted(boolean rightPage, float translation, float vel); /** * Notifies the callback the animation to a side page has ended. */ void onAnimationToSideEnded(); float getMaxTranslationDistance(); void onSwipingStarted(boolean rightIcon); void onSwipingAborted(); void onIconClicked(boolean rightIcon); KeyguardAffordanceView getLeftIcon(); KeyguardAffordanceView getCenterIcon(); KeyguardAffordanceView getRightIcon(); View getLeftPreview(); View getRightPreview(); /** * @return The factor the minimum swipe amount should be multiplied with. */ float getAffordanceFalsingFactor(); boolean needsAntiFalsing(); } }