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