1 /*
2  * Copyright (C) 2024 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.launcher3.celllayout
18 
19 import android.animation.ObjectAnimator
20 import android.animation.ValueAnimator
21 import android.animation.ValueAnimator.areAnimatorsEnabled
22 import android.util.ArrayMap
23 import android.view.View
24 import com.android.app.animation.Interpolators.DECELERATE_1_5
25 import com.android.launcher3.CellLayout
26 import com.android.launcher3.CellLayout.REORDER_ANIMATION_DURATION
27 import com.android.launcher3.Reorderable
28 import com.android.launcher3.Workspace
29 import com.android.launcher3.util.MultiTranslateDelegate.INDEX_REORDER_BOUNCE_OFFSET
30 import com.android.launcher3.util.Thunk
31 import kotlin.math.abs
32 import kotlin.math.atan
33 import kotlin.math.cos
34 import kotlin.math.sign
35 import kotlin.math.sin
36 
37 /**
38  * Class which represents the reorder preview animations. These animations show that an item is in a
39  * temporary state, and hint at where the item will return to.
40  */
41 class ReorderPreviewAnimation<T>(
42     val child: T,
43     // If the mode is MODE_HINT it will only move one period and stop, it then will be going
44     // backwards to the initial position, otherwise it will oscillate.
45     val mode: Int,
46     cellX0: Int,
47     cellY0: Int,
48     cellX1: Int,
49     cellY1: Int,
50     spanX: Int,
51     spanY: Int,
52     reorderMagnitude: Float,
53     cellLayout: CellLayout,
54     private val shakeAnimators: ArrayMap<Reorderable, ReorderPreviewAnimation<T>>
55 ) : ValueAnimator.AnimatorUpdateListener where T : View, T : Reorderable {
56 
57     private var finalDeltaX = 0f
58     private var finalDeltaY = 0f
59     private var initDeltaX =
60         child.getTranslateDelegate().getTranslationX(INDEX_REORDER_BOUNCE_OFFSET).value
61     private var initDeltaY =
62         child.getTranslateDelegate().getTranslationY(INDEX_REORDER_BOUNCE_OFFSET).value
63     private var initScale = child.getReorderBounceScale()
64     private val finalScale = CellLayout.DEFAULT_SCALE - CHILD_DIVIDEND / child.width * initScale
65 
66     private val dir = if (mode == MODE_HINT) -1 else 1
67     var animator: ValueAnimator =
<lambda>null68         ObjectAnimator.ofFloat(0f, 1f).also {
69             it.addUpdateListener(this)
70             it.setDuration((if (mode == MODE_HINT) HINT_DURATION else PREVIEW_DURATION).toLong())
71             it.startDelay = (Math.random() * 60).toLong()
72             // Animations are disabled in power save mode, causing the repeated animation to jump
73             // spastically between beginning and end states. Since this looks bad, we don't repeat
74             // the animation in power save mode.
75             if (areAnimatorsEnabled() && mode == MODE_PREVIEW) {
76                 it.repeatCount = ValueAnimator.INFINITE
77                 it.repeatMode = ValueAnimator.REVERSE
78             }
79         }
80 
81     init {
82         val tmpRes = intArrayOf(0, 0)
83         cellLayout.regionToCenterPoint(cellX0, cellY0, spanX, spanY, tmpRes)
84         val (x0, y0) = tmpRes
85         cellLayout.regionToCenterPoint(cellX1, cellY1, spanX, spanY, tmpRes)
86         val (x1, y1) = tmpRes
87         val dX = x1 - x0
88         val dY = y1 - y0
89 
90         if (dX != 0 || dY != 0) {
91             if (dY == 0) {
92                 finalDeltaX = -dir * sign(dX.toFloat()) * reorderMagnitude
93             } else if (dX == 0) {
94                 finalDeltaY = -dir * sign(dY.toFloat()) * reorderMagnitude
95             } else {
96                 val angle = atan((dY.toFloat() / dX))
97                 finalDeltaX = (-dir * sign(dX.toFloat()) * abs(cos(angle) * reorderMagnitude))
98                 finalDeltaY = (-dir * sign(dY.toFloat()) * abs(sin(angle) * reorderMagnitude))
99             }
100         }
101     }
102 
setInitialAnimationValuesToBaselinenull103     private fun setInitialAnimationValuesToBaseline() {
104         initScale = CellLayout.DEFAULT_SCALE
105         initDeltaX = 0f
106         initDeltaY = 0f
107     }
108 
animatenull109     fun animate() {
110         val noMovement = finalDeltaX == 0f && finalDeltaY == 0f
111         if (shakeAnimators.containsKey(child)) {
112             val oldAnimation: ReorderPreviewAnimation<T>? = shakeAnimators.remove(child)
113             if (noMovement) {
114                 // A previous animation for this item exists, and no new animation will exist.
115                 // Finish the old animation smoothly.
116                 oldAnimation!!.finishAnimation()
117                 return
118             } else {
119                 // A previous animation for this item exists, and a new one will exist. Stop
120                 // the old animation in its tracks, and proceed with the new one.
121                 oldAnimation!!.cancel()
122             }
123         }
124         if (noMovement) {
125             return
126         }
127         shakeAnimators[child] = this
128         animator.start()
129     }
130 
onAnimationUpdatenull131     override fun onAnimationUpdate(updatedAnimation: ValueAnimator) {
132         val progress = updatedAnimation.animatedValue as Float
133         child
134             .getTranslateDelegate()
135             .setTranslation(
136                 INDEX_REORDER_BOUNCE_OFFSET,
137                 /* x = */ progress * finalDeltaX + (1 - progress) * initDeltaX,
138                 /* y = */ progress * finalDeltaY + (1 - progress) * initDeltaY
139             )
140         child.setReorderBounceScale(progress * finalScale + (1 - progress) * initScale)
141     }
142 
cancelnull143     private fun cancel() {
144         animator.cancel()
145     }
146 
147     /** Smoothly returns the item to its baseline position / scale */
148     @Thunk
finishAnimationnull149     fun finishAnimation() {
150         animator.cancel()
151         setInitialAnimationValuesToBaseline()
152         animator = ObjectAnimator.ofFloat((animator.animatedValue as Float), 0f)
153         animator.addUpdateListener(this)
154         animator.interpolator = DECELERATE_1_5
155         animator.setDuration(REORDER_ANIMATION_DURATION.toLong())
156         animator.start()
157     }
158 
159     companion object {
160         const val PREVIEW_DURATION = 300
161         const val HINT_DURATION = Workspace.REORDER_TIMEOUT
162         private const val CHILD_DIVIDEND = 4.0f
163         const val MODE_HINT = 0
164         const val MODE_PREVIEW = 1
165     }
166 }
167