1 /* 2 * Copyright (C) 2019 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.ValueAnimator; 20 import android.content.Context; 21 import android.content.res.Configuration; 22 import android.graphics.Canvas;; 23 import android.graphics.Paint; 24 import android.graphics.Path; 25 import android.graphics.Rect; 26 import android.os.SystemClock; 27 import android.os.VibrationEffect; 28 import android.util.DisplayMetrics; 29 import android.util.MathUtils; 30 import android.view.ContextThemeWrapper; 31 import android.view.MotionEvent; 32 import android.view.VelocityTracker; 33 import android.view.View; 34 import android.view.animation.Interpolator; 35 import android.view.animation.PathInterpolator; 36 37 import com.android.settingslib.Utils; 38 import com.android.systemui.Dependency; 39 import com.android.systemui.Interpolators; 40 import com.android.systemui.R; 41 import com.android.systemui.statusbar.VibratorHelper; 42 43 import androidx.core.graphics.ColorUtils; 44 import androidx.dynamicanimation.animation.DynamicAnimation; 45 import androidx.dynamicanimation.animation.FloatPropertyCompat; 46 import androidx.dynamicanimation.animation.SpringAnimation; 47 import androidx.dynamicanimation.animation.SpringForce; 48 49 public class NavigationBarEdgePanel extends View { 50 51 private static final long COLOR_ANIMATION_DURATION_MS = 120; 52 private static final long DISAPPEAR_FADE_ANIMATION_DURATION_MS = 80; 53 private static final long DISAPPEAR_ARROW_ANIMATION_DURATION_MS = 100; 54 55 /** 56 * The time required since the first vibration effect to automatically trigger a click 57 */ 58 private static final int GESTURE_DURATION_FOR_CLICK_MS = 400; 59 60 /** 61 * The size of the protection of the arrow in px. Only used if this is not background protected 62 */ 63 private static final int PROTECTION_WIDTH_PX = 2; 64 65 /** 66 * The basic translation in dp where the arrow resides 67 */ 68 private static final int BASE_TRANSLATION_DP = 32; 69 70 /** 71 * The length of the arrow leg measured from the center to the end 72 */ 73 private static final int ARROW_LENGTH_DP = 18; 74 75 /** 76 * The angle measured from the xAxis, where the leg is when the arrow rests 77 */ 78 private static final int ARROW_ANGLE_WHEN_EXTENDED_DEGREES = 56; 79 80 /** 81 * The angle that is added per 1000 px speed to the angle of the leg 82 */ 83 private static final int ARROW_ANGLE_ADDED_PER_1000_SPEED = 4; 84 85 /** 86 * The maximum angle offset allowed due to speed 87 */ 88 private static final int ARROW_MAX_ANGLE_SPEED_OFFSET_DEGREES = 4; 89 90 /** 91 * The thickness of the arrow. Adjusted to match the home handle (approximately) 92 */ 93 private static final float ARROW_THICKNESS_DP = 2.5f; 94 95 /** 96 * The amount of rubber banding we do for the vertical translation 97 */ 98 private static final int RUBBER_BAND_AMOUNT = 15; 99 100 /** 101 * The interpolator used to rubberband 102 */ 103 private static final Interpolator RUBBER_BAND_INTERPOLATOR 104 = new PathInterpolator(1.0f / 5.0f, 1.0f, 1.0f, 1.0f); 105 106 /** 107 * The amount of rubber banding we do for the translation before base translation 108 */ 109 private static final int RUBBER_BAND_AMOUNT_APPEAR = 4; 110 111 /** 112 * The interpolator used to rubberband the appearing of the arrow. 113 */ 114 private static final Interpolator RUBBER_BAND_INTERPOLATOR_APPEAR 115 = new PathInterpolator(1.0f / RUBBER_BAND_AMOUNT_APPEAR, 1.0f, 1.0f, 1.0f); 116 117 private final VibratorHelper mVibratorHelper; 118 119 /** 120 * The paint the arrow is drawn with 121 */ 122 private final Paint mPaint = new Paint(); 123 /** 124 * The paint the arrow protection is drawn with 125 */ 126 private final Paint mProtectionPaint; 127 128 private final float mDensity; 129 private final float mBaseTranslation; 130 private final float mArrowLength; 131 private final float mArrowThickness; 132 133 /** 134 * The minimum delta needed in movement for the arrow to change direction / stop triggering back 135 */ 136 private final float mMinDeltaForSwitch; 137 138 private final float mSwipeThreshold; 139 private final Path mArrowPath = new Path(); 140 141 private final SpringAnimation mAngleAnimation; 142 private final SpringAnimation mTranslationAnimation; 143 private final SpringAnimation mVerticalTranslationAnimation; 144 private final SpringForce mAngleAppearForce; 145 private final SpringForce mAngleDisappearForce; 146 private final ValueAnimator mArrowColorAnimator; 147 private final ValueAnimator mArrowDisappearAnimation; 148 private final SpringForce mRegularTranslationSpring; 149 private final SpringForce mTriggerBackSpring; 150 151 private VelocityTracker mVelocityTracker; 152 private boolean mIsDark = false; 153 private boolean mShowProtection = false; 154 private int mProtectionColorLight; 155 private int mArrowPaddingEnd; 156 private int mArrowColorLight; 157 private int mProtectionColorDark; 158 private int mArrowColorDark; 159 private int mProtectionColor; 160 private int mArrowColor; 161 162 /** 163 * True if the panel is currently on the left of the screen 164 */ 165 private boolean mIsLeftPanel; 166 167 private float mStartX; 168 private float mStartY; 169 private float mCurrentAngle; 170 /** 171 * The current translation of the arrow 172 */ 173 private float mCurrentTranslation; 174 /** 175 * Where the arrow will be in the resting position. 176 */ 177 private float mDesiredTranslation; 178 179 private boolean mDragSlopPassed; 180 private boolean mArrowsPointLeft; 181 private float mMaxTranslation; 182 private boolean mTriggerBack; 183 private float mPreviousTouchTranslation; 184 private float mTotalTouchDelta; 185 private float mVerticalTranslation; 186 private float mDesiredVerticalTranslation; 187 private float mDesiredAngle; 188 private float mAngleOffset; 189 private int mArrowStartColor; 190 private int mCurrentArrowColor; 191 private float mDisappearAmount; 192 private long mVibrationTime; 193 private int mScreenSize; 194 195 private DynamicAnimation.OnAnimationEndListener mSetGoneEndListener 196 = new DynamicAnimation.OnAnimationEndListener() { 197 @Override 198 public void onAnimationEnd(DynamicAnimation animation, boolean canceled, float value, 199 float velocity) { 200 animation.removeEndListener(this); 201 if (!canceled) { 202 setVisibility(GONE); 203 } 204 } 205 }; 206 private static final FloatPropertyCompat<NavigationBarEdgePanel> CURRENT_ANGLE = 207 new FloatPropertyCompat<NavigationBarEdgePanel>("currentAngle") { 208 @Override 209 public void setValue(NavigationBarEdgePanel object, float value) { 210 object.setCurrentAngle(value); 211 } 212 213 @Override 214 public float getValue(NavigationBarEdgePanel object) { 215 return object.getCurrentAngle(); 216 } 217 }; 218 219 private static final FloatPropertyCompat<NavigationBarEdgePanel> CURRENT_TRANSLATION = 220 new FloatPropertyCompat<NavigationBarEdgePanel>("currentTranslation") { 221 222 @Override 223 public void setValue(NavigationBarEdgePanel object, float value) { 224 object.setCurrentTranslation(value); 225 } 226 227 @Override 228 public float getValue(NavigationBarEdgePanel object) { 229 return object.getCurrentTranslation(); 230 } 231 }; 232 private static final FloatPropertyCompat<NavigationBarEdgePanel> CURRENT_VERTICAL_TRANSLATION = 233 new FloatPropertyCompat<NavigationBarEdgePanel>("verticalTranslation") { 234 235 @Override 236 public void setValue(NavigationBarEdgePanel object, float value) { 237 object.setVerticalTranslation(value); 238 } 239 240 @Override 241 public float getValue(NavigationBarEdgePanel object) { 242 return object.getVerticalTranslation(); 243 } 244 }; 245 NavigationBarEdgePanel(Context context)246 public NavigationBarEdgePanel(Context context) { 247 super(context); 248 249 mVibratorHelper = Dependency.get(VibratorHelper.class); 250 251 mDensity = context.getResources().getDisplayMetrics().density; 252 253 mBaseTranslation = dp(BASE_TRANSLATION_DP); 254 mArrowLength = dp(ARROW_LENGTH_DP); 255 mArrowThickness = dp(ARROW_THICKNESS_DP); 256 mMinDeltaForSwitch = dp(32); 257 258 mPaint.setStrokeWidth(mArrowThickness); 259 mPaint.setStrokeCap(Paint.Cap.ROUND); 260 mPaint.setAntiAlias(true); 261 mPaint.setStyle(Paint.Style.STROKE); 262 mPaint.setStrokeJoin(Paint.Join.ROUND); 263 264 mArrowColorAnimator = ValueAnimator.ofFloat(0.0f, 1.0f); 265 mArrowColorAnimator.setDuration(COLOR_ANIMATION_DURATION_MS); 266 mArrowColorAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 267 @Override 268 public void onAnimationUpdate(ValueAnimator animation) { 269 int newColor = ColorUtils.blendARGB(mArrowStartColor, mArrowColor, 270 animation.getAnimatedFraction()); 271 setCurrentArrowColor(newColor); 272 } 273 }); 274 275 mArrowDisappearAnimation = ValueAnimator.ofFloat(0.0f, 1.0f); 276 mArrowDisappearAnimation.setDuration(DISAPPEAR_ARROW_ANIMATION_DURATION_MS); 277 mArrowDisappearAnimation.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); 278 mArrowDisappearAnimation.addUpdateListener(animation -> { 279 mDisappearAmount = (float) animation.getAnimatedValue(); 280 invalidate(); 281 }); 282 283 mAngleAnimation = 284 new SpringAnimation(this, CURRENT_ANGLE); 285 mAngleAppearForce = new SpringForce() 286 .setStiffness(500) 287 .setDampingRatio(0.5f); 288 mAngleDisappearForce = new SpringForce() 289 .setStiffness(SpringForce.STIFFNESS_MEDIUM) 290 .setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY) 291 .setFinalPosition(90); 292 mAngleAnimation.setSpring(mAngleAppearForce).setMaxValue(90); 293 294 mTranslationAnimation = 295 new SpringAnimation(this, CURRENT_TRANSLATION); 296 mRegularTranslationSpring = new SpringForce() 297 .setStiffness(SpringForce.STIFFNESS_MEDIUM) 298 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY); 299 mTriggerBackSpring = new SpringForce() 300 .setStiffness(450) 301 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY); 302 mTranslationAnimation.setSpring(mRegularTranslationSpring); 303 mVerticalTranslationAnimation = 304 new SpringAnimation(this, CURRENT_VERTICAL_TRANSLATION); 305 mVerticalTranslationAnimation.setSpring( 306 new SpringForce() 307 .setStiffness(SpringForce.STIFFNESS_MEDIUM) 308 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)); 309 310 mProtectionPaint = new Paint(mPaint); 311 mProtectionPaint.setStrokeWidth(mArrowThickness + PROTECTION_WIDTH_PX); 312 loadDimens(); 313 314 loadColors(context); 315 updateArrowDirection(); 316 317 mSwipeThreshold = context.getResources() 318 .getDimension(R.dimen.navigation_edge_action_drag_threshold); 319 setVisibility(GONE); 320 } 321 322 @Override hasOverlappingRendering()323 public boolean hasOverlappingRendering() { 324 return false; 325 } 326 shouldTriggerBack()327 public boolean shouldTriggerBack() { 328 return mTriggerBack; 329 } 330 setIsDark(boolean isDark, boolean animate)331 public void setIsDark(boolean isDark, boolean animate) { 332 mIsDark = isDark; 333 updateIsDark(animate); 334 } 335 setShowProtection(boolean showProtection)336 public void setShowProtection(boolean showProtection) { 337 mShowProtection = showProtection; 338 invalidate(); 339 } 340 setIsLeftPanel(boolean isLeftPanel)341 public void setIsLeftPanel(boolean isLeftPanel) { 342 mIsLeftPanel = isLeftPanel; 343 } 344 345 /** 346 * Adjust the rect to conform the the actual visible bounding box of the arrow. 347 * 348 * @param samplingRect the existing bounding box in screen coordinates, to be modified 349 */ adjustRectToBoundingBox(Rect samplingRect)350 public void adjustRectToBoundingBox(Rect samplingRect) { 351 float translation = mDesiredTranslation; 352 if (!mTriggerBack) { 353 // Let's take the resting position and bounds as the sampling rect, since we are not 354 // visible right now 355 translation = mBaseTranslation; 356 if (mIsLeftPanel && mArrowsPointLeft 357 || (!mIsLeftPanel && !mArrowsPointLeft)) { 358 // If we're on the left we should move less, because the arrow is facing the other 359 // direction 360 translation -= getStaticArrowWidth(); 361 } 362 } 363 float left = translation - mArrowThickness / 2.0f; 364 left = mIsLeftPanel ? left : samplingRect.width() - left; 365 366 // Let's calculate the position of the end based on the angle 367 float width = getStaticArrowWidth(); 368 float height = polarToCartY(ARROW_ANGLE_WHEN_EXTENDED_DEGREES) * mArrowLength * 2.0f; 369 if (!mArrowsPointLeft) { 370 left -= width; 371 } 372 373 float top = (getHeight() * 0.5f) + mDesiredVerticalTranslation - height / 2.0f; 374 samplingRect.offset((int) left, (int) top); 375 samplingRect.set(samplingRect.left, samplingRect.top, 376 (int) (samplingRect.left + width), 377 (int) (samplingRect.top + height)); 378 } 379 380 /** 381 * Updates the UI based on the motion events passed in device co-ordinates 382 */ handleTouch(MotionEvent event)383 public void handleTouch(MotionEvent event) { 384 if (mVelocityTracker == null) { 385 mVelocityTracker = VelocityTracker.obtain(); 386 } 387 mVelocityTracker.addMovement(event); 388 switch (event.getActionMasked()) { 389 case MotionEvent.ACTION_DOWN : { 390 mDragSlopPassed = false; 391 resetOnDown(); 392 mStartX = event.getX(); 393 mStartY = event.getY(); 394 setVisibility(VISIBLE); 395 break; 396 } 397 case MotionEvent.ACTION_MOVE: { 398 handleMoveEvent(event); 399 break; 400 } 401 // Fall through 402 case MotionEvent.ACTION_UP: 403 case MotionEvent.ACTION_CANCEL: { 404 if (mTriggerBack) { 405 triggerBack(); 406 } else { 407 if (mTranslationAnimation.isRunning()) { 408 mTranslationAnimation.addEndListener(mSetGoneEndListener); 409 } else { 410 setVisibility(GONE); 411 } 412 } 413 mVelocityTracker.recycle(); 414 mVelocityTracker = null; 415 break; 416 } 417 } 418 } 419 420 @Override onConfigurationChanged(Configuration newConfig)421 protected void onConfigurationChanged(Configuration newConfig) { 422 super.onConfigurationChanged(newConfig); 423 updateArrowDirection(); 424 loadDimens(); 425 } 426 427 @Override onDraw(Canvas canvas)428 protected void onDraw(Canvas canvas) { 429 float pointerPosition = mCurrentTranslation - mArrowThickness / 2.0f; 430 canvas.save(); 431 canvas.translate( 432 mIsLeftPanel ? pointerPosition : getWidth() - pointerPosition, 433 (getHeight() * 0.5f) + mVerticalTranslation); 434 435 // Let's calculate the position of the end based on the angle 436 float x = (polarToCartX(mCurrentAngle) * mArrowLength); 437 float y = (polarToCartY(mCurrentAngle) * mArrowLength); 438 Path arrowPath = calculatePath(x,y); 439 if (mShowProtection) { 440 canvas.drawPath(arrowPath, mProtectionPaint); 441 } 442 443 canvas.drawPath(arrowPath, mPaint); 444 canvas.restore(); 445 } 446 447 @Override onLayout(boolean changed, int left, int top, int right, int bottom)448 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 449 super.onLayout(changed, left, top, right, bottom); 450 451 mMaxTranslation = getWidth() - mArrowPaddingEnd; 452 } 453 loadDimens()454 private void loadDimens() { 455 mArrowPaddingEnd = getContext().getResources().getDimensionPixelSize( 456 R.dimen.navigation_edge_panel_padding); 457 DisplayMetrics metrics = getResources().getDisplayMetrics(); 458 mScreenSize = Math.min(metrics.widthPixels, metrics.heightPixels); 459 } 460 updateArrowDirection()461 private void updateArrowDirection() { 462 // Both panels arrow point the same way 463 mArrowsPointLeft = getLayoutDirection() == LAYOUT_DIRECTION_LTR; 464 invalidate(); 465 } 466 loadColors(Context context)467 private void loadColors(Context context) { 468 final int dualToneDarkTheme = Utils.getThemeAttr(context, R.attr.darkIconTheme); 469 final int dualToneLightTheme = Utils.getThemeAttr(context, R.attr.lightIconTheme); 470 Context lightContext = new ContextThemeWrapper(context, dualToneLightTheme); 471 Context darkContext = new ContextThemeWrapper(context, dualToneDarkTheme); 472 mArrowColorLight = Utils.getColorAttrDefaultColor(lightContext, R.attr.singleToneColor); 473 mArrowColorDark = Utils.getColorAttrDefaultColor(darkContext, R.attr.singleToneColor); 474 mProtectionColorDark = mArrowColorLight; 475 mProtectionColorLight = mArrowColorDark; 476 updateIsDark(false /* animate */); 477 } 478 updateIsDark(boolean animate)479 private void updateIsDark(boolean animate) { 480 // TODO: Maybe animate protection as well 481 mProtectionColor = mIsDark ? mProtectionColorDark : mProtectionColorLight; 482 mProtectionPaint.setColor(mProtectionColor); 483 mArrowColor = mIsDark ? mArrowColorDark : mArrowColorLight; 484 mArrowColorAnimator.cancel(); 485 if (!animate) { 486 setCurrentArrowColor(mArrowColor); 487 } else { 488 mArrowStartColor = mCurrentArrowColor; 489 mArrowColorAnimator.start(); 490 } 491 } 492 setCurrentArrowColor(int color)493 private void setCurrentArrowColor(int color) { 494 mCurrentArrowColor = color; 495 mPaint.setColor(color); 496 invalidate(); 497 } 498 getStaticArrowWidth()499 private float getStaticArrowWidth() { 500 return polarToCartX(ARROW_ANGLE_WHEN_EXTENDED_DEGREES) * mArrowLength; 501 } 502 polarToCartX(float angleInDegrees)503 private float polarToCartX(float angleInDegrees) { 504 return (float) Math.cos(Math.toRadians(angleInDegrees)); 505 } 506 polarToCartY(float angleInDegrees)507 private float polarToCartY(float angleInDegrees) { 508 return (float) Math.sin(Math.toRadians(angleInDegrees)); 509 } 510 calculatePath(float x, float y)511 private Path calculatePath(float x, float y) { 512 if (!mArrowsPointLeft) { 513 x = -x; 514 } 515 float extent = MathUtils.lerp(1.0f, 0.75f, mDisappearAmount); 516 x = x * extent; 517 y = y * extent; 518 mArrowPath.reset(); 519 mArrowPath.moveTo(x, y); 520 mArrowPath.lineTo(0, 0); 521 mArrowPath.lineTo(x, -y); 522 return mArrowPath; 523 } 524 getCurrentAngle()525 private float getCurrentAngle() { 526 return mCurrentAngle; 527 } 528 getCurrentTranslation()529 private float getCurrentTranslation() { 530 return mCurrentTranslation; 531 } 532 triggerBack()533 private void triggerBack() { 534 mVelocityTracker.computeCurrentVelocity(1000); 535 // Only do the extra translation if we're not already flinging 536 boolean isSlow = Math.abs(mVelocityTracker.getXVelocity()) < 500; 537 if (isSlow 538 || SystemClock.uptimeMillis() - mVibrationTime >= GESTURE_DURATION_FOR_CLICK_MS) { 539 mVibratorHelper.vibrate(VibrationEffect.EFFECT_CLICK); 540 } 541 542 // Let's also snap the angle a bit 543 if (mAngleOffset > -4) { 544 mAngleOffset = Math.max(-8, mAngleOffset - 8); 545 updateAngle(true /* animated */); 546 } 547 548 // Finally, after the translation, animate back and disappear the arrow 549 Runnable translationEnd = () -> { 550 // let's snap it back 551 mAngleOffset = Math.max(0, mAngleOffset + 8); 552 updateAngle(true /* animated */); 553 554 mTranslationAnimation.setSpring(mTriggerBackSpring); 555 // Translate the arrow back a bit to make for a nice transition 556 setDesiredTranslation(mDesiredTranslation - dp(32), true /* animated */); 557 animate().alpha(0f).setDuration(DISAPPEAR_FADE_ANIMATION_DURATION_MS) 558 .withEndAction(() -> setVisibility(GONE)); 559 mArrowDisappearAnimation.start(); 560 }; 561 if (mTranslationAnimation.isRunning()) { 562 mTranslationAnimation.addEndListener(new DynamicAnimation.OnAnimationEndListener() { 563 @Override 564 public void onAnimationEnd(DynamicAnimation animation, boolean canceled, 565 float value, 566 float velocity) { 567 animation.removeEndListener(this); 568 if (!canceled) { 569 translationEnd.run(); 570 } 571 } 572 }); 573 } else { 574 translationEnd.run(); 575 } 576 577 } 578 resetOnDown()579 private void resetOnDown() { 580 animate().cancel(); 581 mAngleAnimation.cancel(); 582 mTranslationAnimation.cancel(); 583 mVerticalTranslationAnimation.cancel(); 584 mArrowDisappearAnimation.cancel(); 585 mAngleOffset = 0; 586 mTranslationAnimation.setSpring(mRegularTranslationSpring); 587 // Reset the arrow to the side 588 setTriggerBack(false /* triggerBack */, false /* animated */); 589 setDesiredTranslation(0, false /* animated */); 590 setCurrentTranslation(0); 591 updateAngle(false /* animate */); 592 mPreviousTouchTranslation = 0; 593 mTotalTouchDelta = 0; 594 mVibrationTime = 0; 595 setDesiredVerticalTransition(0, false /* animated */); 596 } 597 handleMoveEvent(MotionEvent event)598 private void handleMoveEvent(MotionEvent event) { 599 float x = event.getX(); 600 float y = event.getY(); 601 float touchTranslation = MathUtils.abs(x - mStartX); 602 float yOffset = y - mStartY; 603 float delta = touchTranslation - mPreviousTouchTranslation; 604 if (Math.abs(delta) > 0) { 605 if (Math.signum(delta) == Math.signum(mTotalTouchDelta)) { 606 mTotalTouchDelta += delta; 607 } else { 608 mTotalTouchDelta = delta; 609 } 610 } 611 mPreviousTouchTranslation = touchTranslation; 612 613 // Apply a haptic on drag slop passed 614 if (!mDragSlopPassed && touchTranslation > mSwipeThreshold) { 615 mDragSlopPassed = true; 616 mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK); 617 mVibrationTime = SystemClock.uptimeMillis(); 618 619 // Let's show the arrow and animate it in! 620 mDisappearAmount = 0.0f; 621 setAlpha(1f); 622 // And animate it go to back by default! 623 setTriggerBack(true /* triggerBack */, true /* animated */); 624 } 625 626 // Let's make sure we only go to the baseextend and apply rubberbanding afterwards 627 if (touchTranslation > mBaseTranslation) { 628 float diff = touchTranslation - mBaseTranslation; 629 float progress = MathUtils.saturate(diff / (mScreenSize - mBaseTranslation)); 630 progress = RUBBER_BAND_INTERPOLATOR.getInterpolation(progress) 631 * (mMaxTranslation - mBaseTranslation); 632 touchTranslation = mBaseTranslation + progress; 633 } else { 634 float diff = mBaseTranslation - touchTranslation; 635 float progress = MathUtils.saturate(diff / mBaseTranslation); 636 progress = RUBBER_BAND_INTERPOLATOR_APPEAR.getInterpolation(progress) 637 * (mBaseTranslation / RUBBER_BAND_AMOUNT_APPEAR); 638 touchTranslation = mBaseTranslation - progress; 639 } 640 // By default we just assume the current direction is kept 641 boolean triggerBack = mTriggerBack; 642 643 // First lets see if we had continuous motion in one direction for a while 644 if (Math.abs(mTotalTouchDelta) > mMinDeltaForSwitch) { 645 triggerBack = mTotalTouchDelta > 0; 646 } 647 648 // Then, let's see if our velocity tells us to change direction 649 mVelocityTracker.computeCurrentVelocity(1000); 650 float xVelocity = mVelocityTracker.getXVelocity(); 651 float yVelocity = mVelocityTracker.getYVelocity(); 652 float velocity = MathUtils.mag(xVelocity, yVelocity); 653 mAngleOffset = Math.min(velocity / 1000 * ARROW_ANGLE_ADDED_PER_1000_SPEED, 654 ARROW_MAX_ANGLE_SPEED_OFFSET_DEGREES) * Math.signum(xVelocity); 655 if (mIsLeftPanel && mArrowsPointLeft || !mIsLeftPanel && !mArrowsPointLeft) { 656 mAngleOffset *= -1; 657 } 658 659 // Last if the direction in Y is bigger than X * 2 we also abort 660 if (Math.abs(yOffset) > Math.abs(x - mStartX) * 2) { 661 triggerBack = false; 662 } 663 setTriggerBack(triggerBack, true /* animated */); 664 665 if (!mTriggerBack) { 666 touchTranslation = 0; 667 } else if (mIsLeftPanel && mArrowsPointLeft 668 || (!mIsLeftPanel && !mArrowsPointLeft)) { 669 // If we're on the left we should move less, because the arrow is facing the other 670 // direction 671 touchTranslation -= getStaticArrowWidth(); 672 } 673 setDesiredTranslation(touchTranslation, true /* animated */); 674 updateAngle(true /* animated */); 675 676 float maxYOffset = getHeight() / 2.0f - mArrowLength; 677 float progress = MathUtils.constrain( 678 Math.abs(yOffset) / (maxYOffset * RUBBER_BAND_AMOUNT), 679 0, 1); 680 float verticalTranslation = RUBBER_BAND_INTERPOLATOR.getInterpolation(progress) 681 * maxYOffset * Math.signum(yOffset); 682 setDesiredVerticalTransition(verticalTranslation, true /* animated */); 683 } 684 setDesiredVerticalTransition(float verticalTranslation, boolean animated)685 private void setDesiredVerticalTransition(float verticalTranslation, boolean animated) { 686 if (mDesiredVerticalTranslation != verticalTranslation) { 687 mDesiredVerticalTranslation = verticalTranslation; 688 if (!animated) { 689 setVerticalTranslation(verticalTranslation); 690 } else { 691 mVerticalTranslationAnimation.animateToFinalPosition(verticalTranslation); 692 } 693 invalidate(); 694 } 695 } 696 setVerticalTranslation(float verticalTranslation)697 private void setVerticalTranslation(float verticalTranslation) { 698 mVerticalTranslation = verticalTranslation; 699 invalidate(); 700 } 701 getVerticalTranslation()702 private float getVerticalTranslation() { 703 return mVerticalTranslation; 704 } 705 setDesiredTranslation(float desiredTranslation, boolean animated)706 private void setDesiredTranslation(float desiredTranslation, boolean animated) { 707 if (mDesiredTranslation != desiredTranslation) { 708 mDesiredTranslation = desiredTranslation; 709 if (!animated) { 710 setCurrentTranslation(desiredTranslation); 711 } else { 712 mTranslationAnimation.animateToFinalPosition(desiredTranslation); 713 } 714 } 715 } 716 setCurrentTranslation(float currentTranslation)717 private void setCurrentTranslation(float currentTranslation) { 718 mCurrentTranslation = currentTranslation; 719 invalidate(); 720 } 721 setTriggerBack(boolean triggerBack, boolean animated)722 private void setTriggerBack(boolean triggerBack, boolean animated) { 723 if (mTriggerBack != triggerBack) { 724 mTriggerBack = triggerBack; 725 mAngleAnimation.cancel(); 726 updateAngle(animated); 727 // Whenever the trigger back state changes the existing translation animation should be 728 // cancelled 729 mTranslationAnimation.cancel(); 730 } 731 } 732 updateAngle(boolean animated)733 private void updateAngle(boolean animated) { 734 float newAngle = mTriggerBack ? ARROW_ANGLE_WHEN_EXTENDED_DEGREES + mAngleOffset : 90; 735 if (newAngle != mDesiredAngle) { 736 if (!animated) { 737 setCurrentAngle(newAngle); 738 } else { 739 mAngleAnimation.setSpring(mTriggerBack ? mAngleAppearForce : mAngleDisappearForce); 740 mAngleAnimation.animateToFinalPosition(newAngle); 741 } 742 mDesiredAngle = newAngle; 743 } 744 } 745 setCurrentAngle(float currentAngle)746 private void setCurrentAngle(float currentAngle) { 747 mCurrentAngle = currentAngle; 748 invalidate(); 749 } 750 dp(float dp)751 private float dp(float dp) { 752 return mDensity * dp; 753 } 754 } 755