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.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 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 mMotionPerformedByUser; 67 private boolean mMotionCancelled; 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 if (mMotionCancelled && event.getActionMasked() != 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 (event.getActionMasked()) { 128 case MotionEvent.ACTION_DOWN: 129 if (mSwipingInProgress) { 130 cancelAnimation(); 131 } 132 mInitialTouchY = y; 133 mInitialTouchX = x; 134 mTranslationOnDown = mTranslation; 135 initVelocityTracker(); 136 trackMovement(event); 137 mMotionPerformedByUser = false; 138 mMotionCancelled = false; 139 break; 140 case MotionEvent.ACTION_POINTER_DOWN: 141 mMotionCancelled = true; 142 endMotion(event, true /* forceSnapBack */); 143 break; 144 case MotionEvent.ACTION_MOVE: 145 final float w = x - mInitialTouchX; 146 trackMovement(event); 147 if (((leftSwipePossible() && w > mTouchSlop) 148 || (rightSwipePossible() && w < -mTouchSlop)) 149 && Math.abs(w) > Math.abs(y - mInitialTouchY) 150 && !mSwipingInProgress) { 151 cancelAnimation(); 152 mInitialTouchY = y; 153 mInitialTouchX = x; 154 mTranslationOnDown = mTranslation; 155 setSwipingInProgress(true); 156 } 157 if (mSwipingInProgress) { 158 setTranslation(mTranslationOnDown + x - mInitialTouchX, false, false); 159 } 160 break; 161 162 case MotionEvent.ACTION_UP: 163 isUp = true; 164 case MotionEvent.ACTION_CANCEL: 165 trackMovement(event); 166 endMotion(event, !isUp); 167 break; 168 } 169 return true; 170 } 171 172 private void endMotion(MotionEvent event, boolean forceSnapBack) { 173 if (mSwipingInProgress) { 174 flingWithCurrentVelocity(forceSnapBack); 175 } 176 if (mVelocityTracker != null) { 177 mVelocityTracker.recycle(); 178 mVelocityTracker = null; 179 } 180 } 181 182 private void setSwipingInProgress(boolean inProgress) { 183 mSwipingInProgress = inProgress; 184 if (inProgress) { 185 mCallback.onSwipingStarted(); 186 } 187 } 188 189 private boolean rightSwipePossible() { 190 return mRightIcon.getVisibility() == View.VISIBLE; 191 } 192 193 private boolean leftSwipePossible() { 194 return mLeftIcon.getVisibility() == View.VISIBLE; 195 } 196 197 public boolean onInterceptTouchEvent(MotionEvent ev) { 198 return false; 199 } 200 201 public void startHintAnimation(boolean right, Runnable onFinishedListener) { 202 203 startHintAnimationPhase1(right, onFinishedListener); 204 } 205 206 private void startHintAnimationPhase1(final boolean right, final Runnable onFinishedListener) { 207 final KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon; 208 targetView.showArrow(true); 209 ValueAnimator animator = getAnimatorToRadius(right, mHintGrowAmount); 210 animator.addListener(new AnimatorListenerAdapter() { 211 private boolean mCancelled; 212 213 @Override 214 public void onAnimationCancel(Animator animation) { 215 mCancelled = true; 216 } 217 218 @Override 219 public void onAnimationEnd(Animator animation) { 220 if (mCancelled) { 221 mSwipeAnimator = null; 222 onFinishedListener.run(); 223 targetView.showArrow(false); 224 } else { 225 startUnlockHintAnimationPhase2(right, onFinishedListener); 226 } 227 } 228 }); 229 animator.setInterpolator(mAppearInterpolator); 230 animator.setDuration(HINT_PHASE1_DURATION); 231 animator.start(); 232 mSwipeAnimator = animator; 233 } 234 235 /** 236 * Phase 2: Move back. 237 */ 238 private void startUnlockHintAnimationPhase2(boolean right, final Runnable onFinishedListener) { 239 final KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon; 240 ValueAnimator animator = getAnimatorToRadius(right, 0); 241 animator.addListener(new AnimatorListenerAdapter() { 242 @Override 243 public void onAnimationEnd(Animator animation) { 244 mSwipeAnimator = null; 245 targetView.showArrow(false); 246 onFinishedListener.run(); 247 } 248 249 @Override 250 public void onAnimationStart(Animator animation) { 251 targetView.showArrow(false); 252 } 253 }); 254 animator.setInterpolator(mDisappearInterpolator); 255 animator.setDuration(HINT_PHASE2_DURATION); 256 animator.setStartDelay(HINT_CIRCLE_OPEN_DURATION); 257 animator.start(); 258 mSwipeAnimator = animator; 259 } 260 261 private ValueAnimator getAnimatorToRadius(final boolean right, int radius) { 262 final KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon; 263 ValueAnimator animator = ValueAnimator.ofFloat(targetView.getCircleRadius(), radius); 264 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 265 @Override 266 public void onAnimationUpdate(ValueAnimator animation) { 267 float newRadius = (float) animation.getAnimatedValue(); 268 targetView.setCircleRadiusWithoutAnimation(newRadius); 269 float translation = getTranslationFromRadius(newRadius); 270 mTranslation = right ? -translation : translation; 271 updateIconsFromRadius(targetView, newRadius); 272 } 273 }); 274 return animator; 275 } 276 277 private void cancelAnimation() { 278 if (mSwipeAnimator != null) { 279 mSwipeAnimator.cancel(); 280 } 281 } 282 283 private void flingWithCurrentVelocity(boolean forceSnapBack) { 284 float vel = getCurrentVelocity(); 285 286 // We snap back if the current translation is not far enough 287 boolean snapBack = isBelowFalsingThreshold(); 288 289 // or if the velocity is in the opposite direction. 290 boolean velIsInWrongDirection = vel * mTranslation < 0; 291 snapBack |= Math.abs(vel) > mMinFlingVelocity && velIsInWrongDirection; 292 vel = snapBack ^ velIsInWrongDirection ? 0 : vel; 293 fling(vel, snapBack || forceSnapBack); 294 } 295 296 private boolean isBelowFalsingThreshold() { 297 return Math.abs(mTranslation) < Math.abs(mTranslationOnDown) + getMinTranslationAmount(); 298 } 299 300 private int getMinTranslationAmount() { 301 float factor = mCallback.getAffordanceFalsingFactor(); 302 return (int) (mMinTranslationAmount * factor); 303 } 304 305 private void fling(float vel, final boolean snapBack) { 306 float target = mTranslation < 0 ? -mCallback.getPageWidth() : mCallback.getPageWidth(); 307 target = snapBack ? 0 : target; 308 309 ValueAnimator animator = ValueAnimator.ofFloat(mTranslation, target); 310 mFlingAnimationUtils.apply(animator, mTranslation, target, vel); 311 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 312 @Override 313 public void onAnimationUpdate(ValueAnimator animation) { 314 mTranslation = (float) animation.getAnimatedValue(); 315 } 316 }); 317 animator.addListener(mFlingEndListener); 318 if (!snapBack) { 319 startFinishingCircleAnimation(vel * 0.375f, mAnimationEndRunnable); 320 mCallback.onAnimationToSideStarted(mTranslation < 0, mTranslation, vel); 321 } else { 322 reset(true); 323 } 324 animator.start(); 325 mSwipeAnimator = animator; 326 } 327 328 private void startFinishingCircleAnimation(float velocity, Runnable mAnimationEndRunnable) { 329 KeyguardAffordanceView targetView = mTranslation > 0 ? mLeftIcon : mRightIcon; 330 targetView.finishAnimation(velocity, mAnimationEndRunnable); 331 } 332 333 private void setTranslation(float translation, boolean isReset, boolean animateReset) { 334 translation = rightSwipePossible() ? translation : Math.max(0, translation); 335 translation = leftSwipePossible() ? translation : Math.min(0, translation); 336 float absTranslation = Math.abs(translation); 337 if (absTranslation > Math.abs(mTranslationOnDown) + getMinTranslationAmount() || 338 mMotionPerformedByUser) { 339 mMotionPerformedByUser = true; 340 } 341 if (translation != mTranslation || isReset) { 342 KeyguardAffordanceView targetView = translation > 0 ? mLeftIcon : mRightIcon; 343 KeyguardAffordanceView otherView = translation > 0 ? mRightIcon : mLeftIcon; 344 float alpha = absTranslation / getMinTranslationAmount(); 345 346 // We interpolate the alpha of the other icons to 0 347 float fadeOutAlpha = SWIPE_RESTING_ALPHA_AMOUNT * (1.0f - alpha); 348 fadeOutAlpha = Math.max(0.0f, fadeOutAlpha); 349 350 // We interpolate the alpha of the targetView to 1 351 alpha = fadeOutAlpha + alpha; 352 353 boolean animateIcons = isReset && animateReset; 354 float radius = getRadiusFromTranslation(absTranslation); 355 boolean slowAnimation = isReset && isBelowFalsingThreshold(); 356 if (!isReset) { 357 updateIcon(targetView, radius, alpha, false, false); 358 } else { 359 updateIcon(targetView, 0.0f, fadeOutAlpha, animateIcons, slowAnimation); 360 } 361 updateIcon(otherView, 0.0f, fadeOutAlpha, animateIcons, slowAnimation); 362 updateIcon(mCenterIcon, 0.0f, fadeOutAlpha, animateIcons, slowAnimation); 363 364 mTranslation = translation; 365 } 366 } 367 368 private void updateIconsFromRadius(KeyguardAffordanceView targetView, float newRadius) { 369 float alpha = newRadius / mMinBackgroundRadius; 370 371 // We interpolate the alpha of the other icons to 0 372 float fadeOutAlpha = SWIPE_RESTING_ALPHA_AMOUNT * (1.0f - alpha); 373 fadeOutAlpha = Math.max(0.0f, fadeOutAlpha); 374 375 // We interpolate the alpha of the targetView to 1 376 alpha = fadeOutAlpha + alpha; 377 KeyguardAffordanceView otherView = targetView == mRightIcon ? mLeftIcon : mRightIcon; 378 updateIconAlpha(targetView, alpha, false); 379 updateIconAlpha(otherView, fadeOutAlpha, false); 380 updateIconAlpha(mCenterIcon, fadeOutAlpha, false); 381 } 382 383 private float getTranslationFromRadius(float circleSize) { 384 float translation = (circleSize - mMinBackgroundRadius) / BACKGROUND_RADIUS_SCALE_FACTOR; 385 return Math.max(0, translation); 386 } 387 388 private float getRadiusFromTranslation(float translation) { 389 return translation * BACKGROUND_RADIUS_SCALE_FACTOR + mMinBackgroundRadius; 390 } 391 392 public void animateHideLeftRightIcon() { 393 updateIcon(mRightIcon, 0f, 0f, true, false); 394 updateIcon(mLeftIcon, 0f, 0f, true, false); 395 } 396 397 private void updateIcon(KeyguardAffordanceView view, float circleRadius, float alpha, 398 boolean animate, boolean slowRadiusAnimation) { 399 if (view.getVisibility() != View.VISIBLE) { 400 return; 401 } 402 view.setCircleRadius(circleRadius, slowRadiusAnimation); 403 updateIconAlpha(view, alpha, animate); 404 } 405 406 private void updateIconAlpha(KeyguardAffordanceView view, float alpha, boolean animate) { 407 float scale = getScale(alpha); 408 alpha = Math.min(1.0f, alpha); 409 view.setImageAlpha(alpha, animate); 410 view.setImageScale(scale, animate); 411 } 412 413 private float getScale(float alpha) { 414 float scale = alpha / SWIPE_RESTING_ALPHA_AMOUNT * 0.2f + 415 KeyguardAffordanceView.MIN_ICON_SCALE_AMOUNT; 416 return Math.min(scale, KeyguardAffordanceView.MAX_ICON_SCALE_AMOUNT); 417 } 418 419 private void trackMovement(MotionEvent event) { 420 if (mVelocityTracker != null) { 421 mVelocityTracker.addMovement(event); 422 } 423 } 424 425 private void initVelocityTracker() { 426 if (mVelocityTracker != null) { 427 mVelocityTracker.recycle(); 428 } 429 mVelocityTracker = VelocityTracker.obtain(); 430 } 431 432 private float getCurrentVelocity() { 433 if (mVelocityTracker == null) { 434 return 0; 435 } 436 mVelocityTracker.computeCurrentVelocity(1000); 437 return mVelocityTracker.getXVelocity(); 438 } 439 440 public void onConfigurationChanged() { 441 initDimens(); 442 initIcons(); 443 } 444 445 public void onRtlPropertiesChanged() { 446 initIcons(); 447 } 448 449 public void reset(boolean animate) { 450 if (mSwipeAnimator != null) { 451 mSwipeAnimator.cancel(); 452 } 453 setTranslation(0.0f, true, animate); 454 setSwipingInProgress(false); 455 } 456 457 public interface Callback { 458 459 /** 460 * Notifies the callback when an animation to a side page was started. 461 * 462 * @param rightPage Is the page animated to the right page? 463 */ 464 void onAnimationToSideStarted(boolean rightPage, float translation, float vel); 465 466 /** 467 * Notifies the callback the animation to a side page has ended. 468 */ 469 void onAnimationToSideEnded(); 470 471 float getPageWidth(); 472 473 void onSwipingStarted(); 474 475 KeyguardAffordanceView getLeftIcon(); 476 477 KeyguardAffordanceView getCenterIcon(); 478 479 KeyguardAffordanceView getRightIcon(); 480 481 View getLeftPreview(); 482 483 View getRightPreview(); 484 485 /** 486 * @return The factor the minimum swipe amount should be multiplied with. 487 */ 488 float getAffordanceFalsingFactor(); 489 } 490 } 491