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 import android.view.animation.AnimationUtils; 28 import android.view.animation.Interpolator; 29 30 import com.android.systemui.R; 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 47 private FlingAnimationUtils mFlingAnimationUtils; 48 private Callback mCallback; 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 Interpolator mAppearInterpolator; 63 private Interpolator mDisappearInterpolator; 64 private Animator mSwipeAnimator; 65 private int mMinBackgroundRadius; 66 private boolean mMotionCancelled; 67 private int mTouchTargetSize; 68 private View mTargetedView; 69 private boolean mTouchSlopExeeded; 70 private AnimatorListenerAdapter mFlingEndListener = new AnimatorListenerAdapter() { 71 @Override 72 public void onAnimationEnd(Animator animation) { 73 mSwipeAnimator = null; 74 mSwipingInProgress = false; 75 mTargetedView = null; 76 } 77 }; 78 private Runnable mAnimationEndRunnable = new Runnable() { 79 @Override 80 public void run() { 81 mCallback.onAnimationToSideEnded(); 82 } 83 }; 84 KeyguardAffordanceHelper(Callback callback, Context context)85 KeyguardAffordanceHelper(Callback callback, Context context) { 86 mContext = context; 87 mCallback = callback; 88 initIcons(); 89 updateIcon(mLeftIcon, 0.0f, mLeftIcon.getRestingAlpha(), false, false, true); 90 updateIcon(mCenterIcon, 0.0f, mCenterIcon.getRestingAlpha(), false, false, true); 91 updateIcon(mRightIcon, 0.0f, mRightIcon.getRestingAlpha(), false, false, true); 92 initDimens(); 93 } 94 initDimens()95 private void initDimens() { 96 final ViewConfiguration configuration = ViewConfiguration.get(mContext); 97 mTouchSlop = configuration.getScaledPagingTouchSlop(); 98 mMinFlingVelocity = configuration.getScaledMinimumFlingVelocity(); 99 mMinTranslationAmount = mContext.getResources().getDimensionPixelSize( 100 R.dimen.keyguard_min_swipe_amount); 101 mMinBackgroundRadius = mContext.getResources().getDimensionPixelSize( 102 R.dimen.keyguard_affordance_min_background_radius); 103 mTouchTargetSize = mContext.getResources().getDimensionPixelSize( 104 R.dimen.keyguard_affordance_touch_target_size); 105 mHintGrowAmount = 106 mContext.getResources().getDimensionPixelSize(R.dimen.hint_grow_amount_sideways); 107 mFlingAnimationUtils = new FlingAnimationUtils(mContext, 0.4f); 108 mAppearInterpolator = AnimationUtils.loadInterpolator(mContext, 109 android.R.interpolator.linear_out_slow_in); 110 mDisappearInterpolator = AnimationUtils.loadInterpolator(mContext, 111 android.R.interpolator.fast_out_linear_in); 112 } 113 initIcons()114 private void initIcons() { 115 mLeftIcon = mCallback.getLeftIcon(); 116 mCenterIcon = mCallback.getCenterIcon(); 117 mRightIcon = mCallback.getRightIcon(); 118 updatePreviews(); 119 } 120 updatePreviews()121 public void updatePreviews() { 122 mLeftIcon.setPreviewView(mCallback.getLeftPreview()); 123 mRightIcon.setPreviewView(mCallback.getRightPreview()); 124 } 125 onTouchEvent(MotionEvent event)126 public boolean onTouchEvent(MotionEvent event) { 127 int action = event.getActionMasked(); 128 if (mMotionCancelled && action != MotionEvent.ACTION_DOWN) { 129 return false; 130 } 131 final float y = event.getY(); 132 final float x = event.getX(); 133 134 boolean isUp = false; 135 switch (action) { 136 case MotionEvent.ACTION_DOWN: 137 View targetView = getIconAtPosition(x, y); 138 if (targetView == null || (mTargetedView != null && mTargetedView != targetView)) { 139 mMotionCancelled = true; 140 return false; 141 } 142 if (mTargetedView != null) { 143 cancelAnimation(); 144 } else { 145 mTouchSlopExeeded = false; 146 } 147 mCallback.onSwipingStarted(targetView == mRightIcon); 148 mSwipingInProgress = true; 149 mTargetedView = targetView; 150 mInitialTouchX = x; 151 mInitialTouchY = y; 152 mTranslationOnDown = mTranslation; 153 initVelocityTracker(); 154 trackMovement(event); 155 mMotionCancelled = false; 156 break; 157 case MotionEvent.ACTION_POINTER_DOWN: 158 mMotionCancelled = true; 159 endMotion(true /* forceSnapBack */, x, y); 160 break; 161 case MotionEvent.ACTION_MOVE: 162 trackMovement(event); 163 float xDist = x - mInitialTouchX; 164 float yDist = y - mInitialTouchY; 165 float distance = (float) Math.hypot(xDist, yDist); 166 if (!mTouchSlopExeeded && distance > mTouchSlop) { 167 mTouchSlopExeeded = true; 168 } 169 if (mSwipingInProgress) { 170 if (mTargetedView == mRightIcon) { 171 distance = mTranslationOnDown - distance; 172 distance = Math.min(0, distance); 173 } else { 174 distance = mTranslationOnDown + distance; 175 distance = Math.max(0, distance); 176 } 177 setTranslation(distance, false /* isReset */, false /* animateReset */); 178 } 179 break; 180 181 case MotionEvent.ACTION_UP: 182 isUp = true; 183 case MotionEvent.ACTION_CANCEL: 184 boolean hintOnTheRight = mTargetedView == mRightIcon; 185 trackMovement(event); 186 endMotion(!isUp, x, y); 187 if (!mTouchSlopExeeded && isUp) { 188 mCallback.onIconClicked(hintOnTheRight); 189 } 190 break; 191 } 192 return true; 193 } 194 getIconAtPosition(float x, float y)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 isOnAffordanceIcon(float x, float y)205 public boolean isOnAffordanceIcon(float x, float y) { 206 return isOnIcon(mLeftIcon, x, y) || isOnIcon(mRightIcon, x, y); 207 } 208 isOnIcon(View icon, float x, float y)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 endMotion(boolean forceSnapBack, float lastX, float lastY)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 rightSwipePossible()228 private boolean rightSwipePossible() { 229 return mRightIcon.getVisibility() == View.VISIBLE; 230 } 231 leftSwipePossible()232 private boolean leftSwipePossible() { 233 return mLeftIcon.getVisibility() == View.VISIBLE; 234 } 235 onInterceptTouchEvent(MotionEvent ev)236 public boolean onInterceptTouchEvent(MotionEvent ev) { 237 return false; 238 } 239 startHintAnimation(boolean right, Runnable onFinishedListener)240 public void startHintAnimation(boolean right, 241 Runnable onFinishedListener) { 242 cancelAnimation(); 243 startHintAnimationPhase1(right, onFinishedListener); 244 } 245 startHintAnimationPhase1(final boolean right, final Runnable onFinishedListener)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(mAppearInterpolator); 269 animator.setDuration(HINT_PHASE1_DURATION); 270 animator.start(); 271 mSwipeAnimator = animator; 272 mTargetedView = targetView; 273 } 274 275 /** 276 * Phase 2: Move back. 277 */ startUnlockHintAnimationPhase2(boolean right, final Runnable onFinishedListener)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(mDisappearInterpolator); 289 animator.setDuration(HINT_PHASE2_DURATION); 290 animator.setStartDelay(HINT_CIRCLE_OPEN_DURATION); 291 animator.start(); 292 mSwipeAnimator = animator; 293 } 294 getAnimatorToRadius(final boolean right, int radius)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 cancelAnimation()311 private void cancelAnimation() { 312 if (mSwipeAnimator != null) { 313 mSwipeAnimator.cancel(); 314 } 315 } 316 flingWithCurrentVelocity(boolean forceSnapBack, float lastX, float lastY)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 = 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); 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)339 private void fling(float vel, final boolean snapBack) { 340 float target = mTranslation < 0 341 ? -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); 356 mCallback.onAnimationToSideStarted(mTranslation < 0, mTranslation, vel); 357 } else { 358 reset(true); 359 } 360 animator.start(); 361 mSwipeAnimator = animator; 362 if (snapBack) { 363 mCallback.onSwipingAborted(); 364 } 365 } 366 367 private void startFinishingCircleAnimation(float velocity, Runnable mAnimationEndRunnable) { 368 KeyguardAffordanceView targetView = mTranslation > 0 ? mLeftIcon : mRightIcon; 369 targetView.finishAnimation(velocity, mAnimationEndRunnable); 370 } 371 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 float radius = getRadiusFromTranslation(absTranslation); 387 boolean slowAnimation = isReset && isBelowFalsingThreshold(); 388 if (!isReset) { 389 updateIcon(targetView, radius, alpha + fadeOutAlpha * targetView.getRestingAlpha(), 390 false, false, false); 391 } else { 392 updateIcon(targetView, 0.0f, fadeOutAlpha * targetView.getRestingAlpha(), 393 animateIcons, slowAnimation, false); 394 } 395 updateIcon(otherView, 0.0f, fadeOutAlpha * otherView.getRestingAlpha(), 396 animateIcons, slowAnimation, false); 397 updateIcon(mCenterIcon, 0.0f, fadeOutAlpha * mCenterIcon.getRestingAlpha(), 398 animateIcons, slowAnimation, false); 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 updateIconAlpha(mCenterIcon, fadeOutAlpha * mCenterIcon.getRestingAlpha(), false); 417 } 418 getTranslationFromRadius(float circleSize)419 private float getTranslationFromRadius(float circleSize) { 420 float translation = (circleSize - mMinBackgroundRadius) 421 / BACKGROUND_RADIUS_SCALE_FACTOR; 422 return translation > 0.0f ? translation + mTouchSlop : 0.0f; 423 } 424 getRadiusFromTranslation(float translation)425 private float getRadiusFromTranslation(float translation) { 426 if (translation <= mTouchSlop) { 427 return 0.0f; 428 } 429 return (translation - mTouchSlop) * BACKGROUND_RADIUS_SCALE_FACTOR + mMinBackgroundRadius; 430 } 431 animateHideLeftRightIcon()432 public void animateHideLeftRightIcon() { 433 cancelAnimation(); 434 updateIcon(mRightIcon, 0f, 0f, true, false, false); 435 updateIcon(mLeftIcon, 0f, 0f, true, false, false); 436 } 437 updateIcon(KeyguardAffordanceView view, float circleRadius, float alpha, boolean animate, boolean slowRadiusAnimation, boolean force)438 private void updateIcon(KeyguardAffordanceView view, float circleRadius, float alpha, 439 boolean animate, boolean slowRadiusAnimation, boolean force) { 440 if (view.getVisibility() != View.VISIBLE && !force) { 441 return; 442 } 443 view.setCircleRadius(circleRadius, slowRadiusAnimation); 444 updateIconAlpha(view, alpha, animate); 445 } 446 updateIconAlpha(KeyguardAffordanceView view, float alpha, boolean animate)447 private void updateIconAlpha(KeyguardAffordanceView view, float alpha, boolean animate) { 448 float scale = getScale(alpha, view); 449 alpha = Math.min(1.0f, alpha); 450 view.setImageAlpha(alpha, animate); 451 view.setImageScale(scale, animate); 452 } 453 getScale(float alpha, KeyguardAffordanceView icon)454 private float getScale(float alpha, KeyguardAffordanceView icon) { 455 float scale = alpha / icon.getRestingAlpha() * 0.2f + 456 KeyguardAffordanceView.MIN_ICON_SCALE_AMOUNT; 457 return Math.min(scale, KeyguardAffordanceView.MAX_ICON_SCALE_AMOUNT); 458 } 459 trackMovement(MotionEvent event)460 private void trackMovement(MotionEvent event) { 461 if (mVelocityTracker != null) { 462 mVelocityTracker.addMovement(event); 463 } 464 } 465 initVelocityTracker()466 private void initVelocityTracker() { 467 if (mVelocityTracker != null) { 468 mVelocityTracker.recycle(); 469 } 470 mVelocityTracker = VelocityTracker.obtain(); 471 } 472 getCurrentVelocity(float lastX, float lastY)473 private float getCurrentVelocity(float lastX, float lastY) { 474 if (mVelocityTracker == null) { 475 return 0; 476 } 477 mVelocityTracker.computeCurrentVelocity(1000); 478 float aX = mVelocityTracker.getXVelocity(); 479 float aY = mVelocityTracker.getYVelocity(); 480 float bX = lastX - mInitialTouchX; 481 float bY = lastY - mInitialTouchY; 482 float bLen = (float) Math.hypot(bX, bY); 483 // Project the velocity onto the distance vector: a * b / |b| 484 float projectedVelocity = (aX * bX + aY * bY) / bLen; 485 if (mTargetedView == mRightIcon) { 486 projectedVelocity = -projectedVelocity; 487 } 488 return projectedVelocity; 489 } 490 onConfigurationChanged()491 public void onConfigurationChanged() { 492 initDimens(); 493 initIcons(); 494 } 495 onRtlPropertiesChanged()496 public void onRtlPropertiesChanged() { 497 initIcons(); 498 } 499 reset(boolean animate)500 public void reset(boolean animate) { 501 cancelAnimation(); 502 setTranslation(0.0f, true, animate); 503 mMotionCancelled = true; 504 if (mSwipingInProgress) { 505 mCallback.onSwipingAborted(); 506 } 507 mSwipingInProgress = false; 508 } 509 510 public interface Callback { 511 512 /** 513 * Notifies the callback when an animation to a side page was started. 514 * 515 * @param rightPage Is the page animated to the right page? 516 */ onAnimationToSideStarted(boolean rightPage, float translation, float vel)517 void onAnimationToSideStarted(boolean rightPage, float translation, float vel); 518 519 /** 520 * Notifies the callback the animation to a side page has ended. 521 */ onAnimationToSideEnded()522 void onAnimationToSideEnded(); 523 getMaxTranslationDistance()524 float getMaxTranslationDistance(); 525 onSwipingStarted(boolean rightIcon)526 void onSwipingStarted(boolean rightIcon); 527 onSwipingAborted()528 void onSwipingAborted(); 529 onIconClicked(boolean rightIcon)530 void onIconClicked(boolean rightIcon); 531 getLeftIcon()532 KeyguardAffordanceView getLeftIcon(); 533 getCenterIcon()534 KeyguardAffordanceView getCenterIcon(); 535 getRightIcon()536 KeyguardAffordanceView getRightIcon(); 537 getLeftPreview()538 View getLeftPreview(); 539 getRightPreview()540 View getRightPreview(); 541 542 /** 543 * @return The factor the minimum swipe amount should be multiplied with. 544 */ getAffordanceFalsingFactor()545 float getAffordanceFalsingFactor(); 546 } 547 } 548