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