1 /* <lambda>null2 * Copyright (C) 2022 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 package com.android.systemui.navigationbar.gestural 17 18 import android.content.Context 19 import android.content.res.Configuration 20 import android.graphics.Color 21 import android.graphics.Paint 22 import android.graphics.Point 23 import android.os.Handler 24 import android.util.Log 25 import android.util.MathUtils 26 import android.view.Gravity 27 import android.view.HapticFeedbackConstants 28 import android.view.MotionEvent 29 import android.view.VelocityTracker 30 import android.view.ViewConfiguration 31 import android.view.WindowManager 32 import androidx.annotation.VisibleForTesting 33 import androidx.core.os.postDelayed 34 import androidx.core.view.isVisible 35 import androidx.dynamicanimation.animation.DynamicAnimation 36 import com.android.internal.jank.Cuj 37 import com.android.internal.jank.InteractionJankMonitor 38 import com.android.internal.util.LatencyTracker 39 import com.android.systemui.plugins.NavigationEdgeBackPlugin 40 import com.android.systemui.statusbar.VibratorHelper 41 import com.android.systemui.statusbar.policy.ConfigurationController 42 import com.android.systemui.util.ViewController 43 import com.android.systemui.util.concurrency.BackPanelUiThread 44 import com.android.systemui.util.concurrency.UiThreadContext 45 import com.android.systemui.util.time.SystemClock 46 import java.io.PrintWriter 47 import javax.inject.Inject 48 import kotlin.math.abs 49 import kotlin.math.max 50 import kotlin.math.min 51 import kotlin.math.sign 52 53 private const val TAG = "BackPanelController" 54 private const val ENABLE_FAILSAFE = true 55 private const val FAILSAFE_DELAY_MS = 350L 56 57 private const val PX_PER_SEC = 1000 58 private const val PX_PER_MS = 1 59 60 internal const val MIN_DURATION_ACTIVE_BEFORE_INACTIVE_ANIMATION = 300L 61 private const val MIN_DURATION_ACTIVE_AFTER_INACTIVE_ANIMATION = 130L 62 private const val MIN_DURATION_CANCELLED_ANIMATION = 200L 63 private const val MIN_DURATION_COMMITTED_ANIMATION = 80L 64 private const val MIN_DURATION_COMMITTED_AFTER_FLING_ANIMATION = 120L 65 private const val MIN_DURATION_INACTIVE_BEFORE_FLUNG_ANIMATION = 50L 66 private const val MIN_DURATION_INACTIVE_BEFORE_ACTIVE_ANIMATION = 160F 67 private const val MIN_DURATION_ENTRY_BEFORE_ACTIVE_ANIMATION = 10F 68 internal const val MAX_DURATION_ENTRY_BEFORE_ACTIVE_ANIMATION = 100F 69 private const val MIN_DURATION_FLING_ANIMATION = 160L 70 71 private const val MIN_DURATION_ENTRY_TO_ACTIVE_CONSIDERED_AS_FLING = 100L 72 private const val MIN_DURATION_INACTIVE_TO_ACTIVE_CONSIDERED_AS_FLING = 400L 73 74 private const val POP_ON_FLING_DELAY = 60L 75 private const val POP_ON_FLING_VELOCITY = 2f 76 private const val POP_ON_COMMITTED_VELOCITY = 3f 77 private const val POP_ON_ENTRY_TO_ACTIVE_VELOCITY = 4.5f 78 private const val POP_ON_INACTIVE_TO_ACTIVE_VELOCITY = 4.7f 79 private const val POP_ON_INACTIVE_VELOCITY = -1.5f 80 81 private const val DEBUG = false 82 83 class BackPanelController 84 internal constructor( 85 context: Context, 86 private val windowManager: WindowManager, 87 private val viewConfiguration: ViewConfiguration, 88 private val mainHandler: Handler, 89 private val systemClock: SystemClock, 90 private val vibratorHelper: VibratorHelper, 91 private val configurationController: ConfigurationController, 92 latencyTracker: LatencyTracker, 93 private val interactionJankMonitor: InteractionJankMonitor, 94 ) : ViewController<BackPanel>(BackPanel(context, latencyTracker)), NavigationEdgeBackPlugin { 95 96 /** 97 * Injectable instance to create a new BackPanelController. 98 * 99 * Necessary because EdgeBackGestureHandler sometimes needs to create new instances of 100 * BackPanelController, and we need to match EdgeBackGestureHandler's context. 101 */ 102 class Factory 103 @Inject 104 constructor( 105 private val windowManager: WindowManager, 106 private val viewConfiguration: ViewConfiguration, 107 @BackPanelUiThread private val uiThreadContext: UiThreadContext, 108 private val systemClock: SystemClock, 109 private val vibratorHelper: VibratorHelper, 110 private val configurationController: ConfigurationController, 111 private val latencyTracker: LatencyTracker, 112 private val interactionJankMonitor: InteractionJankMonitor, 113 ) { 114 /** Construct a [BackPanelController]. */ 115 fun create(context: Context): BackPanelController { 116 uiThreadContext.isCurrentThread() 117 return BackPanelController( 118 context, 119 windowManager, 120 viewConfiguration, 121 uiThreadContext.handler, 122 systemClock, 123 vibratorHelper, 124 configurationController, 125 latencyTracker, 126 interactionJankMonitor 127 ) 128 .also { it.init() } 129 } 130 } 131 132 @VisibleForTesting internal var params: EdgePanelParams = EdgePanelParams(resources) 133 @VisibleForTesting internal var currentState: GestureState = GestureState.GONE 134 private var previousState: GestureState = GestureState.GONE 135 136 // Screen attributes 137 private lateinit var layoutParams: WindowManager.LayoutParams 138 private val displaySize = Point() 139 140 private lateinit var backCallback: NavigationEdgeBackPlugin.BackCallback 141 private var previousXTranslationOnActiveOffset = 0f 142 private var previousXTranslation = 0f 143 private var totalTouchDeltaActive = 0f 144 private var totalTouchDeltaInactive = 0f 145 private var touchDeltaStartX = 0f 146 private var velocityTracker: VelocityTracker? = null 147 set(value) { 148 if (field != value) field?.recycle() 149 field = value 150 } 151 get() { 152 if (field == null) field = VelocityTracker.obtain() 153 return field 154 } 155 156 // The x,y position of the first touch event 157 private var startX = 0f 158 private var startY = 0f 159 private var startIsLeft: Boolean? = null 160 161 private var gestureEntryTime = 0L 162 private var gestureInactiveTime = 0L 163 164 private val elapsedTimeSinceInactive 165 get() = systemClock.uptimeMillis() - gestureInactiveTime 166 167 private val elapsedTimeSinceEntry 168 get() = systemClock.uptimeMillis() - gestureEntryTime 169 170 private var pastThresholdWhileEntryOrInactiveTime = 0L 171 private var entryToActiveDelay = 0F 172 private val entryToActiveDelayCalculation = { 173 convertVelocityToAnimationFactor( 174 valueOnFastVelocity = MIN_DURATION_ENTRY_BEFORE_ACTIVE_ANIMATION, 175 valueOnSlowVelocity = MAX_DURATION_ENTRY_BEFORE_ACTIVE_ANIMATION, 176 ) 177 } 178 179 // Whether the current gesture has moved a sufficiently large amount, 180 // so that we can unambiguously start showing the ENTRY animation 181 private var hasPassedDragSlop = false 182 183 // Distance in pixels a drag can be considered for a fling event 184 private var minFlingDistance = 0 185 186 internal val failsafeRunnable = Runnable { onFailsafe() } 187 188 internal enum class GestureState { 189 /* Arrow is off the screen and invisible */ 190 GONE, 191 192 /* Arrow is animating in */ 193 ENTRY, 194 195 /* releasing will commit back */ 196 ACTIVE, 197 198 /* releasing will cancel back */ 199 INACTIVE, 200 201 /* like committed, but animation takes longer */ 202 FLUNG, 203 204 /* back action currently occurring, arrow soon to be GONE */ 205 COMMITTED, 206 207 /* back action currently cancelling, arrow soon to be GONE */ 208 CANCELLED 209 } 210 211 /** 212 * Wrapper around OnAnimationEndListener which runs the given runnable after a delay. The 213 * runnable is not called if the animation is cancelled 214 */ 215 inner class DelayedOnAnimationEndListener 216 internal constructor( 217 private val handler: Handler, 218 private val runnableDelay: Long, 219 val runnable: Runnable, 220 ) : DynamicAnimation.OnAnimationEndListener { 221 222 override fun onAnimationEnd( 223 animation: DynamicAnimation<*>, 224 canceled: Boolean, 225 value: Float, 226 velocity: Float 227 ) { 228 animation.removeEndListener(this) 229 230 if (!canceled) { 231 // The delay between finishing this animation and starting the runnable 232 val delay = max(0, runnableDelay - elapsedTimeSinceEntry) 233 234 handler.postDelayed(runnable, delay) 235 } 236 } 237 238 internal fun run() = runnable.run() 239 } 240 241 private val onEndSetCommittedStateListener = 242 DelayedOnAnimationEndListener(mainHandler, 0L) { updateArrowState(GestureState.COMMITTED) } 243 244 private val onEndSetGoneStateListener = 245 DelayedOnAnimationEndListener(mainHandler, runnableDelay = 0L) { 246 cancelFailsafe() 247 updateArrowState(GestureState.GONE) 248 } 249 250 private val onAlphaEndSetGoneStateListener = 251 DelayedOnAnimationEndListener(mainHandler, 0L) { 252 updateRestingArrowDimens() 253 if (!mView.addAnimationEndListener(mView.backgroundAlpha, onEndSetGoneStateListener)) { 254 scheduleFailsafe() 255 } 256 } 257 258 // Minimum of the screen's width or the predefined threshold 259 private var fullyStretchedThreshold = 0f 260 261 /** Used for initialization and configuration changes */ 262 private fun updateConfiguration() { 263 params.update(resources) 264 mView.updateArrowPaint(params.arrowThickness) 265 minFlingDistance = viewConfiguration.scaledTouchSlop * 3 266 } 267 268 private val configurationListener = 269 object : ConfigurationController.ConfigurationListener { 270 override fun onConfigChanged(newConfig: Configuration?) { 271 updateConfiguration() 272 } 273 274 override fun onLayoutDirectionChanged(isLayoutRtl: Boolean) { 275 updateArrowDirection(isLayoutRtl) 276 } 277 } 278 279 override fun onViewAttached() { 280 updateConfiguration() 281 updateArrowDirection(configurationController.isLayoutRtl) 282 updateArrowState(GestureState.GONE, force = true) 283 updateRestingArrowDimens() 284 configurationController.addCallback(configurationListener) 285 } 286 287 /** Update the arrow direction. The arrow should point the same way for both panels. */ 288 private fun updateArrowDirection(isLayoutRtl: Boolean) { 289 mView.arrowsPointLeft = isLayoutRtl 290 } 291 292 override fun onViewDetached() { 293 configurationController.removeCallback(configurationListener) 294 } 295 296 override fun onMotionEvent(event: MotionEvent) { 297 velocityTracker!!.addMovement(event) 298 when (event.actionMasked) { 299 MotionEvent.ACTION_DOWN -> { 300 cancelAllPendingAnimations() 301 startX = event.x 302 startY = event.y 303 304 updateArrowState(GestureState.GONE) 305 updateYStartPosition(startY) 306 307 // reset animation properties 308 startIsLeft = mView.isLeftPanel 309 hasPassedDragSlop = false 310 mView.resetStretch() 311 } 312 MotionEvent.ACTION_MOVE -> { 313 if (dragSlopExceeded(event.x, startX)) { 314 handleMoveEvent(event) 315 } 316 } 317 MotionEvent.ACTION_UP -> { 318 when (currentState) { 319 GestureState.ENTRY -> { 320 if ( 321 isFlungAwayFromEdge(endX = event.x) || 322 previousXTranslation > params.staticTriggerThreshold 323 ) { 324 updateArrowState(GestureState.FLUNG) 325 } else { 326 updateArrowState(GestureState.CANCELLED) 327 } 328 } 329 GestureState.INACTIVE -> { 330 if (isFlungAwayFromEdge(endX = event.x)) { 331 // This is called outside of updateArrowState so that 332 // BackAnimationController can immediately evaluate state 333 // instead of after the flung delay 334 backCallback.setTriggerBack(true) 335 mainHandler.postDelayed(MIN_DURATION_INACTIVE_BEFORE_FLUNG_ANIMATION) { 336 updateArrowState(GestureState.FLUNG) 337 } 338 } else { 339 updateArrowState(GestureState.CANCELLED) 340 } 341 } 342 GestureState.ACTIVE -> { 343 if ( 344 previousState == GestureState.ENTRY && 345 elapsedTimeSinceEntry < 346 MIN_DURATION_ENTRY_TO_ACTIVE_CONSIDERED_AS_FLING 347 ) { 348 updateArrowState(GestureState.FLUNG) 349 } else if ( 350 previousState == GestureState.INACTIVE && 351 elapsedTimeSinceInactive < 352 MIN_DURATION_INACTIVE_TO_ACTIVE_CONSIDERED_AS_FLING 353 ) { 354 // A delay is added to allow the background to transition back to ACTIVE 355 // since it was briefly in INACTIVE. Without this delay, setting it 356 // immediately to COMMITTED would result in the committed animation 357 // appearing like it was playing in INACTIVE. 358 mainHandler.postDelayed(MIN_DURATION_ACTIVE_AFTER_INACTIVE_ANIMATION) { 359 updateArrowState(GestureState.COMMITTED) 360 } 361 } else { 362 updateArrowState(GestureState.COMMITTED) 363 } 364 } 365 GestureState.GONE, 366 GestureState.FLUNG, 367 GestureState.COMMITTED, 368 GestureState.CANCELLED -> { 369 updateArrowState(GestureState.CANCELLED) 370 } 371 } 372 velocityTracker = null 373 } 374 MotionEvent.ACTION_CANCEL -> { 375 // Receiving a CANCEL implies that something else intercepted 376 // the gesture, i.e., the user did not cancel their gesture. 377 // Therefore, disappear immediately, with minimum fanfare. 378 interactionJankMonitor.cancel(Cuj.CUJ_BACK_PANEL_ARROW) 379 updateArrowState(GestureState.GONE) 380 velocityTracker = null 381 } 382 } 383 } 384 385 private fun cancelAllPendingAnimations() { 386 cancelFailsafe() 387 mView.cancelAnimations() 388 mainHandler.removeCallbacks(onEndSetCommittedStateListener.runnable) 389 mainHandler.removeCallbacks(onEndSetGoneStateListener.runnable) 390 mainHandler.removeCallbacks(onAlphaEndSetGoneStateListener.runnable) 391 } 392 393 /** 394 * Returns false until the current gesture exceeds the touch slop threshold, and returns true 395 * thereafter (we reset on the subsequent back gesture). The moment it switches from false -> 396 * true is important, because that's when we switch state, from GONE -> ENTRY. 397 * 398 * @return whether the current gesture has moved past a minimum threshold. 399 */ 400 private fun dragSlopExceeded(curX: Float, startX: Float): Boolean { 401 if (hasPassedDragSlop) return true 402 403 if (abs(curX - startX) > viewConfiguration.scaledEdgeSlop) { 404 // Reset the arrow to the side 405 updateArrowState(GestureState.ENTRY) 406 407 windowManager.updateViewLayout(mView, layoutParams) 408 mView.startTrackingShowBackArrowLatency() 409 410 hasPassedDragSlop = true 411 } 412 return hasPassedDragSlop 413 } 414 415 private fun updateArrowStateOnMove(yTranslation: Float, xTranslation: Float) { 416 val isWithinYActivationThreshold = xTranslation * 2 >= yTranslation 417 val isPastStaticThreshold = xTranslation > params.staticTriggerThreshold 418 when (currentState) { 419 GestureState.ENTRY -> { 420 if ( 421 isPastThresholdToActive( 422 isPastThreshold = isPastStaticThreshold, 423 dynamicDelay = entryToActiveDelayCalculation 424 ) 425 ) { 426 updateArrowState(GestureState.ACTIVE) 427 } 428 } 429 GestureState.INACTIVE -> { 430 val isPastDynamicReactivationThreshold = 431 totalTouchDeltaInactive >= params.reactivationTriggerThreshold 432 433 if ( 434 isPastThresholdToActive( 435 isPastThreshold = 436 isPastStaticThreshold && 437 isPastDynamicReactivationThreshold && 438 isWithinYActivationThreshold, 439 delay = MIN_DURATION_INACTIVE_BEFORE_ACTIVE_ANIMATION 440 ) 441 ) { 442 updateArrowState(GestureState.ACTIVE) 443 } 444 } 445 GestureState.ACTIVE -> { 446 val isPastDynamicDeactivationThreshold = 447 totalTouchDeltaActive <= params.deactivationTriggerThreshold 448 val isMinDurationElapsed = 449 elapsedTimeSinceEntry > MIN_DURATION_ACTIVE_BEFORE_INACTIVE_ANIMATION 450 val isPastAllThresholds = 451 !isWithinYActivationThreshold || isPastDynamicDeactivationThreshold 452 if (isPastAllThresholds && isMinDurationElapsed) { 453 updateArrowState(GestureState.INACTIVE) 454 } 455 } 456 else -> {} 457 } 458 } 459 460 private fun handleMoveEvent(event: MotionEvent) { 461 val x = event.x 462 val y = event.y 463 464 val yOffset = y - startY 465 466 // How far in the y direction we are from the original touch 467 val yTranslation = abs(yOffset) 468 469 // How far in the x direction we are from the original touch ignoring motion that 470 // occurs between the screen edge and the touch start. 471 val xTranslation = max(0f, if (mView.isLeftPanel) x - startX else startX - x) 472 473 // Compared to last time, how far we moved in the x direction. If <0, we are moving closer 474 // to the edge. If >0, we are moving further from the edge 475 val xDelta = xTranslation - previousXTranslation 476 previousXTranslation = xTranslation 477 478 if (abs(xDelta) > 0) { 479 val isInSameDirection = sign(xDelta) == sign(totalTouchDeltaActive) 480 val isInDynamicRange = totalTouchDeltaActive in params.dynamicTriggerThresholdRange 481 val isTouchInContinuousDirection = isInSameDirection || isInDynamicRange 482 483 if (isTouchInContinuousDirection) { 484 // Direction has NOT changed, so keep counting the delta 485 totalTouchDeltaActive += xDelta 486 } else { 487 // Direction has changed, so reset the delta 488 totalTouchDeltaActive = xDelta 489 touchDeltaStartX = x 490 } 491 492 // Add a slop to to prevent small jitters when arrow is at edge in 493 // emitting small values that cause the arrow to poke out slightly 494 val minimumDelta = -viewConfiguration.scaledTouchSlop.toFloat() 495 totalTouchDeltaInactive = 496 totalTouchDeltaInactive.plus(xDelta).coerceAtLeast(minimumDelta) 497 } 498 499 updateArrowStateOnMove(yTranslation, xTranslation) 500 501 val gestureProgress = 502 when (currentState) { 503 GestureState.ACTIVE -> fullScreenProgress(xTranslation) 504 GestureState.ENTRY -> staticThresholdProgress(xTranslation) 505 GestureState.INACTIVE -> reactivationThresholdProgress(totalTouchDeltaInactive) 506 else -> null 507 } 508 509 gestureProgress?.let { 510 when (currentState) { 511 GestureState.ACTIVE -> stretchActiveBackIndicator(gestureProgress) 512 GestureState.ENTRY -> stretchEntryBackIndicator(gestureProgress) 513 GestureState.INACTIVE -> stretchInactiveBackIndicator(gestureProgress) 514 else -> {} 515 } 516 } 517 518 setArrowStrokeAlpha(gestureProgress) 519 setVerticalTranslation(yOffset) 520 } 521 522 private fun setArrowStrokeAlpha(gestureProgress: Float?) { 523 val strokeAlphaProgress = 524 when (currentState) { 525 GestureState.ENTRY -> gestureProgress 526 GestureState.INACTIVE -> gestureProgress 527 GestureState.ACTIVE, 528 GestureState.FLUNG, 529 GestureState.COMMITTED -> 1f 530 GestureState.CANCELLED, 531 GestureState.GONE -> 0f 532 } 533 534 val indicator = 535 when (currentState) { 536 GestureState.ENTRY -> params.entryIndicator 537 GestureState.INACTIVE -> params.preThresholdIndicator 538 GestureState.ACTIVE -> params.activeIndicator 539 else -> params.preThresholdIndicator 540 } 541 542 strokeAlphaProgress?.let { progress -> 543 indicator.arrowDimens.alphaSpring 544 ?.get(progress) 545 ?.takeIf { it.isNewState } 546 ?.let { mView.popArrowAlpha(0f, it.value) } 547 } 548 } 549 550 private fun setVerticalTranslation(yOffset: Float) { 551 val yTranslation = abs(yOffset) 552 val maxYOffset = (mView.height - params.entryIndicator.backgroundDimens.height) / 2f 553 val rubberbandAmount = 15f 554 val yProgress = MathUtils.saturate(yTranslation / (maxYOffset * rubberbandAmount)) 555 val yPosition = 556 params.verticalTranslationInterpolator.getInterpolation(yProgress) * 557 maxYOffset * 558 sign(yOffset) 559 mView.animateVertically(yPosition) 560 } 561 562 /** 563 * Tracks the relative position of the drag from the time after the arrow is activated until the 564 * arrow is fully stretched (between 0.0 - 1.0f) 565 */ 566 private fun fullScreenProgress(xTranslation: Float): Float { 567 val progress = (xTranslation - previousXTranslationOnActiveOffset) / fullyStretchedThreshold 568 return MathUtils.saturate(progress) 569 } 570 571 /** 572 * Tracks the relative position of the drag from the entry until the threshold where the arrow 573 * activates (between 0.0 - 1.0f) 574 */ 575 private fun staticThresholdProgress(xTranslation: Float): Float { 576 return MathUtils.saturate(xTranslation / params.staticTriggerThreshold) 577 } 578 579 private fun reactivationThresholdProgress(totalTouchDelta: Float): Float { 580 return MathUtils.saturate(totalTouchDelta / params.reactivationTriggerThreshold) 581 } 582 583 private fun stretchActiveBackIndicator(progress: Float) { 584 mView.setStretch( 585 horizontalTranslationStretchAmount = 586 params.horizontalTranslationInterpolator.getInterpolation(progress), 587 arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress), 588 backgroundWidthStretchAmount = 589 params.activeWidthInterpolator.getInterpolation(progress), 590 backgroundAlphaStretchAmount = 1f, 591 backgroundHeightStretchAmount = 1f, 592 arrowAlphaStretchAmount = 1f, 593 edgeCornerStretchAmount = 1f, 594 farCornerStretchAmount = 1f, 595 fullyStretchedDimens = params.fullyStretchedIndicator 596 ) 597 } 598 599 private fun stretchEntryBackIndicator(progress: Float) { 600 mView.setStretch( 601 horizontalTranslationStretchAmount = 0f, 602 arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress), 603 backgroundWidthStretchAmount = params.entryWidthInterpolator.getInterpolation(progress), 604 backgroundHeightStretchAmount = params.heightInterpolator.getInterpolation(progress), 605 backgroundAlphaStretchAmount = 1f, 606 arrowAlphaStretchAmount = 607 params.entryIndicator.arrowDimens.alphaInterpolator?.get(progress)?.value ?: 0f, 608 edgeCornerStretchAmount = params.edgeCornerInterpolator.getInterpolation(progress), 609 farCornerStretchAmount = params.farCornerInterpolator.getInterpolation(progress), 610 fullyStretchedDimens = params.preThresholdIndicator 611 ) 612 } 613 614 private var previousPreThresholdWidthInterpolator = params.entryWidthInterpolator 615 616 private fun preThresholdWidthStretchAmount(progress: Float): Float { 617 val interpolator = run { 618 val isPastSlop = totalTouchDeltaInactive > viewConfiguration.scaledTouchSlop 619 if (isPastSlop) { 620 if (totalTouchDeltaInactive > 0) { 621 params.entryWidthInterpolator 622 } else { 623 params.entryWidthTowardsEdgeInterpolator 624 } 625 } else { 626 previousPreThresholdWidthInterpolator 627 } 628 .also { previousPreThresholdWidthInterpolator = it } 629 } 630 return interpolator.getInterpolation(progress).coerceAtLeast(0f) 631 } 632 633 private fun stretchInactiveBackIndicator(progress: Float) { 634 mView.setStretch( 635 horizontalTranslationStretchAmount = 0f, 636 arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress), 637 backgroundWidthStretchAmount = preThresholdWidthStretchAmount(progress), 638 backgroundHeightStretchAmount = params.heightInterpolator.getInterpolation(progress), 639 backgroundAlphaStretchAmount = 1f, 640 arrowAlphaStretchAmount = 641 params.preThresholdIndicator.arrowDimens.alphaInterpolator?.get(progress)?.value 642 ?: 0f, 643 edgeCornerStretchAmount = params.edgeCornerInterpolator.getInterpolation(progress), 644 farCornerStretchAmount = params.farCornerInterpolator.getInterpolation(progress), 645 fullyStretchedDimens = params.preThresholdIndicator 646 ) 647 } 648 649 override fun onDestroy() { 650 cancelFailsafe() 651 windowManager.removeView(mView) 652 } 653 654 override fun setIsLeftPanel(isLeftPanel: Boolean) { 655 mView.isLeftPanel = isLeftPanel 656 layoutParams.gravity = 657 if (isLeftPanel) { 658 Gravity.LEFT or Gravity.TOP 659 } else { 660 Gravity.RIGHT or Gravity.TOP 661 } 662 } 663 664 override fun setInsets(insetLeft: Int, insetRight: Int) = Unit 665 666 override fun setBackCallback(callback: NavigationEdgeBackPlugin.BackCallback) { 667 backCallback = callback 668 } 669 670 override fun setLayoutParams(layoutParams: WindowManager.LayoutParams) { 671 this.layoutParams = layoutParams 672 windowManager.addView(mView, layoutParams) 673 } 674 675 private fun isFlungAwayFromEdge(endX: Float, startX: Float = touchDeltaStartX): Boolean { 676 val flingDistance = if (mView.isLeftPanel) endX - startX else startX - endX 677 val flingVelocity = 678 velocityTracker?.run { 679 computeCurrentVelocity(PX_PER_SEC) 680 xVelocity.takeIf { mView.isLeftPanel } ?: (xVelocity * -1) 681 } ?: 0f 682 val isPastFlingVelocityThreshold = 683 flingVelocity > viewConfiguration.scaledMinimumFlingVelocity 684 return flingDistance > minFlingDistance && isPastFlingVelocityThreshold 685 } 686 687 private fun isPastThresholdToActive( 688 isPastThreshold: Boolean, 689 delay: Float? = null, 690 dynamicDelay: () -> Float = { delay ?: 0F } 691 ): Boolean { 692 val resetValue = 0L 693 val isPastThresholdForFirstTime = pastThresholdWhileEntryOrInactiveTime == resetValue 694 695 if (!isPastThreshold) { 696 pastThresholdWhileEntryOrInactiveTime = resetValue 697 return false 698 } 699 700 if (isPastThresholdForFirstTime) { 701 pastThresholdWhileEntryOrInactiveTime = systemClock.uptimeMillis() 702 entryToActiveDelay = dynamicDelay() 703 } 704 val timePastThreshold = systemClock.uptimeMillis() - pastThresholdWhileEntryOrInactiveTime 705 706 return timePastThreshold > entryToActiveDelay 707 } 708 709 private fun playWithBackgroundWidthAnimation( 710 onEnd: DelayedOnAnimationEndListener, 711 delay: Long = 0L 712 ) { 713 if (delay == 0L) { 714 updateRestingArrowDimens() 715 if (!mView.addAnimationEndListener(mView.backgroundWidth, onEnd)) { 716 scheduleFailsafe() 717 } 718 } else { 719 mainHandler.postDelayed(delay) { playWithBackgroundWidthAnimation(onEnd, delay = 0L) } 720 } 721 } 722 723 private fun updateYStartPosition(touchY: Float) { 724 var yPosition = touchY - params.fingerOffset 725 yPosition = max(yPosition, params.minArrowYPosition.toFloat()) 726 yPosition -= layoutParams.height / 2.0f 727 layoutParams.y = MathUtils.constrain(yPosition.toInt(), 0, displaySize.y) 728 } 729 730 override fun setDisplaySize(displaySize: Point) { 731 this.displaySize.set(displaySize.x, displaySize.y) 732 fullyStretchedThreshold = min(displaySize.x.toFloat(), params.swipeProgressThreshold) 733 } 734 735 /** Updates resting arrow and background size not accounting for stretch */ 736 private fun updateRestingArrowDimens() { 737 when (currentState) { 738 GestureState.GONE, 739 GestureState.ENTRY -> { 740 mView.setSpring( 741 arrowLength = params.entryIndicator.arrowDimens.lengthSpring, 742 arrowHeight = params.entryIndicator.arrowDimens.heightSpring, 743 scale = params.entryIndicator.scaleSpring, 744 verticalTranslation = params.entryIndicator.verticalTranslationSpring, 745 horizontalTranslation = params.entryIndicator.horizontalTranslationSpring, 746 backgroundAlpha = params.entryIndicator.backgroundDimens.alphaSpring, 747 backgroundWidth = params.entryIndicator.backgroundDimens.widthSpring, 748 backgroundHeight = params.entryIndicator.backgroundDimens.heightSpring, 749 backgroundEdgeCornerRadius = 750 params.entryIndicator.backgroundDimens.edgeCornerRadiusSpring, 751 backgroundFarCornerRadius = 752 params.entryIndicator.backgroundDimens.farCornerRadiusSpring, 753 ) 754 } 755 GestureState.INACTIVE -> { 756 mView.setSpring( 757 arrowLength = params.preThresholdIndicator.arrowDimens.lengthSpring, 758 arrowHeight = params.preThresholdIndicator.arrowDimens.heightSpring, 759 horizontalTranslation = 760 params.preThresholdIndicator.horizontalTranslationSpring, 761 scale = params.preThresholdIndicator.scaleSpring, 762 backgroundWidth = params.preThresholdIndicator.backgroundDimens.widthSpring, 763 backgroundHeight = params.preThresholdIndicator.backgroundDimens.heightSpring, 764 backgroundEdgeCornerRadius = 765 params.preThresholdIndicator.backgroundDimens.edgeCornerRadiusSpring, 766 backgroundFarCornerRadius = 767 params.preThresholdIndicator.backgroundDimens.farCornerRadiusSpring, 768 ) 769 } 770 GestureState.ACTIVE -> { 771 mView.setSpring( 772 arrowLength = params.activeIndicator.arrowDimens.lengthSpring, 773 arrowHeight = params.activeIndicator.arrowDimens.heightSpring, 774 scale = params.activeIndicator.scaleSpring, 775 horizontalTranslation = params.activeIndicator.horizontalTranslationSpring, 776 backgroundWidth = params.activeIndicator.backgroundDimens.widthSpring, 777 backgroundHeight = params.activeIndicator.backgroundDimens.heightSpring, 778 backgroundEdgeCornerRadius = 779 params.activeIndicator.backgroundDimens.edgeCornerRadiusSpring, 780 backgroundFarCornerRadius = 781 params.activeIndicator.backgroundDimens.farCornerRadiusSpring, 782 ) 783 } 784 GestureState.FLUNG -> { 785 mView.setSpring( 786 arrowLength = params.flungIndicator.arrowDimens.lengthSpring, 787 arrowHeight = params.flungIndicator.arrowDimens.heightSpring, 788 backgroundWidth = params.flungIndicator.backgroundDimens.widthSpring, 789 backgroundHeight = params.flungIndicator.backgroundDimens.heightSpring, 790 backgroundEdgeCornerRadius = 791 params.flungIndicator.backgroundDimens.edgeCornerRadiusSpring, 792 backgroundFarCornerRadius = 793 params.flungIndicator.backgroundDimens.farCornerRadiusSpring, 794 ) 795 } 796 GestureState.COMMITTED -> { 797 mView.setSpring( 798 arrowLength = params.committedIndicator.arrowDimens.lengthSpring, 799 arrowHeight = params.committedIndicator.arrowDimens.heightSpring, 800 scale = params.committedIndicator.scaleSpring, 801 backgroundAlpha = params.committedIndicator.backgroundDimens.alphaSpring, 802 backgroundWidth = params.committedIndicator.backgroundDimens.widthSpring, 803 backgroundHeight = params.committedIndicator.backgroundDimens.heightSpring, 804 backgroundEdgeCornerRadius = 805 params.committedIndicator.backgroundDimens.edgeCornerRadiusSpring, 806 backgroundFarCornerRadius = 807 params.committedIndicator.backgroundDimens.farCornerRadiusSpring, 808 ) 809 } 810 GestureState.CANCELLED -> { 811 mView.setSpring( 812 backgroundAlpha = params.cancelledIndicator.backgroundDimens.alphaSpring 813 ) 814 } 815 else -> {} 816 } 817 818 mView.setRestingDimens( 819 animate = 820 !(currentState == GestureState.FLUNG || currentState == GestureState.COMMITTED), 821 restingParams = 822 EdgePanelParams.BackIndicatorDimens( 823 scale = 824 when (currentState) { 825 GestureState.ACTIVE, 826 GestureState.FLUNG -> params.activeIndicator.scale 827 GestureState.COMMITTED -> params.committedIndicator.scale 828 else -> params.preThresholdIndicator.scale 829 }, 830 scalePivotX = 831 when (currentState) { 832 GestureState.GONE, 833 GestureState.ENTRY, 834 GestureState.INACTIVE, 835 GestureState.CANCELLED -> params.preThresholdIndicator.scalePivotX 836 GestureState.ACTIVE -> params.activeIndicator.scalePivotX 837 GestureState.FLUNG, 838 GestureState.COMMITTED -> params.committedIndicator.scalePivotX 839 }, 840 horizontalTranslation = 841 when (currentState) { 842 GestureState.GONE -> { 843 params.activeIndicator.backgroundDimens.width?.times(-1) 844 } 845 GestureState.ENTRY, 846 GestureState.INACTIVE -> params.entryIndicator.horizontalTranslation 847 GestureState.FLUNG -> params.activeIndicator.horizontalTranslation 848 GestureState.ACTIVE -> params.activeIndicator.horizontalTranslation 849 GestureState.CANCELLED -> { 850 params.cancelledIndicator.horizontalTranslation 851 } 852 else -> null 853 }, 854 arrowDimens = 855 when (currentState) { 856 GestureState.GONE, 857 GestureState.ENTRY, 858 GestureState.INACTIVE -> params.entryIndicator.arrowDimens 859 GestureState.ACTIVE -> params.activeIndicator.arrowDimens 860 GestureState.FLUNG -> params.flungIndicator.arrowDimens 861 GestureState.COMMITTED -> params.committedIndicator.arrowDimens 862 GestureState.CANCELLED -> params.cancelledIndicator.arrowDimens 863 }, 864 backgroundDimens = 865 when (currentState) { 866 GestureState.GONE, 867 GestureState.ENTRY, 868 GestureState.INACTIVE -> params.entryIndicator.backgroundDimens 869 GestureState.ACTIVE -> params.activeIndicator.backgroundDimens 870 GestureState.FLUNG -> params.activeIndicator.backgroundDimens 871 GestureState.COMMITTED -> params.committedIndicator.backgroundDimens 872 GestureState.CANCELLED -> params.cancelledIndicator.backgroundDimens 873 } 874 ) 875 ) 876 } 877 878 /** 879 * Update arrow state. If state has not changed, this is a no-op. 880 * 881 * Transitioning to active/inactive will indicate whether or not releasing touch will trigger 882 * the back action. 883 */ 884 private fun updateArrowState(newState: GestureState, force: Boolean = false) { 885 if (!force && currentState == newState) return 886 887 previousState = currentState 888 currentState = newState 889 890 // First, update the jank tracker 891 when (currentState) { 892 GestureState.ENTRY -> { 893 interactionJankMonitor.cancel(Cuj.CUJ_BACK_PANEL_ARROW) 894 interactionJankMonitor.begin(mView, Cuj.CUJ_BACK_PANEL_ARROW) 895 } 896 GestureState.GONE -> interactionJankMonitor.end(Cuj.CUJ_BACK_PANEL_ARROW) 897 else -> {} 898 } 899 900 when (currentState) { 901 GestureState.CANCELLED -> { 902 backCallback.cancelBack() 903 } 904 GestureState.FLUNG, 905 GestureState.COMMITTED -> { 906 // When flung, trigger back immediately but don't fire again 907 // once state resolves to committed. 908 if (previousState != GestureState.FLUNG) backCallback.triggerBack() 909 } 910 GestureState.ENTRY, 911 GestureState.INACTIVE -> { 912 backCallback.setTriggerBack(false) 913 } 914 GestureState.ACTIVE -> { 915 backCallback.setTriggerBack(true) 916 } 917 GestureState.GONE -> {} 918 } 919 920 when (currentState) { 921 // Transitioning to GONE never animates since the arrow is (presumably) already off the 922 // screen 923 GestureState.GONE -> { 924 updateRestingArrowDimens() 925 mView.isVisible = false 926 } 927 GestureState.ENTRY -> { 928 mView.isVisible = true 929 930 updateRestingArrowDimens() 931 gestureEntryTime = systemClock.uptimeMillis() 932 } 933 GestureState.ACTIVE -> { 934 previousXTranslationOnActiveOffset = previousXTranslation 935 updateRestingArrowDimens() 936 performActivatedHapticFeedback() 937 val popVelocity = 938 if (previousState == GestureState.INACTIVE) { 939 POP_ON_INACTIVE_TO_ACTIVE_VELOCITY 940 } else { 941 POP_ON_ENTRY_TO_ACTIVE_VELOCITY 942 } 943 mView.popOffEdge(popVelocity) 944 } 945 GestureState.INACTIVE -> { 946 gestureInactiveTime = systemClock.uptimeMillis() 947 948 // Typically entering INACTIVE means 949 // totalTouchDelta <= deactivationSwipeTriggerThreshold 950 // but because we can also independently enter this state 951 // if touch Y >> touch X, we force it to deactivationSwipeTriggerThreshold 952 // so that gesture progress in this state is consistent regardless of entry 953 totalTouchDeltaInactive = params.deactivationTriggerThreshold 954 955 mView.popOffEdge(POP_ON_INACTIVE_VELOCITY) 956 957 performDeactivatedHapticFeedback() 958 updateRestingArrowDimens() 959 } 960 GestureState.FLUNG -> { 961 // Typically a vibration is only played while transitioning to ACTIVE. However there 962 // are instances where a fling to trigger back occurs while not in that state. 963 // (e.g. A fling is detected before crossing the trigger threshold.) 964 if (previousState != GestureState.ACTIVE) { 965 performActivatedHapticFeedback() 966 } 967 mainHandler.postDelayed(POP_ON_FLING_DELAY) { 968 mView.popScale(POP_ON_FLING_VELOCITY) 969 } 970 mainHandler.postDelayed( 971 onEndSetCommittedStateListener.runnable, 972 MIN_DURATION_FLING_ANIMATION 973 ) 974 updateRestingArrowDimens() 975 } 976 GestureState.COMMITTED -> { 977 // In most cases, animating between states is handled via `updateRestingArrowDimens` 978 // which plays an animation immediately upon state change. Some animations however 979 // occur after a delay upon state change and these animations may be independent 980 // or non-sequential from the state change animation. `postDelayed` is used to 981 // manually play these kinds of animations in parallel. 982 if (previousState == GestureState.FLUNG) { 983 updateRestingArrowDimens() 984 mainHandler.postDelayed( 985 onEndSetGoneStateListener.runnable, 986 MIN_DURATION_COMMITTED_AFTER_FLING_ANIMATION 987 ) 988 } else { 989 mView.popScale(POP_ON_COMMITTED_VELOCITY) 990 mainHandler.postDelayed( 991 onAlphaEndSetGoneStateListener.runnable, 992 MIN_DURATION_COMMITTED_ANIMATION 993 ) 994 } 995 } 996 GestureState.CANCELLED -> { 997 val delay = max(0, MIN_DURATION_CANCELLED_ANIMATION - elapsedTimeSinceEntry) 998 playWithBackgroundWidthAnimation(onEndSetGoneStateListener, delay) 999 1000 val springForceOnCancelled = 1001 params.cancelledIndicator.arrowDimens.alphaSpring?.get(0f)?.value 1002 mView.popArrowAlpha(0f, springForceOnCancelled) 1003 } 1004 } 1005 } 1006 1007 private fun performDeactivatedHapticFeedback() { 1008 vibratorHelper.performHapticFeedback( 1009 mView, 1010 HapticFeedbackConstants.GESTURE_THRESHOLD_DEACTIVATE 1011 ) 1012 } 1013 1014 private fun performActivatedHapticFeedback() { 1015 vibratorHelper.performHapticFeedback( 1016 mView, 1017 HapticFeedbackConstants.GESTURE_THRESHOLD_ACTIVATE 1018 ) 1019 } 1020 1021 private fun convertVelocityToAnimationFactor( 1022 valueOnFastVelocity: Float, 1023 valueOnSlowVelocity: Float, 1024 fastVelocityBound: Float = 1f, 1025 slowVelocityBound: Float = 0.5f, 1026 ): Float { 1027 val factor = 1028 velocityTracker?.run { 1029 computeCurrentVelocity(PX_PER_MS) 1030 MathUtils.smoothStep(slowVelocityBound, fastVelocityBound, abs(xVelocity)) 1031 } ?: valueOnFastVelocity 1032 1033 return MathUtils.lerp(valueOnFastVelocity, valueOnSlowVelocity, 1 - factor) 1034 } 1035 1036 private fun scheduleFailsafe() { 1037 if (!ENABLE_FAILSAFE) return 1038 cancelFailsafe() 1039 if (DEBUG) Log.d(TAG, "scheduleFailsafe") 1040 mainHandler.postDelayed(failsafeRunnable, FAILSAFE_DELAY_MS) 1041 } 1042 1043 private fun cancelFailsafe() { 1044 if (DEBUG) Log.d(TAG, "cancelFailsafe") 1045 mainHandler.removeCallbacks(failsafeRunnable) 1046 } 1047 1048 private fun onFailsafe() { 1049 if (DEBUG) Log.d(TAG, "onFailsafe") 1050 updateArrowState(GestureState.GONE, force = true) 1051 } 1052 1053 override fun dump(pw: PrintWriter) { 1054 pw.println("$TAG:") 1055 pw.println(" currentState=$currentState") 1056 pw.println(" isLeftPanel=${mView.isLeftPanel}") 1057 } 1058 1059 @VisibleForTesting 1060 internal fun getBackPanelView(): BackPanel { 1061 return mView 1062 } 1063 1064 init { 1065 if (DEBUG) 1066 mView.drawDebugInfo = { canvas -> 1067 val preProgress = staticThresholdProgress(previousXTranslation) * 100 1068 val postProgress = fullScreenProgress(previousXTranslation) * 100 1069 val debugStrings = 1070 listOf( 1071 "$currentState", 1072 "startX=$startX", 1073 "startY=$startY", 1074 "xDelta=${"%.1f".format(totalTouchDeltaActive)}", 1075 "xTranslation=${"%.1f".format(previousXTranslation)}", 1076 "pre=${"%.0f".format(preProgress)}%", 1077 "post=${"%.0f".format(postProgress)}%" 1078 ) 1079 val debugPaint = Paint().apply { color = Color.WHITE } 1080 val debugInfoBottom = debugStrings.size * 32f + 4f 1081 canvas.drawRect( 1082 4f, 1083 4f, 1084 canvas.width.toFloat(), 1085 debugStrings.size * 32f + 4f, 1086 debugPaint 1087 ) 1088 debugPaint.apply { 1089 color = Color.BLACK 1090 textSize = 32f 1091 } 1092 var offset = 32f 1093 for (debugText in debugStrings) { 1094 canvas.drawText(debugText, 10f, offset, debugPaint) 1095 offset += 32f 1096 } 1097 debugPaint.apply { 1098 color = Color.RED 1099 style = Paint.Style.STROKE 1100 strokeWidth = 4f 1101 } 1102 val canvasWidth = canvas.width.toFloat() 1103 val canvasHeight = canvas.height.toFloat() 1104 canvas.drawRect(0f, 0f, canvasWidth, canvasHeight, debugPaint) 1105 1106 fun drawVerticalLine(x: Float, color: Int) { 1107 debugPaint.color = color 1108 val x = if (mView.isLeftPanel) x else canvasWidth - x 1109 canvas.drawLine(x, debugInfoBottom, x, canvas.height.toFloat(), debugPaint) 1110 } 1111 1112 drawVerticalLine(x = params.staticTriggerThreshold, color = Color.BLUE) 1113 drawVerticalLine(x = params.deactivationTriggerThreshold, color = Color.BLUE) 1114 drawVerticalLine(x = startX, color = Color.GREEN) 1115 drawVerticalLine(x = previousXTranslation, color = Color.DKGRAY) 1116 } 1117 } 1118 } 1119 1120 /** 1121 * In addition to a typical step function which returns one or two values based on a threshold, 1122 * `Step` also gracefully handles quick changes in input near the threshold value that would 1123 * typically result in the output rapidly changing. 1124 * 1125 * In the context of Back arrow, the arrow's stroke opacity should always appear transparent or 1126 * opaque. Using a typical Step function, this would resulting in a flickering appearance as the 1127 * output would change rapidly. `Step` addresses this by moving the threshold after it is crossed so 1128 * it cannot be easily crossed again with small changes in touch events. 1129 */ 1130 class Step<T>( 1131 private val threshold: Float, 1132 private val factor: Float = 1.1f, 1133 private val postThreshold: T, 1134 private val preThreshold: T 1135 ) { 1136 1137 data class Value<T>(val value: T, val isNewState: Boolean) 1138 1139 private val lowerFactor = 2 - factor 1140 1141 private lateinit var startValue: Value<T> 1142 private lateinit var previousValue: Value<T> 1143 private var hasCrossedUpperBoundAtLeastOnce = false 1144 private var progress: Float = 0f 1145 1146 init { 1147 reset() 1148 } 1149 resetnull1150 fun reset() { 1151 hasCrossedUpperBoundAtLeastOnce = false 1152 progress = 0f 1153 startValue = Value(preThreshold, false) 1154 previousValue = startValue 1155 } 1156 getnull1157 fun get(progress: Float): Value<T> { 1158 this.progress = progress 1159 1160 val hasCrossedUpperBound = progress > threshold * factor 1161 val hasCrossedLowerBound = progress > threshold * lowerFactor 1162 1163 return when { 1164 hasCrossedUpperBound && !hasCrossedUpperBoundAtLeastOnce -> { 1165 hasCrossedUpperBoundAtLeastOnce = true 1166 Value(postThreshold, true) 1167 } 1168 hasCrossedLowerBound -> previousValue.copy(isNewState = false) 1169 hasCrossedUpperBoundAtLeastOnce -> { 1170 hasCrossedUpperBoundAtLeastOnce = false 1171 Value(preThreshold, true) 1172 } 1173 else -> startValue 1174 }.also { previousValue = it } 1175 } 1176 } 1177