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