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