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