KeyguardAffordanceHelper.java revision 0a8182227249df6d8a76e19886f762b80a046e76
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 17package com.android.systemui.statusbar.phone; 18 19import android.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.animation.ValueAnimator; 22import android.content.Context; 23import android.view.MotionEvent; 24import android.view.VelocityTracker; 25import android.view.View; 26import android.view.ViewConfiguration; 27import android.view.animation.AnimationUtils; 28import android.view.animation.Interpolator; 29 30import com.android.systemui.R; 31import com.android.systemui.statusbar.FlingAnimationUtils; 32import com.android.systemui.statusbar.KeyguardAffordanceView; 33 34/** 35 * A touch handler of the keyguard which is responsible for launching phone and camera affordances. 36 */ 37public 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.15f; 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 int mTrackingPointer; 50 private VelocityTracker mVelocityTracker; 51 private boolean mSwipingInProgress; 52 private float mInitialTouchX; 53 private float mInitialTouchY; 54 private float mTranslation; 55 private float mTranslationOnDown; 56 private int mTouchSlop; 57 private int mMinTranslationAmount; 58 private int mMinFlingVelocity; 59 private int mHintGrowAmount; 60 private KeyguardAffordanceView mLeftIcon; 61 private KeyguardAffordanceView mCenterIcon; 62 private KeyguardAffordanceView mRightIcon; 63 private Interpolator mAppearInterpolator; 64 private Interpolator mDisappearInterpolator; 65 private Animator mSwipeAnimator; 66 private int mMinBackgroundRadius; 67 private boolean mMotionPerformedByUser; 68 private AnimatorListenerAdapter mFlingEndListener = new AnimatorListenerAdapter() { 69 @Override 70 public void onAnimationEnd(Animator animation) { 71 mSwipeAnimator = null; 72 setSwipingInProgress(false); 73 } 74 }; 75 private Runnable mAnimationEndRunnable = new Runnable() { 76 @Override 77 public void run() { 78 mCallback.onAnimationToSideEnded(); 79 } 80 }; 81 82 KeyguardAffordanceHelper(Callback callback, Context context) { 83 mContext = context; 84 mCallback = callback; 85 initIcons(); 86 updateIcon(mLeftIcon, 0.0f, SWIPE_RESTING_ALPHA_AMOUNT, false, false); 87 updateIcon(mCenterIcon, 0.0f, SWIPE_RESTING_ALPHA_AMOUNT, false, false); 88 updateIcon(mRightIcon, 0.0f, SWIPE_RESTING_ALPHA_AMOUNT, false, false); 89 initDimens(); 90 } 91 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 mHintGrowAmount = 101 mContext.getResources().getDimensionPixelSize(R.dimen.hint_grow_amount_sideways); 102 mFlingAnimationUtils = new FlingAnimationUtils(mContext, 0.4f); 103 mAppearInterpolator = AnimationUtils.loadInterpolator(mContext, 104 android.R.interpolator.linear_out_slow_in); 105 mDisappearInterpolator = AnimationUtils.loadInterpolator(mContext, 106 android.R.interpolator.fast_out_linear_in); 107 } 108 109 private void initIcons() { 110 mLeftIcon = mCallback.getLeftIcon(); 111 mLeftIcon.setIsLeft(true); 112 mCenterIcon = mCallback.getCenterIcon(); 113 mRightIcon = mCallback.getRightIcon(); 114 mRightIcon.setIsLeft(false); 115 mLeftIcon.setPreviewView(mCallback.getLeftPreview()); 116 mRightIcon.setPreviewView(mCallback.getRightPreview()); 117 } 118 119 public boolean onTouchEvent(MotionEvent event) { 120 int pointerIndex = event.findPointerIndex(mTrackingPointer); 121 if (pointerIndex < 0) { 122 pointerIndex = 0; 123 mTrackingPointer = event.getPointerId(pointerIndex); 124 } 125 final float y = event.getY(pointerIndex); 126 final float x = event.getX(pointerIndex); 127 128 boolean isUp = false; 129 switch (event.getActionMasked()) { 130 case MotionEvent.ACTION_DOWN: 131 if (mSwipingInProgress) { 132 cancelAnimation(); 133 } 134 mInitialTouchY = y; 135 mInitialTouchX = x; 136 mTranslationOnDown = mTranslation; 137 initVelocityTracker(); 138 trackMovement(event); 139 mMotionPerformedByUser = false; 140 break; 141 142 case MotionEvent.ACTION_POINTER_UP: 143 final int upPointer = event.getPointerId(event.getActionIndex()); 144 if (mTrackingPointer == upPointer) { 145 // gesture is ongoing, find a new pointer to track 146 final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1; 147 final float newY = event.getY(newIndex); 148 final float newX = event.getX(newIndex); 149 mTrackingPointer = event.getPointerId(newIndex); 150 mInitialTouchY = newY; 151 mInitialTouchX = newX; 152 mTranslationOnDown = mTranslation; 153 } 154 break; 155 156 case MotionEvent.ACTION_MOVE: 157 final float w = x - mInitialTouchX; 158 trackMovement(event); 159 if (((leftSwipePossible() && w > mTouchSlop) 160 || (rightSwipePossible() && w < -mTouchSlop)) 161 && Math.abs(w) > Math.abs(y - mInitialTouchY) 162 && !mSwipingInProgress) { 163 cancelAnimation(); 164 mInitialTouchY = y; 165 mInitialTouchX = x; 166 mTranslationOnDown = mTranslation; 167 setSwipingInProgress(true); 168 } 169 if (mSwipingInProgress) { 170 setTranslation(mTranslationOnDown + x - mInitialTouchX, false, false); 171 } 172 break; 173 174 case MotionEvent.ACTION_UP: 175 isUp = true; 176 case MotionEvent.ACTION_CANCEL: 177 mTrackingPointer = -1; 178 trackMovement(event); 179 if (mSwipingInProgress) { 180 flingWithCurrentVelocity(!isUp); 181 } 182 if (mVelocityTracker != null) { 183 mVelocityTracker.recycle(); 184 mVelocityTracker = null; 185 } 186 break; 187 } 188 return true; 189 } 190 191 private void setSwipingInProgress(boolean inProgress) { 192 mSwipingInProgress = inProgress; 193 if (inProgress) { 194 mCallback.onSwipingStarted(); 195 } 196 } 197 198 private boolean rightSwipePossible() { 199 return mRightIcon.getVisibility() == View.VISIBLE; 200 } 201 202 private boolean leftSwipePossible() { 203 return mLeftIcon.getVisibility() == View.VISIBLE; 204 } 205 206 public boolean onInterceptTouchEvent(MotionEvent ev) { 207 return false; 208 } 209 210 public void startHintAnimation(boolean right, Runnable onFinishedListener) { 211 212 startHintAnimationPhase1(right, onFinishedListener); 213 } 214 215 private void startHintAnimationPhase1(final boolean right, final Runnable onFinishedListener) { 216 final KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon; 217 targetView.showArrow(true); 218 ValueAnimator animator = getAnimatorToRadius(right, mHintGrowAmount); 219 animator.addListener(new AnimatorListenerAdapter() { 220 private boolean mCancelled; 221 222 @Override 223 public void onAnimationCancel(Animator animation) { 224 mCancelled = true; 225 } 226 227 @Override 228 public void onAnimationEnd(Animator animation) { 229 if (mCancelled) { 230 mSwipeAnimator = null; 231 onFinishedListener.run(); 232 targetView.showArrow(false); 233 } else { 234 startUnlockHintAnimationPhase2(right, onFinishedListener); 235 } 236 } 237 }); 238 animator.setInterpolator(mAppearInterpolator); 239 animator.setDuration(HINT_PHASE1_DURATION); 240 animator.start(); 241 mSwipeAnimator = animator; 242 } 243 244 /** 245 * Phase 2: Move back. 246 */ 247 private void startUnlockHintAnimationPhase2(boolean right, final Runnable onFinishedListener) { 248 final KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon; 249 ValueAnimator animator = getAnimatorToRadius(right, 0); 250 animator.addListener(new AnimatorListenerAdapter() { 251 @Override 252 public void onAnimationEnd(Animator animation) { 253 mSwipeAnimator = null; 254 targetView.showArrow(false); 255 onFinishedListener.run(); 256 } 257 258 @Override 259 public void onAnimationStart(Animator animation) { 260 targetView.showArrow(false); 261 } 262 }); 263 animator.setInterpolator(mDisappearInterpolator); 264 animator.setDuration(HINT_PHASE2_DURATION); 265 animator.setStartDelay(HINT_CIRCLE_OPEN_DURATION); 266 animator.start(); 267 mSwipeAnimator = animator; 268 } 269 270 private ValueAnimator getAnimatorToRadius(final boolean right, int radius) { 271 final KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon; 272 ValueAnimator animator = ValueAnimator.ofFloat(targetView.getCircleRadius(), radius); 273 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 274 @Override 275 public void onAnimationUpdate(ValueAnimator animation) { 276 float newRadius = (float) animation.getAnimatedValue(); 277 targetView.setCircleRadiusWithoutAnimation(newRadius); 278 float translation = getTranslationFromRadius(newRadius); 279 mTranslation = right ? -translation : translation; 280 updateIconsFromRadius(targetView, newRadius); 281 } 282 }); 283 return animator; 284 } 285 286 private void cancelAnimation() { 287 if (mSwipeAnimator != null) { 288 mSwipeAnimator.cancel(); 289 } 290 } 291 292 private void flingWithCurrentVelocity(boolean forceSnapBack) { 293 float vel = getCurrentVelocity(); 294 295 // We snap back if the current translation is not far enough 296 boolean snapBack = isBelowFalsingThreshold(); 297 298 // or if the velocity is in the opposite direction. 299 boolean velIsInWrongDirection = vel * mTranslation < 0; 300 snapBack |= Math.abs(vel) > mMinFlingVelocity && velIsInWrongDirection; 301 vel = snapBack ^ velIsInWrongDirection ? 0 : vel; 302 fling(vel, snapBack || forceSnapBack); 303 } 304 305 private boolean isBelowFalsingThreshold() { 306 return Math.abs(mTranslation) < Math.abs(mTranslationOnDown) + getMinTranslationAmount(); 307 } 308 309 private int getMinTranslationAmount() { 310 float factor = mCallback.getAffordanceFalsingFactor(); 311 return (int) (mMinTranslationAmount * factor); 312 } 313 314 private void fling(float vel, final boolean snapBack) { 315 float target = mTranslation < 0 ? -mCallback.getPageWidth() : mCallback.getPageWidth(); 316 target = snapBack ? 0 : target; 317 318 ValueAnimator animator = ValueAnimator.ofFloat(mTranslation, target); 319 mFlingAnimationUtils.apply(animator, mTranslation, target, vel); 320 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 321 @Override 322 public void onAnimationUpdate(ValueAnimator animation) { 323 mTranslation = (float) animation.getAnimatedValue(); 324 } 325 }); 326 animator.addListener(mFlingEndListener); 327 if (!snapBack) { 328 startFinishingCircleAnimation(vel * 0.375f, mAnimationEndRunnable); 329 mCallback.onAnimationToSideStarted(mTranslation < 0); 330 } else { 331 reset(true); 332 } 333 animator.start(); 334 mSwipeAnimator = animator; 335 } 336 337 private void startFinishingCircleAnimation(float velocity, Runnable mAnimationEndRunnable) { 338 KeyguardAffordanceView targetView = mTranslation > 0 ? mLeftIcon : mRightIcon; 339 targetView.finishAnimation(velocity, mAnimationEndRunnable); 340 } 341 342 private void setTranslation(float translation, boolean isReset, boolean animateReset) { 343 translation = rightSwipePossible() ? translation : Math.max(0, translation); 344 translation = leftSwipePossible() ? translation : Math.min(0, translation); 345 float absTranslation = Math.abs(translation); 346 if (absTranslation > Math.abs(mTranslationOnDown) + getMinTranslationAmount() || 347 mMotionPerformedByUser) { 348 mMotionPerformedByUser = true; 349 } 350 if (translation != mTranslation || isReset) { 351 KeyguardAffordanceView targetView = translation > 0 ? mLeftIcon : mRightIcon; 352 KeyguardAffordanceView otherView = translation > 0 ? mRightIcon : mLeftIcon; 353 float alpha = absTranslation / getMinTranslationAmount(); 354 355 // We interpolate the alpha of the other icons to 0 356 float fadeOutAlpha = SWIPE_RESTING_ALPHA_AMOUNT * (1.0f - alpha); 357 fadeOutAlpha = Math.max(0.0f, fadeOutAlpha); 358 359 // We interpolate the alpha of the targetView to 1 360 alpha = fadeOutAlpha + alpha; 361 362 boolean animateIcons = isReset && animateReset; 363 float radius = getRadiusFromTranslation(absTranslation); 364 boolean slowAnimation = isReset && isBelowFalsingThreshold(); 365 if (!isReset) { 366 updateIcon(targetView, radius, alpha, false, false); 367 } else { 368 updateIcon(targetView, 0.0f, fadeOutAlpha, animateIcons, slowAnimation); 369 } 370 updateIcon(otherView, 0.0f, fadeOutAlpha, animateIcons, slowAnimation); 371 updateIcon(mCenterIcon, 0.0f, fadeOutAlpha, animateIcons, slowAnimation); 372 373 mTranslation = translation; 374 } 375 } 376 377 private void updateIconsFromRadius(KeyguardAffordanceView targetView, float newRadius) { 378 float alpha = newRadius / mMinBackgroundRadius; 379 380 // We interpolate the alpha of the other icons to 0 381 float fadeOutAlpha = SWIPE_RESTING_ALPHA_AMOUNT * (1.0f - alpha); 382 fadeOutAlpha = Math.max(0.0f, fadeOutAlpha); 383 384 // We interpolate the alpha of the targetView to 1 385 alpha = fadeOutAlpha + alpha; 386 KeyguardAffordanceView otherView = targetView == mRightIcon ? mLeftIcon : mRightIcon; 387 updateIconAlpha(targetView, alpha, false); 388 updateIconAlpha(otherView, fadeOutAlpha, false); 389 updateIconAlpha(mCenterIcon, fadeOutAlpha, false); 390 } 391 392 private float getTranslationFromRadius(float circleSize) { 393 float translation = (circleSize - mMinBackgroundRadius) / BACKGROUND_RADIUS_SCALE_FACTOR; 394 return Math.max(0, translation); 395 } 396 397 private float getRadiusFromTranslation(float translation) { 398 return translation * BACKGROUND_RADIUS_SCALE_FACTOR + mMinBackgroundRadius; 399 } 400 401 public void animateHideLeftRightIcon() { 402 updateIcon(mRightIcon, 0f, 0f, true, false); 403 updateIcon(mLeftIcon, 0f, 0f, true, false); 404 } 405 406 private void updateIcon(KeyguardAffordanceView view, float circleRadius, float alpha, 407 boolean animate, boolean slowRadiusAnimation) { 408 if (view.getVisibility() != View.VISIBLE) { 409 return; 410 } 411 view.setCircleRadius(circleRadius, slowRadiusAnimation); 412 updateIconAlpha(view, alpha, animate); 413 } 414 415 private void updateIconAlpha(KeyguardAffordanceView view, float alpha, boolean animate) { 416 float scale = getScale(alpha); 417 alpha = Math.min(1.0f, alpha); 418 view.setImageAlpha(alpha, animate); 419 view.setImageScale(scale, animate); 420 } 421 422 private float getScale(float alpha) { 423 float scale = alpha / SWIPE_RESTING_ALPHA_AMOUNT * 0.2f + 424 KeyguardAffordanceView.MIN_ICON_SCALE_AMOUNT; 425 return Math.min(scale, KeyguardAffordanceView.MAX_ICON_SCALE_AMOUNT); 426 } 427 428 private void trackMovement(MotionEvent event) { 429 if (mVelocityTracker != null) { 430 mVelocityTracker.addMovement(event); 431 } 432 } 433 434 private void initVelocityTracker() { 435 if (mVelocityTracker != null) { 436 mVelocityTracker.recycle(); 437 } 438 mVelocityTracker = VelocityTracker.obtain(); 439 } 440 441 private float getCurrentVelocity() { 442 if (mVelocityTracker == null) { 443 return 0; 444 } 445 mVelocityTracker.computeCurrentVelocity(1000); 446 return mVelocityTracker.getXVelocity(); 447 } 448 449 public void onConfigurationChanged() { 450 initDimens(); 451 initIcons(); 452 } 453 454 public void onRtlPropertiesChanged() { 455 initIcons(); 456 } 457 458 public void reset(boolean animate) { 459 if (mSwipeAnimator != null) { 460 mSwipeAnimator.cancel(); 461 } 462 setTranslation(0.0f, true, animate); 463 setSwipingInProgress(false); 464 } 465 466 public interface Callback { 467 468 /** 469 * Notifies the callback when an animation to a side page was started. 470 * 471 * @param rightPage Is the page animated to the right page? 472 */ 473 void onAnimationToSideStarted(boolean rightPage); 474 475 /** 476 * Notifies the callback the animation to a side page has ended. 477 */ 478 void onAnimationToSideEnded(); 479 480 float getPageWidth(); 481 482 void onSwipingStarted(); 483 484 KeyguardAffordanceView getLeftIcon(); 485 486 KeyguardAffordanceView getCenterIcon(); 487 488 KeyguardAffordanceView getRightIcon(); 489 490 View getLeftPreview(); 491 492 View getRightPreview(); 493 494 /** 495 * @return The factor the minimum swipe amount should be multiplied with. 496 */ 497 float getAffordanceFalsingFactor(); 498 } 499} 500