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