1 /* 2 * Copyright (C) 2023 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.taskbar.bubbles; 18 19 import static androidx.dynamicanimation.animation.SpringForce.DAMPING_RATIO_LOW_BOUNCY; 20 import static androidx.dynamicanimation.animation.SpringForce.STIFFNESS_LOW; 21 22 import android.content.res.Resources; 23 import android.graphics.PointF; 24 import android.view.View; 25 26 import androidx.annotation.NonNull; 27 import androidx.annotation.Nullable; 28 import androidx.dynamicanimation.animation.DynamicAnimation; 29 import androidx.dynamicanimation.animation.FloatPropertyCompat; 30 31 import com.android.launcher3.R; 32 import com.android.wm.shell.common.bubbles.DismissCircleView; 33 import com.android.wm.shell.common.bubbles.DismissView; 34 import com.android.wm.shell.shared.animation.PhysicsAnimator; 35 36 /** 37 * The animator performs the bubble animations while dragging and coordinates bubble and dismiss 38 * view animations when it gets magnetized, released or dismissed. 39 */ 40 public class BubbleDragAnimator { 41 private static final float SCALE_BUBBLE_FOCUSED = 1.2f; 42 private static final float SCALE_BUBBLE_CAPTURED = 0.9f; 43 private static final float SCALE_BUBBLE_BAR_FOCUSED = 1.1f; 44 // 400f matches to MEDIUM_LOW spring stiffness 45 private static final float TRANSLATION_SPRING_STIFFNESS = 400f; 46 47 private final PhysicsAnimator.SpringConfig mDefaultConfig = 48 new PhysicsAnimator.SpringConfig(STIFFNESS_LOW, DAMPING_RATIO_LOW_BOUNCY); 49 private final PhysicsAnimator.SpringConfig mTranslationConfig = 50 new PhysicsAnimator.SpringConfig(TRANSLATION_SPRING_STIFFNESS, 51 DAMPING_RATIO_LOW_BOUNCY); 52 @NonNull 53 private final View mView; 54 @NonNull 55 private final PhysicsAnimator<View> mBubbleAnimator; 56 @Nullable 57 private DismissView mDismissView; 58 @Nullable 59 private PhysicsAnimator<DismissCircleView> mDismissAnimator; 60 private final float mBubbleFocusedScale; 61 private final float mBubbleCapturedScale; 62 private final float mDismissCapturedScale; 63 64 /** 65 * Should be initialised for each dragged view 66 * 67 * @param view the dragged view to animate 68 */ BubbleDragAnimator(@onNull View view)69 public BubbleDragAnimator(@NonNull View view) { 70 mView = view; 71 mBubbleAnimator = PhysicsAnimator.getInstance(view); 72 mBubbleAnimator.setDefaultSpringConfig(mDefaultConfig); 73 74 Resources resources = view.getResources(); 75 final int collapsedSize = resources.getDimensionPixelSize( 76 R.dimen.bubblebar_dismiss_target_small_size); 77 final int expandedSize = resources.getDimensionPixelSize( 78 R.dimen.bubblebar_dismiss_target_size); 79 mDismissCapturedScale = (float) collapsedSize / expandedSize; 80 81 if (view instanceof BubbleBarView) { 82 mBubbleFocusedScale = SCALE_BUBBLE_BAR_FOCUSED; 83 mBubbleCapturedScale = mDismissCapturedScale; 84 } else { 85 mBubbleFocusedScale = SCALE_BUBBLE_FOCUSED; 86 mBubbleCapturedScale = SCALE_BUBBLE_CAPTURED; 87 } 88 } 89 90 /** 91 * Sets dismiss view to be animated alongside the dragged bubble 92 */ setDismissView(@onNull DismissView dismissView)93 public void setDismissView(@NonNull DismissView dismissView) { 94 mDismissView = dismissView; 95 mDismissAnimator = PhysicsAnimator.getInstance(dismissView.getCircle()); 96 mDismissAnimator.setDefaultSpringConfig(mDefaultConfig); 97 } 98 99 /** 100 * Animates the focused state of the bubble when the dragging starts 101 */ animateFocused()102 public void animateFocused() { 103 mBubbleAnimator.cancel(); 104 mBubbleAnimator 105 .spring(DynamicAnimation.SCALE_X, mBubbleFocusedScale) 106 .spring(DynamicAnimation.SCALE_Y, mBubbleFocusedScale) 107 .start(); 108 } 109 110 /** 111 * Animates the dragged bubble movement back to the initial position. 112 * 113 * @param restingPosition the position to animate to 114 * @param velocity the initial velocity to use for the spring animation 115 * @param endActions gets called when the animation completes or gets cancelled 116 */ animateToRestingState(@onNull PointF restingPosition, @NonNull PointF velocity, @Nullable Runnable endActions)117 public void animateToRestingState(@NonNull PointF restingPosition, @NonNull PointF velocity, 118 @Nullable Runnable endActions) { 119 mBubbleAnimator.cancel(); 120 mBubbleAnimator 121 .spring(DynamicAnimation.SCALE_X, 1f) 122 .spring(DynamicAnimation.SCALE_Y, 1f) 123 .spring(BubbleDragController.DRAG_TRANSLATION_X, restingPosition.x, velocity.x, 124 mTranslationConfig) 125 .spring(DynamicAnimation.TRANSLATION_Y, restingPosition.y, velocity.y, 126 mTranslationConfig) 127 .addEndListener((View target, @NonNull FloatPropertyCompat<? super View> property, 128 boolean wasFling, boolean canceled, float finalValue, float finalVelocity, 129 boolean allRelevantPropertyAnimationsEnded) -> { 130 if (canceled || allRelevantPropertyAnimationsEnded) { 131 resetAnimatedViews(restingPosition); 132 if (endActions != null) { 133 endActions.run(); 134 } 135 } 136 }) 137 .start(); 138 } 139 140 /** 141 * Animates the dragged view alongside the dismiss view when it gets captured in the dismiss 142 * target area. 143 */ animateDismissCaptured()144 public void animateDismissCaptured() { 145 mBubbleAnimator.cancel(); 146 mBubbleAnimator 147 .spring(DynamicAnimation.SCALE_X, mBubbleCapturedScale) 148 .spring(DynamicAnimation.SCALE_Y, mBubbleCapturedScale) 149 .spring(DynamicAnimation.ALPHA, mDismissCapturedScale) 150 .start(); 151 152 if (mDismissAnimator != null) { 153 mDismissAnimator.cancel(); 154 mDismissAnimator 155 .spring(DynamicAnimation.SCALE_X, mDismissCapturedScale) 156 .spring(DynamicAnimation.SCALE_Y, mDismissCapturedScale) 157 .start(); 158 } 159 } 160 161 /** 162 * Animates the dragged view alongside the dismiss view when it gets released from the dismiss 163 * target area. 164 */ animateDismissReleased()165 public void animateDismissReleased() { 166 mBubbleAnimator.cancel(); 167 mBubbleAnimator 168 .spring(DynamicAnimation.SCALE_X, mBubbleFocusedScale) 169 .spring(DynamicAnimation.SCALE_Y, mBubbleFocusedScale) 170 .spring(DynamicAnimation.ALPHA, 1f) 171 .start(); 172 173 if (mDismissAnimator != null) { 174 mDismissAnimator.cancel(); 175 mDismissAnimator 176 .spring(DynamicAnimation.SCALE_X, 1f) 177 .spring(DynamicAnimation.SCALE_Y, 1f) 178 .start(); 179 } 180 } 181 182 /** 183 * Animates the dragged bubble dismiss when it's released in the dismiss target area. 184 * 185 * @param initialPosition the initial position to move the bubble too after animation finishes 186 * @param endActions gets called when the animation completes or gets cancelled 187 */ animateDismiss(@onNull PointF initialPosition, @Nullable Runnable endActions)188 public void animateDismiss(@NonNull PointF initialPosition, @Nullable Runnable endActions) { 189 float dismissHeight = mDismissView != null ? mDismissView.getHeight() : 0f; 190 float translationY = mView.getTranslationY() + dismissHeight; 191 mBubbleAnimator 192 .spring(DynamicAnimation.TRANSLATION_Y, translationY) 193 .spring(DynamicAnimation.SCALE_X, 0f) 194 .spring(DynamicAnimation.SCALE_Y, 0f) 195 .spring(DynamicAnimation.ALPHA, 0f) 196 .addEndListener((View target, @NonNull FloatPropertyCompat<? super View> property, 197 boolean wasFling, boolean canceled, float finalValue, float finalVelocity, 198 boolean allRelevantPropertyAnimationsEnded) -> { 199 if (canceled || allRelevantPropertyAnimationsEnded) { 200 resetAnimatedViews(initialPosition); 201 if (endActions != null) endActions.run(); 202 } 203 }) 204 .start(); 205 } 206 207 /** 208 * Reset the animated views to the initial state 209 * 210 * @param initialPosition position of the bubble 211 */ resetAnimatedViews(@onNull PointF initialPosition)212 private void resetAnimatedViews(@NonNull PointF initialPosition) { 213 mView.setScaleX(1f); 214 mView.setScaleY(1f); 215 mView.setAlpha(1f); 216 mView.setTranslationX(initialPosition.x); 217 mView.setTranslationY(initialPosition.y); 218 219 if (mDismissView != null) { 220 mDismissView.getCircle().setScaleX(1f); 221 mDismissView.getCircle().setScaleY(1f); 222 } 223 } 224 } 225