1 /* 2 * Copyright (C) 2020 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.systemui.car.hvac; 18 19 import static com.android.systemui.car.hvac.AnimatedTemperatureView.isHorizontal; 20 import static com.android.systemui.car.hvac.AnimatedTemperatureView.isLeft; 21 import static com.android.systemui.car.hvac.AnimatedTemperatureView.isTop; 22 import static com.android.systemui.car.hvac.AnimatedTemperatureView.isVertical; 23 24 import android.animation.Animator; 25 import android.animation.AnimatorListenerAdapter; 26 import android.animation.AnimatorSet; 27 import android.animation.ObjectAnimator; 28 import android.annotation.IntDef; 29 import android.graphics.Rect; 30 import android.view.View; 31 import android.view.ViewAnimationUtils; 32 import android.view.animation.AnticipateInterpolator; 33 import android.widget.ImageView; 34 35 import java.util.ArrayList; 36 import java.util.List; 37 38 /** 39 * Controls circular reveal animation of temperature background 40 */ 41 class TemperatureBackgroundAnimator { 42 43 private static final AnticipateInterpolator ANTICIPATE_INTERPOLATOR = 44 new AnticipateInterpolator(); 45 private static final float MAX_OPACITY = .6f; 46 47 private final View mAnimatedView; 48 49 private int mPivotX; 50 private int mPivotY; 51 private int mGoneRadius; 52 private int mOvershootRadius; 53 private int mRestingRadius; 54 private int mBumpRadius; 55 56 @CircleState 57 private int mCircleState; 58 59 private Animator mCircularReveal; 60 private boolean mAnimationsReady; 61 62 @IntDef({CircleState.GONE, CircleState.ENTERING, CircleState.OVERSHOT, CircleState.RESTING, 63 CircleState.RESTED, CircleState.BUMPING, CircleState.BUMPED, CircleState.EXITING}) 64 private @interface CircleState { 65 int GONE = 0; 66 int ENTERING = 1; 67 int OVERSHOT = 2; 68 int RESTING = 3; 69 int RESTED = 4; 70 int BUMPING = 5; 71 int BUMPED = 6; 72 int EXITING = 7; 73 } 74 TemperatureBackgroundAnimator( AnimatedTemperatureView parent, ImageView animatedView)75 TemperatureBackgroundAnimator( 76 AnimatedTemperatureView parent, 77 ImageView animatedView) { 78 mAnimatedView = animatedView; 79 mAnimatedView.setAlpha(0); 80 81 parent.addOnLayoutChangeListener( 82 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> 83 setupAnimations(parent.getGravity(), parent.getPivotOffset(), 84 parent.getPaddingRect(), parent.getWidth(), parent.getHeight())); 85 } 86 setupAnimations(int gravity, int pivotOffset, Rect paddingRect, int width, int height)87 private void setupAnimations(int gravity, int pivotOffset, Rect paddingRect, 88 int width, int height) { 89 int padding; 90 if (isHorizontal(gravity)) { 91 mGoneRadius = pivotOffset; 92 if (isLeft(gravity, mAnimatedView.getLayoutDirection())) { 93 mPivotX = -pivotOffset; 94 padding = paddingRect.right; 95 } else { 96 mPivotX = width + pivotOffset; 97 padding = paddingRect.left; 98 } 99 mPivotY = height / 2; 100 mOvershootRadius = pivotOffset + width; 101 } else if (isVertical(gravity)) { 102 mGoneRadius = pivotOffset; 103 if (isTop(gravity)) { 104 mPivotY = -pivotOffset; 105 padding = paddingRect.bottom; 106 } else { 107 mPivotY = height + pivotOffset; 108 padding = paddingRect.top; 109 } 110 mPivotX = width / 2; 111 mOvershootRadius = pivotOffset + height; 112 } else { 113 mPivotX = width / 2; 114 mPivotY = height / 2; 115 mGoneRadius = 0; 116 if (width > height) { 117 mOvershootRadius = height; 118 padding = Math.max(paddingRect.top, paddingRect.bottom); 119 } else { 120 mOvershootRadius = width; 121 padding = Math.max(paddingRect.left, paddingRect.right); 122 } 123 } 124 mRestingRadius = mOvershootRadius - padding; 125 mBumpRadius = mOvershootRadius - padding / 3; 126 mAnimationsReady = true; 127 } 128 isOpen()129 boolean isOpen() { 130 return mCircleState != CircleState.GONE; 131 } 132 animateOpen()133 void animateOpen() { 134 if (!mAnimationsReady 135 || !mAnimatedView.isAttachedToWindow() 136 || mCircleState == CircleState.ENTERING) { 137 return; 138 } 139 140 AnimatorSet set = new AnimatorSet(); 141 List<Animator> animators = new ArrayList<>(); 142 switch (mCircleState) { 143 case CircleState.ENTERING: 144 throw new AssertionError("Should not be able to reach this statement"); 145 case CircleState.GONE: { 146 Animator startCircle = createEnterAnimator(); 147 markState(startCircle, CircleState.ENTERING); 148 animators.add(startCircle); 149 Animator holdOvershoot = ViewAnimationUtils 150 .createCircularReveal(mAnimatedView, mPivotX, mPivotY, mOvershootRadius, 151 mOvershootRadius); 152 holdOvershoot.setDuration(50); 153 markState(holdOvershoot, CircleState.OVERSHOT); 154 animators.add(holdOvershoot); 155 Animator rest = ViewAnimationUtils 156 .createCircularReveal(mAnimatedView, mPivotX, mPivotY, mOvershootRadius, 157 mRestingRadius); 158 markState(rest, CircleState.RESTING); 159 animators.add(rest); 160 Animator holdRest = ViewAnimationUtils 161 .createCircularReveal(mAnimatedView, mPivotX, mPivotY, mRestingRadius, 162 mRestingRadius); 163 markState(holdRest, CircleState.RESTED); 164 holdRest.setDuration(1000); 165 animators.add(holdRest); 166 Animator exit = createExitAnimator(mRestingRadius); 167 markState(exit, CircleState.EXITING); 168 animators.add(exit); 169 } 170 break; 171 case CircleState.RESTED: 172 case CircleState.RESTING: 173 case CircleState.EXITING: 174 case CircleState.OVERSHOT: 175 int startRadius = 176 mCircleState == CircleState.OVERSHOT ? mOvershootRadius : mRestingRadius; 177 Animator bump = ViewAnimationUtils 178 .createCircularReveal(mAnimatedView, mPivotX, mPivotY, startRadius, 179 mBumpRadius); 180 bump.setDuration(50); 181 markState(bump, CircleState.BUMPING); 182 animators.add(bump); 183 // fallthrough intentional 184 case CircleState.BUMPED: 185 case CircleState.BUMPING: 186 Animator holdBump = ViewAnimationUtils 187 .createCircularReveal(mAnimatedView, mPivotX, mPivotY, mBumpRadius, 188 mBumpRadius); 189 holdBump.setDuration(100); 190 markState(holdBump, CircleState.BUMPED); 191 animators.add(holdBump); 192 Animator rest = ViewAnimationUtils 193 .createCircularReveal(mAnimatedView, mPivotX, mPivotY, mBumpRadius, 194 mRestingRadius); 195 markState(rest, CircleState.RESTING); 196 animators.add(rest); 197 Animator holdRest = ViewAnimationUtils 198 .createCircularReveal(mAnimatedView, mPivotX, mPivotY, mRestingRadius, 199 mRestingRadius); 200 holdRest.setDuration(1000); 201 markState(holdRest, CircleState.RESTED); 202 animators.add(holdRest); 203 Animator exit = createExitAnimator(mRestingRadius); 204 markState(exit, CircleState.EXITING); 205 animators.add(exit); 206 break; 207 } 208 set.playSequentially(animators); 209 set.addListener(new AnimatorListenerAdapter() { 210 private boolean mCanceled = false; 211 212 @Override 213 public void onAnimationStart(Animator animation) { 214 if (mCircularReveal != null) { 215 mCircularReveal.cancel(); 216 } 217 mCircularReveal = animation; 218 mAnimatedView.setVisibility(View.VISIBLE); 219 } 220 221 @Override 222 public void onAnimationCancel(Animator animation) { 223 mCanceled = true; 224 } 225 226 @Override 227 public void onAnimationEnd(Animator animation) { 228 if (mCanceled) { 229 return; 230 } 231 mCircularReveal = null; 232 mCircleState = CircleState.GONE; 233 mAnimatedView.setVisibility(View.GONE); 234 } 235 }); 236 237 set.start(); 238 } 239 createEnterAnimator()240 private Animator createEnterAnimator() { 241 AnimatorSet animatorSet = new AnimatorSet(); 242 Animator circularReveal = ViewAnimationUtils 243 .createCircularReveal(mAnimatedView, mPivotX, mPivotY, mGoneRadius, 244 mOvershootRadius); 245 Animator fade = ObjectAnimator.ofFloat(mAnimatedView, View.ALPHA, MAX_OPACITY); 246 animatorSet.playTogether(circularReveal, fade); 247 return animatorSet; 248 } 249 createExitAnimator(int startRadius)250 private Animator createExitAnimator(int startRadius) { 251 AnimatorSet animatorSet = new AnimatorSet(); 252 Animator circularHide = ViewAnimationUtils 253 .createCircularReveal(mAnimatedView, mPivotX, mPivotY, startRadius, 254 (mGoneRadius + startRadius) / 2); 255 circularHide.setInterpolator(ANTICIPATE_INTERPOLATOR); 256 Animator fade = ObjectAnimator.ofFloat(mAnimatedView, View.ALPHA, 0); 257 fade.setStartDelay(50); 258 animatorSet.playTogether(circularHide, fade); 259 return animatorSet; 260 } 261 hideCircle()262 void hideCircle() { 263 if (!mAnimationsReady || mCircleState == CircleState.GONE 264 || mCircleState == CircleState.EXITING) { 265 return; 266 } 267 268 int startRadius; 269 switch (mCircleState) { 270 // Unreachable, but here to exhaust switch cases 271 //noinspection ConstantConditions 272 case CircleState.EXITING: 273 //noinspection ConstantConditions 274 case CircleState.GONE: 275 throw new AssertionError("Should not be able to reach this statement"); 276 case CircleState.BUMPED: 277 case CircleState.BUMPING: 278 startRadius = mBumpRadius; 279 break; 280 case CircleState.OVERSHOT: 281 startRadius = mOvershootRadius; 282 break; 283 case CircleState.ENTERING: 284 case CircleState.RESTED: 285 case CircleState.RESTING: 286 startRadius = mRestingRadius; 287 break; 288 default: 289 throw new IllegalStateException("Unknown CircleState: " + mCircleState); 290 } 291 292 Animator hideAnimation = createExitAnimator(startRadius); 293 if (startRadius == mRestingRadius) { 294 hideAnimation.setInterpolator(ANTICIPATE_INTERPOLATOR); 295 } 296 hideAnimation.addListener(new AnimatorListenerAdapter() { 297 private boolean mCanceled = false; 298 299 @Override 300 public void onAnimationStart(Animator animation) { 301 mCircleState = CircleState.EXITING; 302 if (mCircularReveal != null) { 303 mCircularReveal.cancel(); 304 } 305 mCircularReveal = animation; 306 } 307 308 @Override 309 public void onAnimationCancel(Animator animation) { 310 mCanceled = true; 311 } 312 313 @Override 314 public void onAnimationEnd(Animator animation) { 315 if (mCanceled) { 316 return; 317 } 318 mCircularReveal = null; 319 mCircleState = CircleState.GONE; 320 mAnimatedView.setVisibility(View.GONE); 321 } 322 }); 323 hideAnimation.start(); 324 } 325 stopAnimations()326 void stopAnimations() { 327 if (mCircularReveal != null) { 328 mCircularReveal.end(); 329 } 330 } 331 markState(Animator animator, @CircleState int startState)332 private void markState(Animator animator, @CircleState int startState) { 333 animator.addListener(new AnimatorListenerAdapter() { 334 @Override 335 public void onAnimationStart(Animator animation) { 336 mCircleState = startState; 337 } 338 }); 339 } 340 } 341