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 28 import com.android.systemui.Interpolators; 29 import com.android.systemui.R; 30 import com.android.systemui.classifier.FalsingManager; 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.25f; 43 private static final int HINT_CIRCLE_OPEN_DURATION = 500; 44 45 private final Context mContext; 46 private final Callback mCallback; 47 48 private FlingAnimationUtils mFlingAnimationUtils; 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 Animator mSwipeAnimator; 63 private FalsingManager mFalsingManager; 64 private int mMinBackgroundRadius; 65 private boolean mMotionCancelled; 66 private int mTouchTargetSize; 67 private View mTargetedView; 68 private boolean mTouchSlopExeeded; 69 private AnimatorListenerAdapter mFlingEndListener = new AnimatorListenerAdapter() { 70 @Override 71 public void onAnimationEnd(Animator animation) { 72 mSwipeAnimator = null; 73 mSwipingInProgress = false; 74 mTargetedView = null; 75 } 76 }; 77 private Runnable mAnimationEndRunnable = new Runnable() { 78 @Override 79 public void run() { 80 mCallback.onAnimationToSideEnded(); 81 } 82 }; 83 84 KeyguardAffordanceHelper(Callback callback, Context context) { 85 mContext = context; 86 mCallback = callback; 87 initIcons(); 88 updateIcon(mLeftIcon, 0.0f, mLeftIcon.getRestingAlpha(), false, false, true, false); 89 updateIcon(mCenterIcon, 0.0f, mCenterIcon.getRestingAlpha(), false, false, true, false); 90 updateIcon(mRightIcon, 0.0f, mRightIcon.getRestingAlpha(), false, false, true, false); 91 initDimens(); 92 } 93 94 private void initDimens() { 95 final ViewConfiguration configuration = ViewConfiguration.get(mContext); 96 mTouchSlop = configuration.getScaledPagingTouchSlop(); 97 mMinFlingVelocity = configuration.getScaledMinimumFlingVelocity(); 98 mMinTranslationAmount = mContext.getResources().getDimensionPixelSize( 99 R.dimen.keyguard_min_swipe_amount); 100 mMinBackgroundRadius = mContext.getResources().getDimensionPixelSize( 101 R.dimen.keyguard_affordance_min_background_radius); 102 mTouchTargetSize = mContext.getResources().getDimensionPixelSize( 103 R.dimen.keyguard_affordance_touch_target_size); 104 mHintGrowAmount = 105 mContext.getResources().getDimensionPixelSize(R.dimen.hint_grow_amount_sideways); 106 mFlingAnimationUtils = new FlingAnimationUtils(mContext, 0.4f); 107 mFalsingManager = FalsingManager.getInstance(mContext); 108 } 109 110 private void initIcons() { 111 mLeftIcon = mCallback.getLeftIcon(); 112 mCenterIcon = mCallback.getCenterIcon(); 113 mRightIcon = mCallback.getRightIcon(); 114 updatePreviews(); 115 } 116 117 public void updatePreviews() { 118 mLeftIcon.setPreviewView(mCallback.getLeftPreview()); 119 mRightIcon.setPreviewView(mCallback.getRightPreview()); 120 } 121 122 public boolean onTouchEvent(MotionEvent event) { 123 int action = event.getActionMasked(); 124 if (mMotionCancelled && action != MotionEvent.ACTION_DOWN) { 125 return false; 126 } 127 final float y = event.getY(); 128 final float x = event.getX(); 129 130 boolean isUp = false; 131 switch (action) { 132 case MotionEvent.ACTION_DOWN: 133 View targetView = getIconAtPosition(x, y); 134 if (targetView == null || (mTargetedView != null && mTargetedView != targetView)) { 135 mMotionCancelled = true; 136 return false; 137 } 138 if (mTargetedView != null) { 139 cancelAnimation(); 140 } else { 141 mTouchSlopExeeded = false; 142 } 143 startSwiping(targetView); 144 mInitialTouchX = x; 145 mInitialTouchY = y; 146 mTranslationOnDown = mTranslation; 147 initVelocityTracker(); 148 trackMovement(event); 149 mMotionCancelled = false; 150 break; 151 case MotionEvent.ACTION_POINTER_DOWN: 152 mMotionCancelled = true; 153 endMotion(true /* forceSnapBack */, x, y); 154 break; 155 case MotionEvent.ACTION_MOVE: 156 trackMovement(event); 157 float xDist = x - mInitialTouchX; 158 float yDist = y - mInitialTouchY; 159 float distance = (float) Math.hypot(xDist, yDist); 160 if (!mTouchSlopExeeded && distance > mTouchSlop) { 161 mTouchSlopExeeded = true; 162 } 163 if (mSwipingInProgress) { 164 if (mTargetedView == mRightIcon) { 165 distance = mTranslationOnDown - distance; 166 distance = Math.min(0, distance); 167 } else { 168 distance = mTranslationOnDown + distance; 169 distance = Math.max(0, distance); 170 } 171 setTranslation(distance, false /* isReset */, false /* animateReset */); 172 } 173 break; 174 175 case MotionEvent.ACTION_UP: 176 isUp = true; 177 case MotionEvent.ACTION_CANCEL: 178 boolean hintOnTheRight = mTargetedView == mRightIcon; 179 trackMovement(event); 180 endMotion(!isUp, x, y); 181 if (!mTouchSlopExeeded && isUp) { 182 mCallback.onIconClicked(hintOnTheRight); 183 } 184 break; 185 } 186 return true; 187 } 188 189 private void startSwiping(View targetView) { 190 mCallback.onSwipingStarted(targetView == mRightIcon); 191 mSwipingInProgress = true; 192 mTargetedView = targetView; 193 } 194 195 private View getIconAtPosition(float x, float y) { 196 if (leftSwipePossible() && isOnIcon(mLeftIcon, x, y)) { 197 return mLeftIcon; 198 } 199 if (rightSwipePossible() && isOnIcon(mRightIcon, x, y)) { 200 return mRightIcon; 201 } 202 return null; 203 } 204 205 public boolean isOnAffordanceIcon(float x, float y) { 206 return isOnIcon(mLeftIcon, x, y) || isOnIcon(mRightIcon, x, y); 207 } 208 209 private boolean isOnIcon(View icon, float x, float y) { 210 float iconX = icon.getX() + icon.getWidth() / 2.0f; 211 float iconY = icon.getY() + icon.getHeight() / 2.0f; 212 double distance = Math.hypot(x - iconX, y - iconY); 213 return distance <= mTouchTargetSize / 2; 214 } 215 216 private void endMotion(boolean forceSnapBack, float lastX, float lastY) { 217 if (mSwipingInProgress) { 218 flingWithCurrentVelocity(forceSnapBack, lastX, lastY); 219 } else { 220 mTargetedView = null; 221 } 222 if (mVelocityTracker != null) { 223 mVelocityTracker.recycle(); 224 mVelocityTracker = null; 225 } 226 } 227 228 private boolean rightSwipePossible() { 229 return mRightIcon.getVisibility() == View.VISIBLE; 230 } 231 232 private boolean leftSwipePossible() { 233 return mLeftIcon.getVisibility() == View.VISIBLE; 234 } 235 236 public boolean onInterceptTouchEvent(MotionEvent ev) { 237 return false; 238 } 239 240 public void startHintAnimation(boolean right, 241 Runnable onFinishedListener) { 242 cancelAnimation(); 243 startHintAnimationPhase1(right, onFinishedListener); 244 } 245 246 private void startHintAnimationPhase1(final boolean right, final Runnable onFinishedListener) { 247 final KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon; 248 ValueAnimator animator = getAnimatorToRadius(right, mHintGrowAmount); 249 animator.addListener(new AnimatorListenerAdapter() { 250 private boolean mCancelled; 251 252 @Override 253 public void onAnimationCancel(Animator animation) { 254 mCancelled = true; 255 } 256 257 @Override 258 public void onAnimationEnd(Animator animation) { 259 if (mCancelled) { 260 mSwipeAnimator = null; 261 mTargetedView = null; 262 onFinishedListener.run(); 263 } else { 264 startUnlockHintAnimationPhase2(right, onFinishedListener); 265 } 266 } 267 }); 268 animator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN); 269 animator.setDuration(HINT_PHASE1_DURATION); 270 animator.start(); 271 mSwipeAnimator = animator; 272 mTargetedView = targetView; 273 } 274 275 /** 276 * Phase 2: Move back. 277 */ 278 private void startUnlockHintAnimationPhase2(boolean right, final Runnable onFinishedListener) { 279 ValueAnimator animator = getAnimatorToRadius(right, 0); 280 animator.addListener(new AnimatorListenerAdapter() { 281 @Override 282 public void onAnimationEnd(Animator animation) { 283 mSwipeAnimator = null; 284 mTargetedView = null; 285 onFinishedListener.run(); 286 } 287 }); 288 animator.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN); 289 animator.setDuration(HINT_PHASE2_DURATION); 290 animator.setStartDelay(HINT_CIRCLE_OPEN_DURATION); 291 animator.start(); 292 mSwipeAnimator = animator; 293 } 294 295 private ValueAnimator getAnimatorToRadius(final boolean right, int radius) { 296 final KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon; 297 ValueAnimator animator = ValueAnimator.ofFloat(targetView.getCircleRadius(), radius); 298 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 299 @Override 300 public void onAnimationUpdate(ValueAnimator animation) { 301 float newRadius = (float) animation.getAnimatedValue(); 302 targetView.setCircleRadiusWithoutAnimation(newRadius); 303 float translation = getTranslationFromRadius(newRadius); 304 mTranslation = right ? -translation : translation; 305 updateIconsFromTranslation(targetView); 306 } 307 }); 308 return animator; 309 } 310 311 private void cancelAnimation() { 312 if (mSwipeAnimator != null) { 313 mSwipeAnimator.cancel(); 314 } 315 } 316 317 private void flingWithCurrentVelocity(boolean forceSnapBack, float lastX, float lastY) { 318 float vel = getCurrentVelocity(lastX, lastY); 319 320 // We snap back if the current translation is not far enough 321 boolean snapBack = false; 322 if (mCallback.needsAntiFalsing()) { 323 snapBack = snapBack || mFalsingManager.isFalseTouch(); 324 } 325 snapBack = snapBack || isBelowFalsingThreshold(); 326 327 // or if the velocity is in the opposite direction. 328 boolean velIsInWrongDirection = vel * mTranslation < 0; 329 snapBack |= Math.abs(vel) > mMinFlingVelocity && velIsInWrongDirection; 330 vel = snapBack ^ velIsInWrongDirection ? 0 : vel; 331 fling(vel, snapBack || forceSnapBack, mTranslation < 0); 332 } 333 334 private boolean isBelowFalsingThreshold() { 335 return Math.abs(mTranslation) < Math.abs(mTranslationOnDown) + getMinTranslationAmount(); 336 } 337 338 private int getMinTranslationAmount() { 339 float factor = mCallback.getAffordanceFalsingFactor(); 340 return (int) (mMinTranslationAmount * factor); 341 } 342 343 private void fling(float vel, final boolean snapBack, boolean right) { 344 float target = right ? -mCallback.getMaxTranslationDistance() 345 : mCallback.getMaxTranslationDistance(); 346 target = snapBack ? 0 : target; 347 348 ValueAnimator animator = ValueAnimator.ofFloat(mTranslation, target); 349 mFlingAnimationUtils.apply(animator, mTranslation, target, vel); 350 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 351 @Override 352 public void onAnimationUpdate(ValueAnimator animation) { 353 mTranslation = (float) animation.getAnimatedValue(); 354 } 355 }); 356 animator.addListener(mFlingEndListener); 357 if (!snapBack) { 358 startFinishingCircleAnimation(vel * 0.375f, mAnimationEndRunnable, right); 359 mCallback.onAnimationToSideStarted(right, mTranslation, vel); 360 } else { 361 reset(true); 362 } 363 animator.start(); 364 mSwipeAnimator = animator; 365 if (snapBack) { 366 mCallback.onSwipingAborted(); 367 } 368 } 369 370 private void startFinishingCircleAnimation(float velocity, Runnable mAnimationEndRunnable, 371 boolean right) { 372 KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon; 373 targetView.finishAnimation(velocity, mAnimationEndRunnable); 374 } 375 376 private void setTranslation(float translation, boolean isReset, boolean animateReset) { 377 translation = rightSwipePossible() ? translation : Math.max(0, translation); 378 translation = leftSwipePossible() ? translation : Math.min(0, translation); 379 float absTranslation = Math.abs(translation); 380 if (translation != mTranslation || isReset) { 381 KeyguardAffordanceView targetView = translation > 0 ? mLeftIcon : mRightIcon; 382 KeyguardAffordanceView otherView = translation > 0 ? mRightIcon : mLeftIcon; 383 float alpha = absTranslation / getMinTranslationAmount(); 384 385 // We interpolate the alpha of the other icons to 0 386 float fadeOutAlpha = 1.0f - alpha; 387 fadeOutAlpha = Math.max(fadeOutAlpha, 0.0f); 388 389 boolean animateIcons = isReset && animateReset; 390 boolean forceNoCircleAnimation = isReset && !animateReset; 391 float radius = getRadiusFromTranslation(absTranslation); 392 boolean slowAnimation = isReset && isBelowFalsingThreshold(); 393 if (!isReset) { 394 updateIcon(targetView, radius, alpha + fadeOutAlpha * targetView.getRestingAlpha(), 395 false, false, false, false); 396 } else { 397 updateIcon(targetView, 0.0f, fadeOutAlpha * targetView.getRestingAlpha(), 398 animateIcons, slowAnimation, false, forceNoCircleAnimation); 399 } 400 updateIcon(otherView, 0.0f, fadeOutAlpha * otherView.getRestingAlpha(), 401 animateIcons, slowAnimation, false, forceNoCircleAnimation); 402 updateIcon(mCenterIcon, 0.0f, fadeOutAlpha * mCenterIcon.getRestingAlpha(), 403 animateIcons, slowAnimation, false, forceNoCircleAnimation); 404 405 mTranslation = translation; 406 } 407 } 408 409 private void updateIconsFromTranslation(KeyguardAffordanceView targetView) { 410 float absTranslation = Math.abs(mTranslation); 411 float alpha = absTranslation / getMinTranslationAmount(); 412 413 // We interpolate the alpha of the other icons to 0 414 float fadeOutAlpha = 1.0f - alpha; 415 fadeOutAlpha = Math.max(0.0f, fadeOutAlpha); 416 417 // We interpolate the alpha of the targetView to 1 418 KeyguardAffordanceView otherView = targetView == mRightIcon ? mLeftIcon : mRightIcon; 419 updateIconAlpha(targetView, alpha + fadeOutAlpha * targetView.getRestingAlpha(), false); 420 updateIconAlpha(otherView, fadeOutAlpha * otherView.getRestingAlpha(), false); 421 updateIconAlpha(mCenterIcon, fadeOutAlpha * mCenterIcon.getRestingAlpha(), false); 422 } 423 424 private float getTranslationFromRadius(float circleSize) { 425 float translation = (circleSize - mMinBackgroundRadius) 426 / BACKGROUND_RADIUS_SCALE_FACTOR; 427 return translation > 0.0f ? translation + mTouchSlop : 0.0f; 428 } 429 430 private float getRadiusFromTranslation(float translation) { 431 if (translation <= mTouchSlop) { 432 return 0.0f; 433 } 434 return (translation - mTouchSlop) * BACKGROUND_RADIUS_SCALE_FACTOR + mMinBackgroundRadius; 435 } 436 437 public void animateHideLeftRightIcon() { 438 cancelAnimation(); 439 updateIcon(mRightIcon, 0f, 0f, true, false, false, false); 440 updateIcon(mLeftIcon, 0f, 0f, true, false, false, false); 441 } 442 443 private void updateIcon(KeyguardAffordanceView view, float circleRadius, float alpha, 444 boolean animate, boolean slowRadiusAnimation, boolean force, 445 boolean forceNoCircleAnimation) { 446 if (view.getVisibility() != View.VISIBLE && !force) { 447 return; 448 } 449 if (forceNoCircleAnimation) { 450 view.setCircleRadiusWithoutAnimation(circleRadius); 451 } else { 452 view.setCircleRadius(circleRadius, slowRadiusAnimation); 453 } 454 updateIconAlpha(view, alpha, animate); 455 } 456 457 private void updateIconAlpha(KeyguardAffordanceView view, float alpha, boolean animate) { 458 float scale = getScale(alpha, view); 459 alpha = Math.min(1.0f, alpha); 460 view.setImageAlpha(alpha, animate); 461 view.setImageScale(scale, animate); 462 } 463 464 private float getScale(float alpha, KeyguardAffordanceView icon) { 465 float scale = alpha / icon.getRestingAlpha() * 0.2f + 466 KeyguardAffordanceView.MIN_ICON_SCALE_AMOUNT; 467 return Math.min(scale, KeyguardAffordanceView.MAX_ICON_SCALE_AMOUNT); 468 } 469 470 private void trackMovement(MotionEvent event) { 471 if (mVelocityTracker != null) { 472 mVelocityTracker.addMovement(event); 473 } 474 } 475 476 private void initVelocityTracker() { 477 if (mVelocityTracker != null) { 478 mVelocityTracker.recycle(); 479 } 480 mVelocityTracker = VelocityTracker.obtain(); 481 } 482 483 private float getCurrentVelocity(float lastX, float lastY) { 484 if (mVelocityTracker == null) { 485 return 0; 486 } 487 mVelocityTracker.computeCurrentVelocity(1000); 488 float aX = mVelocityTracker.getXVelocity(); 489 float aY = mVelocityTracker.getYVelocity(); 490 float bX = lastX - mInitialTouchX; 491 float bY = lastY - mInitialTouchY; 492 float bLen = (float) Math.hypot(bX, bY); 493 // Project the velocity onto the distance vector: a * b / |b| 494 float projectedVelocity = (aX * bX + aY * bY) / bLen; 495 if (mTargetedView == mRightIcon) { 496 projectedVelocity = -projectedVelocity; 497 } 498 return projectedVelocity; 499 } 500 501 public void onConfigurationChanged() { 502 initDimens(); 503 initIcons(); 504 } 505 506 public void onRtlPropertiesChanged() { 507 initIcons(); 508 } 509 510 public void reset(boolean animate) { 511 cancelAnimation(); 512 setTranslation(0.0f, true, animate); 513 mMotionCancelled = true; 514 if (mSwipingInProgress) { 515 mCallback.onSwipingAborted(); 516 mSwipingInProgress = false; 517 } 518 } 519 520 public boolean isSwipingInProgress() { 521 return mSwipingInProgress; 522 } 523 524 public void launchAffordance(boolean animate, boolean left) { 525 if (mSwipingInProgress) { 526 // We don't want to mess with the state if the user is actually swiping already. 527 return; 528 } 529 KeyguardAffordanceView targetView = left ? mLeftIcon : mRightIcon; 530 KeyguardAffordanceView otherView = left ? mRightIcon : mLeftIcon; 531 startSwiping(targetView); 532 if (animate) { 533 fling(0, false, !left); 534 updateIcon(otherView, 0.0f, 0, true, false, true, false); 535 updateIcon(mCenterIcon, 0.0f, 0, true, false, true, false); 536 } else { 537 mCallback.onAnimationToSideStarted(!left, mTranslation, 0); 538 mTranslation = left ? mCallback.getMaxTranslationDistance() 539 : mCallback.getMaxTranslationDistance(); 540 updateIcon(mCenterIcon, 0.0f, 0.0f, false, false, true, false); 541 updateIcon(otherView, 0.0f, 0.0f, false, false, true, false); 542 targetView.instantFinishAnimation(); 543 mFlingEndListener.onAnimationEnd(null); 544 mAnimationEndRunnable.run(); 545 } 546 } 547 548 public interface Callback { 549 550 /** 551 * Notifies the callback when an animation to a side page was started. 552 * 553 * @param rightPage Is the page animated to the right page? 554 */ 555 void onAnimationToSideStarted(boolean rightPage, float translation, float vel); 556 557 /** 558 * Notifies the callback the animation to a side page has ended. 559 */ 560 void onAnimationToSideEnded(); 561 562 float getMaxTranslationDistance(); 563 564 void onSwipingStarted(boolean rightIcon); 565 566 void onSwipingAborted(); 567 568 void onIconClicked(boolean rightIcon); 569 570 KeyguardAffordanceView getLeftIcon(); 571 572 KeyguardAffordanceView getCenterIcon(); 573 574 KeyguardAffordanceView getRightIcon(); 575 576 View getLeftPreview(); 577 578 View getRightPreview(); 579 580 /** 581 * @return The factor the minimum swipe amount should be multiplied with. 582 */ 583 float getAffordanceFalsingFactor(); 584 585 boolean needsAntiFalsing(); 586 } 587 } 588