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