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