1 /*
2  * Copyright (c) 2018 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.statusbar.hvac;
18 
19 import static com.android.systemui.statusbar.hvac.AnimatedTemperatureView.isHorizontal;
20 import static com.android.systemui.statusbar.hvac.AnimatedTemperatureView.isLeft;
21 import static com.android.systemui.statusbar.hvac.AnimatedTemperatureView.isTop;
22 import static com.android.systemui.statusbar.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