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