blob: 858023dc6c62544b1598172a59e5ec759ea2c344 [file] [log] [blame]
/*
* 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.plugins.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 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 mRightIcon;
private Animator mSwipeAnimator;
private final 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, FalsingManager falsingManager) {
mContext = context;
mCallback = callback;
initIcons();
updateIcon(mLeftIcon, 0.0f, mLeftIcon.getRestingAlpha(), false, false, true, false);
updateIcon(mRightIcon, 0.0f, mRightIcon.getRestingAlpha(), false, false, true, false);
mFalsingManager = falsingManager;
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.getResources().getDisplayMetrics(),
0.4f);
}
private void initIcons() {
mLeftIcon = mCallback.getLeftIcon();
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 animationEndRunnable,
boolean right) {
KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon;
targetView.finishAnimation(velocity, animationEndRunnable);
}
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);
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);
}
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);
// Do not animate the circle expanding if the affordance isn't visible,
// otherwise the circle will be meaningless.
if (targetView.getVisibility() != View.VISIBLE) {
animate = false;
}
if (animate) {
fling(0, false, !left);
updateIcon(otherView, 0.0f, 0, true, false, true, false);
} else {
mCallback.onAnimationToSideStarted(!left, mTranslation, 0);
mTranslation = left ? mCallback.getMaxTranslationDistance()
: mCallback.getMaxTranslationDistance();
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 getRightIcon();
View getLeftPreview();
View getRightPreview();
/**
* @return The factor the minimum swipe amount should be multiplied with.
*/
float getAffordanceFalsingFactor();
boolean needsAntiFalsing();
}
}