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