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.launcher3.R; 44 import com.android.launcher3.ResourceUtils; 45 import com.android.launcher3.anim.Interpolators; 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(currentNightMode == Configuration.UI_MODE_NIGHT_YES 286 ? R.color.back_arrow_color_light 287 : R.color.back_arrow_color_dark)); 288 loadDimens(); 289 updateArrowDirection(); 290 291 mSwipeThreshold = ResourceUtils.getDimenByName( 292 "navigation_edge_action_drag_threshold", context.getResources(), 16 /* defaultValue */); 293 parent.addView(this, layoutParams); 294 setVisibility(GONE); 295 } 296 onDestroy()297 void onDestroy() { 298 ViewGroup parent = (ViewGroup) getParent(); 299 if (parent != null) { 300 parent.removeView(this); 301 } 302 } 303 304 @Override hasOverlappingRendering()305 public boolean hasOverlappingRendering() { 306 return false; 307 } 308 309 @SuppressLint("RtlHardcoded") setIsLeftPanel(boolean isLeftPanel)310 void setIsLeftPanel(boolean isLeftPanel) { 311 mIsLeftPanel = isLeftPanel; 312 } 313 getIsLeftPanel()314 boolean getIsLeftPanel() { 315 return mIsLeftPanel; 316 } 317 setDisplaySize(Point displaySize)318 void setDisplaySize(Point displaySize) { 319 mDisplaySize.set(displaySize.x, displaySize.y); 320 mScreenSize = Math.min(mDisplaySize.x, mDisplaySize.y); 321 } 322 setBackCallback(BackCallback callback)323 void setBackCallback(BackCallback callback) { 324 mBackCallback = callback; 325 } 326 getCurrentAngle()327 private float getCurrentAngle() { 328 return mCurrentAngle; 329 } 330 getCurrentTranslation()331 private float getCurrentTranslation() { 332 return mCurrentTranslation; 333 } 334 onMotionEvent(MotionEvent event)335 void onMotionEvent(MotionEvent event) { 336 if (mVelocityTracker == null) { 337 mVelocityTracker = VelocityTracker.obtain(); 338 } 339 mVelocityTracker.addMovement(event); 340 switch (event.getActionMasked()) { 341 case MotionEvent.ACTION_DOWN: 342 mDragSlopPassed = false; 343 resetOnDown(); 344 mStartX = event.getX(); 345 mStartY = event.getY(); 346 setVisibility(VISIBLE); 347 updatePosition(event.getY()); 348 break; 349 case MotionEvent.ACTION_MOVE: 350 handleMoveEvent(event); 351 break; 352 case MotionEvent.ACTION_UP: 353 if (mTriggerBack) { 354 triggerBack(); 355 } else { 356 cancelBack(); 357 } 358 mVelocityTracker.recycle(); 359 mVelocityTracker = null; 360 break; 361 case MotionEvent.ACTION_CANCEL: 362 cancelBack(); 363 mVelocityTracker.recycle(); 364 mVelocityTracker = null; 365 break; 366 } 367 } 368 369 @Override onConfigurationChanged(Configuration newConfig)370 protected void onConfigurationChanged(Configuration newConfig) { 371 super.onConfigurationChanged(newConfig); 372 updateArrowDirection(); 373 loadDimens(); 374 } 375 376 @Override onDraw(Canvas canvas)377 protected void onDraw(Canvas canvas) { 378 float pointerPosition = mCurrentTranslation - mArrowThickness / 2.0f; 379 canvas.save(); 380 canvas.translate( 381 mIsLeftPanel ? pointerPosition : getWidth() - pointerPosition, 382 (getHeight() * 0.5f) + mVerticalTranslation); 383 384 // Let's calculate the position of the end based on the angle 385 float x = (polarToCartX(mCurrentAngle) * mArrowLength); 386 float y = (polarToCartY(mCurrentAngle) * mArrowLength); 387 Path arrowPath = calculatePath(x, y); 388 389 canvas.drawPath(arrowPath, mPaint); 390 canvas.restore(); 391 } 392 393 @Override onLayout(boolean changed, int left, int top, int right, int bottom)394 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 395 super.onLayout(changed, left, top, right, bottom); 396 mMaxTranslation = getWidth() - mArrowPaddingEnd; 397 } 398 loadDimens()399 private void loadDimens() { 400 Resources res = getResources(); 401 mArrowPaddingEnd = ResourceUtils.getDimenByName("navigation_edge_panel_padding", res, 8); 402 mMinArrowPosition = ResourceUtils.getDimenByName("navigation_edge_arrow_min_y", res, 64); 403 mFingerOffset = ResourceUtils.getDimenByName("navigation_edge_finger_offset", res, 48); 404 } 405 updateArrowDirection()406 private void updateArrowDirection() { 407 // Both panels arrow point the same way 408 mArrowsPointLeft = getLayoutDirection() == LAYOUT_DIRECTION_LTR; 409 invalidate(); 410 } 411 getStaticArrowWidth()412 private float getStaticArrowWidth() { 413 return polarToCartX(ARROW_ANGLE_WHEN_EXTENDED_DEGREES) * mArrowLength; 414 } 415 polarToCartX(float angleInDegrees)416 private float polarToCartX(float angleInDegrees) { 417 return (float) Math.cos(Math.toRadians(angleInDegrees)); 418 } 419 polarToCartY(float angleInDegrees)420 private float polarToCartY(float angleInDegrees) { 421 return (float) Math.sin(Math.toRadians(angleInDegrees)); 422 } 423 calculatePath(float x, float y)424 private Path calculatePath(float x, float y) { 425 if (!mArrowsPointLeft) { 426 x = -x; 427 } 428 float extent = lerp(1.0f, 0.75f, mDisappearAmount); 429 x = x * extent; 430 y = y * extent; 431 mArrowPath.reset(); 432 mArrowPath.moveTo(x, y); 433 mArrowPath.lineTo(0, 0); 434 mArrowPath.lineTo(x, -y); 435 return mArrowPath; 436 } 437 lerp(float start, float stop, float amount)438 private static float lerp(float start, float stop, float amount) { 439 return start + (stop - start) * amount; 440 } 441 triggerBack()442 private void triggerBack() { 443 if (mBackCallback != null) { 444 mBackCallback.triggerBack(); 445 } 446 447 if (mVelocityTracker == null) { 448 mVelocityTracker = VelocityTracker.obtain(); 449 } 450 mVelocityTracker.computeCurrentVelocity(1000); 451 // Only do the extra translation if we're not already flinging 452 boolean isSlow = Math.abs(mVelocityTracker.getXVelocity()) < 500; 453 if (isSlow 454 || SystemClock.uptimeMillis() - mVibrationTime >= GESTURE_DURATION_FOR_CLICK_MS) { 455 VibratorWrapper.INSTANCE.get(getContext()).vibrate(VibratorWrapper.EFFECT_CLICK); 456 } 457 458 // Let's also snap the angle a bit 459 if (mAngleOffset > -4) { 460 mAngleOffset = Math.max(-8, mAngleOffset - 8); 461 updateAngle(true /* animated */); 462 } 463 464 // Finally, after the translation, animate back and disappear the arrow 465 Runnable translationEnd = () -> { 466 // let's snap it back 467 mAngleOffset = Math.max(0, mAngleOffset + 8); 468 updateAngle(true /* animated */); 469 470 mTranslationAnimation.setSpring(mTriggerBackSpring); 471 // Translate the arrow back a bit to make for a nice transition 472 setDesiredTranslation(mDesiredTranslation - dp(32), true /* animated */); 473 animate().alpha(0f).setDuration(DISAPPEAR_FADE_ANIMATION_DURATION_MS) 474 .withEndAction(() -> setVisibility(GONE)); 475 mArrowDisappearAnimation.start(); 476 }; 477 if (mTranslationAnimation.isRunning()) { 478 mTranslationAnimation.addEndListener(new DynamicAnimation.OnAnimationEndListener() { 479 @Override 480 public void onAnimationEnd(DynamicAnimation animation, boolean canceled, 481 float value, 482 float velocity) { 483 animation.removeEndListener(this); 484 if (!canceled) { 485 translationEnd.run(); 486 } 487 } 488 }); 489 } else { 490 translationEnd.run(); 491 } 492 } 493 cancelBack()494 private void cancelBack() { 495 if (mBackCallback != null) { 496 mBackCallback.cancelBack(); 497 } 498 499 if (mTranslationAnimation.isRunning()) { 500 mTranslationAnimation.addEndListener(mSetGoneEndListener); 501 } else { 502 setVisibility(GONE); 503 } 504 } 505 resetOnDown()506 private void resetOnDown() { 507 animate().cancel(); 508 mAngleAnimation.cancel(); 509 mTranslationAnimation.cancel(); 510 mVerticalTranslationAnimation.cancel(); 511 mArrowDisappearAnimation.cancel(); 512 mAngleOffset = 0; 513 mTranslationAnimation.setSpring(mRegularTranslationSpring); 514 // Reset the arrow to the side 515 setTriggerBack(false /* triggerBack */, false /* animated */); 516 setDesiredTranslation(0, false /* animated */); 517 setCurrentTranslation(0); 518 updateAngle(false /* animate */); 519 mPreviousTouchTranslation = 0; 520 mTotalTouchDelta = 0; 521 mVibrationTime = 0; 522 setDesiredVerticalTransition(0, false /* animated */); 523 } 524 handleMoveEvent(MotionEvent event)525 private void handleMoveEvent(MotionEvent event) { 526 float x = event.getX(); 527 float y = event.getY(); 528 float touchTranslation = Math.abs(x - mStartX); 529 float yOffset = y - mStartY; 530 float delta = touchTranslation - mPreviousTouchTranslation; 531 if (Math.abs(delta) > 0) { 532 if (Math.signum(delta) == Math.signum(mTotalTouchDelta)) { 533 mTotalTouchDelta += delta; 534 } else { 535 mTotalTouchDelta = delta; 536 } 537 } 538 mPreviousTouchTranslation = touchTranslation; 539 540 // Apply a haptic on drag slop passed 541 if (!mDragSlopPassed && touchTranslation > mSwipeThreshold) { 542 mDragSlopPassed = true; 543 VibratorWrapper.INSTANCE.get(getContext()).vibrate(VibratorWrapper.EFFECT_CLICK); 544 mVibrationTime = SystemClock.uptimeMillis(); 545 546 // Let's show the arrow and animate it in! 547 mDisappearAmount = 0.0f; 548 setAlpha(1f); 549 // And animate it go to back by default! 550 setTriggerBack(true /* triggerBack */, true /* animated */); 551 } 552 553 // Let's make sure we only go to the baseextend and apply rubberbanding afterwards 554 if (touchTranslation > mBaseTranslation) { 555 float diff = touchTranslation - mBaseTranslation; 556 float progress = MathUtils.clamp(diff / (mScreenSize - mBaseTranslation), 0, 1); 557 progress = RUBBER_BAND_INTERPOLATOR.getInterpolation(progress) 558 * (mMaxTranslation - mBaseTranslation); 559 touchTranslation = mBaseTranslation + progress; 560 } else { 561 float diff = mBaseTranslation - touchTranslation; 562 float progress = MathUtils.clamp(diff / mBaseTranslation, 0, 1); 563 progress = RUBBER_BAND_INTERPOLATOR_APPEAR.getInterpolation(progress) 564 * (mBaseTranslation / RUBBER_BAND_AMOUNT_APPEAR); 565 touchTranslation = mBaseTranslation - progress; 566 } 567 // By default we just assume the current direction is kept 568 boolean triggerBack = mTriggerBack; 569 570 // First lets see if we had continuous motion in one direction for a while 571 if (Math.abs(mTotalTouchDelta) > mMinDeltaForSwitch) { 572 triggerBack = mTotalTouchDelta > 0; 573 } 574 575 // Then, let's see if our velocity tells us to change direction 576 mVelocityTracker.computeCurrentVelocity(1000); 577 float xVelocity = mVelocityTracker.getXVelocity(); 578 float yVelocity = mVelocityTracker.getYVelocity(); 579 float velocity = (float) Math.hypot(xVelocity, yVelocity); 580 mAngleOffset = Math.min(velocity / 1000 * ARROW_ANGLE_ADDED_PER_1000_SPEED, 581 ARROW_MAX_ANGLE_SPEED_OFFSET_DEGREES) * Math.signum(xVelocity); 582 if (mIsLeftPanel && mArrowsPointLeft || !mIsLeftPanel && !mArrowsPointLeft) { 583 mAngleOffset *= -1; 584 } 585 586 // Last if the direction in Y is bigger than X * 2 we also abort 587 if (Math.abs(yOffset) > Math.abs(x - mStartX) * 2) { 588 triggerBack = false; 589 } 590 setTriggerBack(triggerBack, true /* animated */); 591 592 if (!mTriggerBack) { 593 touchTranslation = 0; 594 } else if (mIsLeftPanel && mArrowsPointLeft 595 || (!mIsLeftPanel && !mArrowsPointLeft)) { 596 // If we're on the left we should move less, because the arrow is facing the other 597 // direction 598 touchTranslation -= getStaticArrowWidth(); 599 } 600 setDesiredTranslation(touchTranslation, true /* animated */); 601 updateAngle(true /* animated */); 602 603 float maxYOffset = getHeight() / 2.0f - mArrowLength; 604 float progress = 605 MathUtils.clamp(Math.abs(yOffset) / (maxYOffset * RUBBER_BAND_AMOUNT), 0, 1); 606 float verticalTranslation = RUBBER_BAND_INTERPOLATOR.getInterpolation(progress) 607 * maxYOffset * Math.signum(yOffset); 608 setDesiredVerticalTransition(verticalTranslation, true /* animated */); 609 } 610 updatePosition(float touchY)611 private void updatePosition(float touchY) { 612 float positionY = touchY - mFingerOffset; 613 positionY = Math.max(positionY, mMinArrowPosition); 614 positionY -= getLayoutParams().height / 2.0f; 615 setX(mIsLeftPanel ? 0 : mDisplaySize.x - getLayoutParams().width); 616 setY(MathUtils.clamp((int) positionY, 0, mDisplaySize.y)); 617 } 618 setDesiredVerticalTransition(float verticalTranslation, boolean animated)619 private void setDesiredVerticalTransition(float verticalTranslation, boolean animated) { 620 if (mDesiredVerticalTranslation != verticalTranslation) { 621 mDesiredVerticalTranslation = verticalTranslation; 622 if (!animated) { 623 setVerticalTranslation(verticalTranslation); 624 } else { 625 mVerticalTranslationAnimation.animateToFinalPosition(verticalTranslation); 626 } 627 invalidate(); 628 } 629 } 630 setVerticalTranslation(float verticalTranslation)631 private void setVerticalTranslation(float verticalTranslation) { 632 mVerticalTranslation = verticalTranslation; 633 invalidate(); 634 } 635 getVerticalTranslation()636 private float getVerticalTranslation() { 637 return mVerticalTranslation; 638 } 639 setDesiredTranslation(float desiredTranslation, boolean animated)640 private void setDesiredTranslation(float desiredTranslation, boolean animated) { 641 if (mDesiredTranslation != desiredTranslation) { 642 mDesiredTranslation = desiredTranslation; 643 if (!animated) { 644 setCurrentTranslation(desiredTranslation); 645 } else { 646 mTranslationAnimation.animateToFinalPosition(desiredTranslation); 647 } 648 } 649 } 650 setCurrentTranslation(float currentTranslation)651 private void setCurrentTranslation(float currentTranslation) { 652 mCurrentTranslation = currentTranslation; 653 invalidate(); 654 } 655 setTriggerBack(boolean triggerBack, boolean animated)656 private void setTriggerBack(boolean triggerBack, boolean animated) { 657 if (mTriggerBack != triggerBack) { 658 mTriggerBack = triggerBack; 659 mAngleAnimation.cancel(); 660 updateAngle(animated); 661 // Whenever the trigger back state changes the existing translation animation should be 662 // cancelled 663 mTranslationAnimation.cancel(); 664 } 665 } 666 updateAngle(boolean animated)667 private void updateAngle(boolean animated) { 668 float newAngle = mTriggerBack ? ARROW_ANGLE_WHEN_EXTENDED_DEGREES + mAngleOffset : 90; 669 if (newAngle != mDesiredAngle) { 670 if (!animated) { 671 setCurrentAngle(newAngle); 672 } else { 673 mAngleAnimation.setSpring(mTriggerBack ? mAngleAppearForce : mAngleDisappearForce); 674 mAngleAnimation.animateToFinalPosition(newAngle); 675 } 676 mDesiredAngle = newAngle; 677 } 678 } 679 setCurrentAngle(float currentAngle)680 private void setCurrentAngle(float currentAngle) { 681 mCurrentAngle = currentAngle; 682 invalidate(); 683 } 684 dp(float dp)685 private float dp(float dp) { 686 return mDensity * dp; 687 } 688 689 /** Callback to let the gesture handler react to the detected back gestures. */ 690 interface BackCallback { 691 /** Indicates that a Back gesture was recognized. */ triggerBack()692 void triggerBack(); 693 694 /** Indicates that the gesture was cancelled. */ cancelBack()695 void cancelBack(); 696 } 697 } 698