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