• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2025 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.quickstep.views
18 
19 import android.os.VibrationAttributes
20 import androidx.dynamicanimation.animation.FloatPropertyCompat
21 import androidx.dynamicanimation.animation.FloatValueHolder
22 import androidx.dynamicanimation.animation.SpringAnimation
23 import androidx.dynamicanimation.animation.SpringForce
24 import com.android.launcher3.Flags.enableGridOnlyOverview
25 import com.android.launcher3.R
26 import com.android.launcher3.Utilities.boundToRange
27 import com.android.launcher3.util.DynamicResource
28 import com.android.launcher3.util.MSDLPlayerWrapper
29 import com.android.quickstep.util.TaskGridNavHelper
30 import com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY
31 import com.google.android.msdl.data.model.MSDLToken
32 import com.google.android.msdl.domain.InteractionProperties
33 import kotlin.math.abs
34 import kotlin.math.roundToInt
35 import kotlin.math.sign
36 
37 /**
38  * Helper class for [RecentsView]. This util class contains refactored and extracted functions from
39  * RecentsView related to TaskView dismissal.
40  */
41 class RecentsDismissUtils(private val recentsView: RecentsView<*, *>) {
42 
43     /**
44      * Creates the spring animations which run when a dragged task view in overview is released.
45      *
46      * <p>When a task dismiss is cancelled, the task will return to its original position via a
47      * spring animation. As it passes the threshold of its settling state, its neighbors will spring
48      * in response to the perceived impact of the settling task.
49      */
50     fun createTaskDismissSettlingSpringAnimation(
51         draggedTaskView: TaskView?,
52         velocity: Float,
53         isDismissing: Boolean,
54         dismissLength: Int,
55         onEndRunnable: () -> Unit,
56     ): SpringAnimation? {
57         draggedTaskView ?: return null
58         val taskDismissFloatProperty =
59             FloatPropertyCompat.createFloatPropertyCompat(
60                 draggedTaskView.secondaryDismissTranslationProperty
61             )
62         val minVelocity =
63             recentsView.pagedOrientationHandler.getSecondaryDimension(draggedTaskView).toFloat()
64         val startVelocity = abs(velocity).coerceAtLeast(minVelocity) * velocity.sign
65         // Animate dragged task towards dismissal or rest state.
66         val draggedTaskViewSpringAnimation =
67             SpringAnimation(draggedTaskView, taskDismissFloatProperty)
68                 .setSpring(createExpressiveDismissSpringForce())
69                 .setStartVelocity(startVelocity)
70                 .addUpdateListener { animation, value, _ ->
71                     if (isDismissing && abs(value) >= abs(dismissLength)) {
72                         animation.cancel()
73                     } else if (draggedTaskView.isRunningTask && recentsView.enableDrawingLiveTile) {
74                         recentsView.runActionOnRemoteHandles { remoteTargetHandle ->
75                             remoteTargetHandle.taskViewSimulator.taskSecondaryTranslation.value =
76                                 taskDismissFloatProperty.getValue(draggedTaskView)
77                         }
78                         recentsView.redrawLiveTile()
79                     }
80                 }
81                 .addEndListener { _, _, _, _ ->
82                     if (isDismissing) {
83                         if (!recentsView.showAsGrid() || enableGridOnlyOverview()) {
84                             runTaskGridReflowSpringAnimation(
85                                 draggedTaskView,
86                                 getDismissedTaskGapForReflow(draggedTaskView),
87                                 onEndRunnable,
88                             )
89                         } else {
90                             recentsView.dismissTaskView(
91                                 draggedTaskView,
92                                 /* animateTaskView = */ false,
93                                 /* removeTask = */ true,
94                             )
95                             onEndRunnable()
96                         }
97                     } else {
98                         recentsView.onDismissAnimationEnds()
99                         onEndRunnable()
100                     }
101                 }
102         if (!isDismissing) {
103             addNeighborSettlingSpringAnimations(
104                 draggedTaskView,
105                 draggedTaskViewSpringAnimation,
106                 driverProgressThreshold = 0f,
107                 isSpringDirectionVertical = true,
108                 minVelocity = startVelocity,
109             )
110         }
111         return draggedTaskViewSpringAnimation
112     }
113 
114     private fun addNeighborSettlingSpringAnimations(
115         draggedTaskView: TaskView,
116         springAnimationDriver: SpringAnimation,
117         tasksToExclude: List<TaskView> = emptyList(),
118         driverProgressThreshold: Float,
119         isSpringDirectionVertical: Boolean,
120         minVelocity: Float,
121     ) {
122         // Empty spring animation exists for conditional start, and to drive neighboring springs.
123         val neighborsToSettle =
124             SpringAnimation(FloatValueHolder()).setSpring(createExpressiveDismissSpringForce())
125 
126         // Add tasks before dragged index, fanning out from the dragged task.
127         // The order they are added matters, as each spring drives the next.
128         var previousNeighbor = neighborsToSettle
129         getTasksOffsetPairAdjacentToDraggedTask(draggedTaskView, towardsStart = true)
130             .filter { (taskView, _) -> !tasksToExclude.contains(taskView) }
131             .forEach { (taskView, offset) ->
132                 previousNeighbor =
133                     createNeighboringTaskViewSpringAnimation(
134                         taskView,
135                         offset * ADDITIONAL_DISMISS_DAMPING_RATIO,
136                         previousNeighbor,
137                         isSpringDirectionVertical,
138                     )
139             }
140         // Add tasks after dragged index, fanning out from the dragged task.
141         // The order they are added matters, as each spring drives the next.
142         previousNeighbor = neighborsToSettle
143         getTasksOffsetPairAdjacentToDraggedTask(draggedTaskView, towardsStart = false)
144             .filter { (taskView, _) -> !tasksToExclude.contains(taskView) }
145             .forEach { (taskView, offset) ->
146                 previousNeighbor =
147                     createNeighboringTaskViewSpringAnimation(
148                         taskView,
149                         offset * ADDITIONAL_DISMISS_DAMPING_RATIO,
150                         previousNeighbor,
151                         isSpringDirectionVertical,
152                     )
153             }
154 
155         val isCurrentDisplacementAboveOrigin =
156             recentsView.pagedOrientationHandler.isGoingUp(
157                 draggedTaskView.secondaryDismissTranslationProperty.get(draggedTaskView),
158                 recentsView.isRtl,
159             )
160         addThresholdSpringAnimationTrigger(
161             springAnimationDriver,
162             progressThreshold = driverProgressThreshold,
163             neighborsToSettle,
164             isCurrentDisplacementAboveOrigin,
165             minVelocity,
166         )
167     }
168 
169     /** As spring passes threshold for the first time, run conditional spring with velocity. */
170     private fun addThresholdSpringAnimationTrigger(
171         springAnimationDriver: SpringAnimation,
172         progressThreshold: Float,
173         conditionalSpring: SpringAnimation,
174         isCurrentDisplacementAboveOrigin: Boolean,
175         minVelocity: Float,
176     ) {
177         val runSettlingAtVelocity = { velocity: Float ->
178             conditionalSpring.setStartVelocity(velocity).animateToFinalPosition(0f)
179             playDismissSettlingHaptic(velocity)
180         }
181         if (isCurrentDisplacementAboveOrigin) {
182             var lastPosition = 0f
183             var startSettling = false
184             springAnimationDriver.addUpdateListener { _, value, velocity ->
185                 // We do not compare to the threshold directly, as the update listener
186                 // does not necessarily hit every value. Do not check again once it has started
187                 // settling, as a spring can bounce past the end value multiple times.
188                 if (startSettling) return@addUpdateListener
189                 if (
190                     lastPosition < progressThreshold && value >= progressThreshold ||
191                         lastPosition > progressThreshold && value <= progressThreshold
192                 ) {
193                     startSettling = true
194                 }
195                 lastPosition = value
196                 if (startSettling) {
197                     runSettlingAtVelocity(velocity)
198                 }
199             }
200         } else {
201             // Run settling animations immediately when displacement is already below settled state.
202             runSettlingAtVelocity(minVelocity)
203         }
204     }
205 
206     /**
207      * Gets pairs of (TaskView, offset) adjacent the dragged task in visual order.
208      *
209      * <p>Gets tasks either before or after the dragged task along with their offset from it. The
210      * offset is the distance between indices for carousels, or distance between columns for grids.
211      */
212     private fun getTasksOffsetPairAdjacentToDraggedTask(
213         draggedTaskView: TaskView,
214         towardsStart: Boolean,
215     ): Sequence<Pair<TaskView, Int>> {
216         if (recentsView.showAsGrid()) {
217             val taskGridNavHelper =
218                 TaskGridNavHelper(
219                     recentsView.mUtils.getTopRowIdArray(),
220                     recentsView.mUtils.getBottomRowIdArray(),
221                     recentsView.mUtils.getLargeTaskViewIds(),
222                     hasAddDesktopButton = false,
223                 )
224             return taskGridNavHelper
225                 .gridTaskViewIdOffsetPairInTabOrderSequence(
226                     draggedTaskView.taskViewId,
227                     towardsStart,
228                 )
229                 .mapNotNull { (taskViewId, columnOffset) ->
230                     recentsView.getTaskViewFromTaskViewId(taskViewId)?.let { taskView ->
231                         Pair(taskView, columnOffset)
232                     }
233                 }
234         } else {
235             val taskViewList = recentsView.mUtils.taskViews.toList()
236             val draggedTaskViewIndex = taskViewList.indexOf(draggedTaskView)
237 
238             return if (towardsStart) {
239                 taskViewList
240                     .take(draggedTaskViewIndex)
241                     .reversed()
242                     .mapIndexed { index, taskView -> Pair(taskView, index + 1) }
243                     .asSequence()
244             } else {
245                 taskViewList
246                     .takeLast(taskViewList.size - draggedTaskViewIndex - 1)
247                     .mapIndexed { index, taskView -> Pair(taskView, index + 1) }
248                     .asSequence()
249             }
250         }
251     }
252 
253     /** Creates a neighboring task view spring, driven by the spring of its neighbor. */
254     private fun createNeighboringTaskViewSpringAnimation(
255         taskView: TaskView,
256         dampingOffsetRatio: Float,
257         previousNeighborSpringAnimation: SpringAnimation,
258         springingDirectionVertical: Boolean,
259     ): SpringAnimation {
260         val springProperty =
261             if (springingDirectionVertical) taskView.secondaryDismissTranslationProperty
262             else taskView.primaryDismissTranslationProperty
263         val neighboringTaskViewSpringAnimation =
264             SpringAnimation(taskView, FloatPropertyCompat.createFloatPropertyCompat(springProperty))
265                 .setSpring(createExpressiveDismissSpringForce(dampingOffsetRatio))
266         // Update live tile on spring animation.
267         if (taskView.isRunningTask && recentsView.enableDrawingLiveTile) {
268             neighboringTaskViewSpringAnimation.addUpdateListener { _, _, _ ->
269                 recentsView.runActionOnRemoteHandles { remoteTargetHandle ->
270                     val taskTranslation =
271                         if (springingDirectionVertical) {
272                             remoteTargetHandle.taskViewSimulator.taskSecondaryTranslation
273                         } else {
274                             remoteTargetHandle.taskViewSimulator.taskPrimaryTranslation
275                         }
276                     taskTranslation.value = springProperty.get(taskView)
277                 }
278                 recentsView.redrawLiveTile()
279             }
280         }
281         // Drive current neighbor's spring with the previous neighbor's.
282         previousNeighborSpringAnimation.addUpdateListener { _, value, _ ->
283             neighboringTaskViewSpringAnimation.animateToFinalPosition(value)
284         }
285         return neighboringTaskViewSpringAnimation
286     }
287 
288     private fun createExpressiveDismissSpringForce(dampingRatioOffset: Float = 0f): SpringForce {
289         val resourceProvider = DynamicResource.provider(recentsView.mContainer)
290         return SpringForce()
291             .setDampingRatio(
292                 resourceProvider.getFloat(R.dimen.expressive_dismiss_task_trans_y_damping_ratio) +
293                     dampingRatioOffset
294             )
295             .setStiffness(
296                 resourceProvider.getFloat(R.dimen.expressive_dismiss_task_trans_y_stiffness)
297             )
298     }
299 
300     private fun createExpressiveGridReflowSpringForce(
301         finalPosition: Float = Float.MAX_VALUE
302     ): SpringForce {
303         val resourceProvider = DynamicResource.provider(recentsView.mContainer)
304         return SpringForce(finalPosition)
305             .setDampingRatio(
306                 resourceProvider.getFloat(R.dimen.expressive_dismiss_task_trans_x_damping_ratio)
307             )
308             .setStiffness(
309                 resourceProvider.getFloat(R.dimen.expressive_dismiss_task_trans_x_stiffness)
310             )
311     }
312 
313     /**
314      * Plays a haptic as the dragged task view settles back into its rest state.
315      *
316      * <p>Haptic intensity is proportional to velocity.
317      */
318     private fun playDismissSettlingHaptic(velocity: Float) {
319         val maxDismissSettlingVelocity =
320             recentsView.pagedOrientationHandler.getSecondaryDimension(recentsView)
321         MSDLPlayerWrapper.INSTANCE.get(recentsView.context)
322             ?.playToken(
323                 MSDLToken.CANCEL,
324                 InteractionProperties.DynamicVibrationScale(
325                     boundToRange(abs(velocity) / maxDismissSettlingVelocity, 0f, 1f),
326                     VibrationAttributes.Builder()
327                         .setUsage(VibrationAttributes.USAGE_TOUCH)
328                         .setFlags(VibrationAttributes.FLAG_PIPELINED_EFFECT)
329                         .build(),
330                 ),
331             )
332     }
333 
334     /** Animates RecentsView's scale to the provided value, using spring animations. */
335     fun animateRecentsScale(scale: Float): SpringAnimation {
336         val resourceProvider = DynamicResource.provider(recentsView.mContainer)
337         val dampingRatio = resourceProvider.getFloat(R.dimen.swipe_up_rect_scale_damping_ratio)
338         val stiffness = resourceProvider.getFloat(R.dimen.swipe_up_rect_scale_stiffness)
339 
340         // Spring which sets the Recents scale on update. This is needed, as the SpringAnimation
341         // struggles to animate small values like changing recents scale from 0.9 to 1. So
342         // we animate over a larger range (e.g. 900 to 1000) and convert back to the required value.
343         // (This is instead of converting RECENTS_SCALE_PROPERTY to a FloatPropertyCompat and
344         // animating it directly via springs.)
345         val initialRecentsScaleSpringValue =
346             RECENTS_SCALE_SPRING_MULTIPLIER * RECENTS_SCALE_PROPERTY.get(recentsView)
347         return SpringAnimation(FloatValueHolder(initialRecentsScaleSpringValue))
348             .setSpring(
349                 SpringForce(initialRecentsScaleSpringValue)
350                     .setDampingRatio(dampingRatio)
351                     .setStiffness(stiffness)
352             )
353             .addUpdateListener { _, value, _ ->
354                 RECENTS_SCALE_PROPERTY.setValue(
355                     recentsView,
356                     value / RECENTS_SCALE_SPRING_MULTIPLIER,
357                 )
358             }
359             .apply { animateToFinalPosition(RECENTS_SCALE_SPRING_MULTIPLIER * scale) }
360     }
361 
362     /** Animates with springs the TaskViews beyond the dismissed task to fill the gap it left. */
363     private fun runTaskGridReflowSpringAnimation(
364         dismissedTaskView: TaskView,
365         dismissedTaskGap: Float,
366         onEndRunnable: () -> Unit,
367     ) {
368         // Empty spring animation exists for conditional start, and to drive neighboring springs.
369         val springAnimationDriver =
370             SpringAnimation(FloatValueHolder())
371                 .setSpring(createExpressiveGridReflowSpringForce(finalPosition = dismissedTaskGap))
372         val towardsStart = if (recentsView.isRtl) dismissedTaskGap < 0 else dismissedTaskGap > 0
373 
374         var tasksToReflow: List<TaskView>
375         // Build the chains of Spring Animations
376         when {
377             !recentsView.showAsGrid() -> {
378                 tasksToReflow =
379                     getTasksToReflow(
380                         recentsView.mUtils.taskViews.toList(),
381                         dismissedTaskView,
382                         towardsStart,
383                     )
384                 buildDismissReflowSpringAnimationChain(
385                     tasksToReflow,
386                     dismissedTaskGap,
387                     previousSpring = springAnimationDriver,
388                 )
389             }
390             dismissedTaskView.isLargeTile -> {
391                 tasksToReflow =
392                     getTasksToReflow(
393                         recentsView.mUtils.getLargeTaskViews(),
394                         dismissedTaskView,
395                         towardsStart,
396                     )
397                 val lastSpringAnimation =
398                     buildDismissReflowSpringAnimationChain(
399                         tasksToReflow,
400                         dismissedTaskGap,
401                         previousSpring = springAnimationDriver,
402                     )
403                 // Add all top and bottom grid tasks when animating towards the end of the grid.
404                 if (!towardsStart) {
405                     tasksToReflow += recentsView.mUtils.getTopRowTaskViews()
406                     tasksToReflow += recentsView.mUtils.getBottomRowTaskViews()
407                     buildDismissReflowSpringAnimationChain(
408                         recentsView.mUtils.getTopRowTaskViews(),
409                         dismissedTaskGap,
410                         previousSpring = lastSpringAnimation,
411                     )
412                     buildDismissReflowSpringAnimationChain(
413                         recentsView.mUtils.getBottomRowTaskViews(),
414                         dismissedTaskGap,
415                         previousSpring = lastSpringAnimation,
416                     )
417                 }
418             }
419             recentsView.isOnGridBottomRow(dismissedTaskView) -> {
420                 tasksToReflow =
421                     getTasksToReflow(
422                         recentsView.mUtils.getBottomRowTaskViews(),
423                         dismissedTaskView,
424                         towardsStart,
425                     )
426                 buildDismissReflowSpringAnimationChain(
427                     tasksToReflow,
428                     dismissedTaskGap,
429                     previousSpring = springAnimationDriver,
430                 )
431             }
432             else -> {
433                 tasksToReflow =
434                     getTasksToReflow(
435                         recentsView.mUtils.getTopRowTaskViews(),
436                         dismissedTaskView,
437                         towardsStart,
438                     )
439                 buildDismissReflowSpringAnimationChain(
440                     tasksToReflow,
441                     dismissedTaskGap,
442                     previousSpring = springAnimationDriver,
443                 )
444             }
445         }
446 
447         if (tasksToReflow.isNotEmpty()) {
448             addNeighborSettlingSpringAnimations(
449                 dismissedTaskView,
450                 springAnimationDriver,
451                 tasksToExclude = tasksToReflow,
452                 driverProgressThreshold = dismissedTaskGap,
453                 isSpringDirectionVertical = false,
454                 minVelocity = 0f,
455             )
456         } else {
457             springAnimationDriver.addEndListener { _, _, _, _ ->
458                 // Play the same haptic as when neighbors spring into place.
459                 MSDLPlayerWrapper.INSTANCE.get(recentsView.context)?.playToken(MSDLToken.CANCEL)
460             }
461         }
462 
463         // Start animations and remove the dismissed task at the end, dismiss immediately if no
464         // neighboring tasks exist.
465         val runGridEndAnimationAndRelayout = {
466             recentsView.expressiveDismissTaskView(dismissedTaskView, onEndRunnable)
467         }
468         springAnimationDriver?.apply {
469             addEndListener { _, _, _, _ -> runGridEndAnimationAndRelayout() }
470             animateToFinalPosition(dismissedTaskGap)
471         } ?: runGridEndAnimationAndRelayout()
472     }
473 
474     private fun getDismissedTaskGapForReflow(dismissedTaskView: TaskView): Float {
475         val screenStart = recentsView.pagedOrientationHandler.getPrimaryScroll(recentsView)
476         val screenEnd =
477             screenStart + recentsView.pagedOrientationHandler.getMeasuredSize(recentsView)
478         val taskStart =
479             recentsView.pagedOrientationHandler.getChildStart(dismissedTaskView) +
480                 dismissedTaskView.getOffsetAdjustment(recentsView.showAsGrid())
481         val taskSize =
482             recentsView.pagedOrientationHandler.getMeasuredSize(dismissedTaskView) *
483                 dismissedTaskView.getSizeAdjustment(recentsView.showAsFullscreen())
484         val taskEnd = taskStart + taskSize
485 
486         val isDismissedTaskBeyondEndOfScreen =
487             if (recentsView.isRtl) taskEnd > screenEnd else taskStart < screenStart
488         if (
489             dismissedTaskView.isLargeTile &&
490                 isDismissedTaskBeyondEndOfScreen &&
491                 recentsView.mUtils.getLargeTileCount() == 1
492         ) {
493             return with(recentsView) {
494                     pagedOrientationHandler.getPrimaryScroll(this) -
495                         getScrollForPage(indexOfChild(mUtils.getFirstNonDesktopTaskView()))
496                 }
497                 .toFloat()
498         }
499 
500         // If current page is beyond last TaskView's index, use last TaskView to calculate offset.
501         val lastTaskViewIndex = recentsView.indexOfChild(recentsView.mUtils.getLastTaskView())
502         val currentPage = recentsView.currentPage.coerceAtMost(lastTaskViewIndex)
503         val dismissHorizontalFactor =
504             when {
505                 dismissedTaskView.isGridTask -> 1f
506                 currentPage == lastTaskViewIndex -> -1f
507                 recentsView.indexOfChild(dismissedTaskView) < currentPage -> -1f
508                 else -> 1f
509             } * (if (recentsView.isRtl) 1f else -1f)
510 
511         return (recentsView.pagedOrientationHandler.getPrimarySize(dismissedTaskView) +
512             recentsView.pageSpacing) * dismissHorizontalFactor
513     }
514 
515     private fun getTasksToReflow(
516         taskViews: List<TaskView>,
517         dismissedTaskView: TaskView,
518         towardsStart: Boolean,
519     ): List<TaskView> {
520         val dismissedTaskViewIndex = taskViews.indexOf(dismissedTaskView)
521         if (dismissedTaskViewIndex == -1) {
522             return emptyList()
523         }
524         return if (towardsStart) {
525             taskViews.take(dismissedTaskViewIndex).reversed()
526         } else {
527             taskViews.takeLast(taskViews.size - dismissedTaskViewIndex - 1)
528         }
529     }
530 
531     private fun willTaskBeVisibleAfterDismiss(taskView: TaskView, taskTranslation: Int): Boolean {
532         val screenStart = recentsView.pagedOrientationHandler.getPrimaryScroll(recentsView)
533         val screenEnd =
534             screenStart + recentsView.pagedOrientationHandler.getMeasuredSize(recentsView)
535         return recentsView.isTaskViewWithinBounds(
536             taskView,
537             screenStart,
538             screenEnd,
539             /* taskViewTranslation = */ taskTranslation,
540         )
541     }
542 
543     /** Builds a chain of spring animations for task reflow after dismissal */
544     private fun buildDismissReflowSpringAnimationChain(
545         taskViews: Iterable<TaskView>,
546         dismissedTaskGap: Float,
547         previousSpring: SpringAnimation,
548     ): SpringAnimation {
549         var lastTaskViewSpring = previousSpring
550         taskViews
551             .filter { taskView ->
552                 willTaskBeVisibleAfterDismiss(taskView, dismissedTaskGap.roundToInt())
553             }
554             .forEach { taskView ->
555                 val taskViewSpringAnimation =
556                     SpringAnimation(
557                             taskView,
558                             FloatPropertyCompat.createFloatPropertyCompat(
559                                 taskView.primaryDismissTranslationProperty
560                             ),
561                         )
562                         .setSpring(createExpressiveGridReflowSpringForce(dismissedTaskGap))
563                 // Update live tile on spring animation.
564                 if (taskView.isRunningTask && recentsView.enableDrawingLiveTile) {
565                     taskViewSpringAnimation.addUpdateListener { _, _, _ ->
566                         recentsView.runActionOnRemoteHandles { remoteTargetHandle ->
567                             remoteTargetHandle.taskViewSimulator.taskPrimaryTranslation.value =
568                                 taskView.primaryDismissTranslationProperty.get(taskView)
569                         }
570                         recentsView.redrawLiveTile()
571                     }
572                 }
573                 lastTaskViewSpring.addUpdateListener { _, value, _ ->
574                     taskViewSpringAnimation.animateToFinalPosition(value)
575                 }
576                 lastTaskViewSpring = taskViewSpringAnimation
577             }
578         return lastTaskViewSpring
579     }
580 
581     private companion object {
582         // The additional damping to apply to tasks further from the dismissed task.
583         private const val ADDITIONAL_DISMISS_DAMPING_RATIO = 0.15f
584         private const val RECENTS_SCALE_SPRING_MULTIPLIER = 1000f
585     }
586 }
587