• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 package com.android.quickstep.util;
17 
18 import android.animation.Animator;
19 import android.animation.AnimatorListenerAdapter;
20 import android.annotation.ColorInt;
21 import android.annotation.Nullable;
22 import android.graphics.Canvas;
23 import android.graphics.Color;
24 import android.graphics.Paint;
25 import android.graphics.Rect;
26 import android.view.View;
27 import android.view.animation.Interpolator;
28 
29 import androidx.annotation.NonNull;
30 import androidx.annotation.Px;
31 
32 import com.android.app.animation.Interpolators;
33 import com.android.launcher3.anim.AnimatedFloat;
34 import com.android.launcher3.anim.AnimatorListeners;
35 
36 /**
37  * Utility class for drawing a rounded-rect border around a view.
38  * <p>
39  * To use this class:
40  * 1. Create an instance in the target view. NOTE: The border will animate outwards from the
41  *      provided border bounds. See {@link SimpleParams} and {@link ScalingParams} to determine
42  *      which would be best for your target view.
43  * 2. Override the target view's {@link android.view.View#draw(Canvas)} method and call
44  *      {@link BorderAnimator#drawBorder(Canvas)} after {@code super.draw(canvas)}.
45  * 3. Call {@link BorderAnimator#buildAnimator(boolean)} and start the animation or call
46  *      {@link BorderAnimator#setBorderVisible(boolean)} where appropriate.
47  */
48 public final class BorderAnimator {
49 
50     public static final int DEFAULT_BORDER_COLOR = Color.WHITE;
51 
52     private static final long DEFAULT_APPEARANCE_ANIMATION_DURATION_MS = 300;
53     private static final long DEFAULT_DISAPPEARANCE_ANIMATION_DURATION_MS = 133;
54     private static final Interpolator DEFAULT_INTERPOLATOR = Interpolators.EMPHASIZED_DECELERATE;
55 
56     @NonNull private final AnimatedFloat mBorderAnimationProgress = new AnimatedFloat(
57             this::updateOutline);
58     @Px private final int mBorderRadiusPx;
59     @NonNull private final BorderAnimationParams mBorderAnimationParams;
60     private final long mAppearanceDurationMs;
61     private final long mDisappearanceDurationMs;
62     @NonNull private final Interpolator mInterpolator;
63     @NonNull private final Paint mBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
64 
65     @Nullable private Animator mRunningBorderAnimation;
66 
BorderAnimator( @x int borderRadiusPx, @ColorInt int borderColor, @NonNull BorderAnimationParams borderAnimationParams)67     public BorderAnimator(
68             @Px int borderRadiusPx,
69             @ColorInt int borderColor,
70             @NonNull BorderAnimationParams borderAnimationParams) {
71         this(borderRadiusPx,
72                 borderColor,
73                 borderAnimationParams,
74                 DEFAULT_APPEARANCE_ANIMATION_DURATION_MS,
75                 DEFAULT_DISAPPEARANCE_ANIMATION_DURATION_MS,
76                 DEFAULT_INTERPOLATOR);
77     }
78 
79     /**
80      * @param borderRadiusPx the radius of the border's corners, in pixels
81      * @param borderColor the border's color
82      * @param borderAnimationParams params for handling different target view layout situation.
83      * @param appearanceDurationMs appearance animation duration, in milliseconds
84      * @param disappearanceDurationMs disappearance animation duration, in milliseconds
85      * @param interpolator animation interpolator
86      */
BorderAnimator( @x int borderRadiusPx, @ColorInt int borderColor, @NonNull BorderAnimationParams borderAnimationParams, long appearanceDurationMs, long disappearanceDurationMs, @NonNull Interpolator interpolator)87     public BorderAnimator(
88             @Px int borderRadiusPx,
89             @ColorInt int borderColor,
90             @NonNull BorderAnimationParams borderAnimationParams,
91             long appearanceDurationMs,
92             long disappearanceDurationMs,
93             @NonNull Interpolator interpolator) {
94         mBorderRadiusPx = borderRadiusPx;
95         mBorderAnimationParams = borderAnimationParams;
96         mAppearanceDurationMs = appearanceDurationMs;
97         mDisappearanceDurationMs = disappearanceDurationMs;
98         mInterpolator = interpolator;
99 
100         mBorderPaint.setColor(borderColor);
101         mBorderPaint.setStyle(Paint.Style.STROKE);
102         mBorderPaint.setAlpha(0);
103     }
104 
updateOutline()105     private void updateOutline() {
106         float interpolatedProgress = mInterpolator.getInterpolation(
107                 mBorderAnimationProgress.value);
108 
109         mBorderAnimationParams.setProgress(interpolatedProgress);
110         mBorderPaint.setAlpha(Math.round(255 * interpolatedProgress));
111         mBorderPaint.setStrokeWidth(mBorderAnimationParams.getBorderWidth());
112         mBorderAnimationParams.mTargetView.invalidate();
113     }
114 
115     /**
116      * Draws the border on the given canvas.
117      * <p>
118      * Call this method in the target view's {@link android.view.View#draw(Canvas)} method after
119      * calling super.
120      */
drawBorder(Canvas canvas)121     public void drawBorder(Canvas canvas) {
122         float alignmentAdjustment = mBorderAnimationParams.getAlignmentAdjustment();
123         canvas.drawRoundRect(
124                 /* left= */ mBorderAnimationParams.mBorderBounds.left + alignmentAdjustment,
125                 /* top= */ mBorderAnimationParams.mBorderBounds.top + alignmentAdjustment,
126                 /* right= */ mBorderAnimationParams.mBorderBounds.right - alignmentAdjustment,
127                 /* bottom= */ mBorderAnimationParams.mBorderBounds.bottom - alignmentAdjustment,
128                 /* rx= */ mBorderRadiusPx + mBorderAnimationParams.getRadiusAdjustment(),
129                 /* ry= */ mBorderRadiusPx + mBorderAnimationParams.getRadiusAdjustment(),
130                 /* paint= */ mBorderPaint);
131     }
132 
133     /**
134      * Builds the border appearance/disappearance animation.
135      */
136     @NonNull
buildAnimator(boolean isAppearing)137     public Animator buildAnimator(boolean isAppearing) {
138         mRunningBorderAnimation = mBorderAnimationProgress.animateToValue(isAppearing ? 1f : 0f);
139         mRunningBorderAnimation.setDuration(
140                 isAppearing ? mAppearanceDurationMs : mDisappearanceDurationMs);
141 
142         mRunningBorderAnimation.addListener(new AnimatorListenerAdapter() {
143             @Override
144             public void onAnimationStart(Animator animation) {
145                 mBorderAnimationParams.onShowBorder();
146             }
147         });
148         mRunningBorderAnimation.addListener(
149                 AnimatorListeners.forEndCallback(() -> {
150                     mRunningBorderAnimation = null;
151                     if (isAppearing) {
152                         return;
153                     }
154                     mBorderAnimationParams.onHideBorder();
155                 }));
156 
157         return mRunningBorderAnimation;
158     }
159 
160     /**
161      * Immediately shows/hides the border without an animation.
162      * <p>
163      * To animate the appearance/disappearance, see {@link BorderAnimator#buildAnimator(boolean)}
164      */
setBorderVisible(boolean visible)165     public void setBorderVisible(boolean visible) {
166         if (mRunningBorderAnimation != null) {
167             mRunningBorderAnimation.end();
168         }
169         if (visible) {
170             mBorderAnimationParams.onShowBorder();
171         }
172         mBorderAnimationProgress.updateValue(visible ? 1f : 0f);
173         if (!visible) {
174             mBorderAnimationParams.onHideBorder();
175         }
176     }
177 
178     /**
179      * Callback to update the border bounds when building this animation.
180      */
181     public interface BorderBoundsBuilder {
182 
183         /**
184          * Sets the given rect to the most up-to-date bounds.
185          */
updateBorderBounds(Rect rect)186         void updateBorderBounds(Rect rect);
187     }
188 
189     /**
190      * Params for handling different target view layout situation.
191      */
192     private abstract static class BorderAnimationParams {
193 
194         @NonNull private final Rect mBorderBounds = new Rect();
195         @NonNull private final BorderBoundsBuilder mBoundsBuilder;
196 
197         @NonNull final View mTargetView;
198         @Px final int mBorderWidthPx;
199 
200         private float mAnimationProgress = 0f;
201         @Nullable private View.OnLayoutChangeListener mLayoutChangeListener;
202 
203         /**
204          * @param borderWidthPx the width of the border, in pixels
205          * @param boundsBuilder callback to update the border bounds
206          * @param targetView the view that will be drawing the border
207          */
BorderAnimationParams( @x int borderWidthPx, @NonNull BorderBoundsBuilder boundsBuilder, @NonNull View targetView)208         private BorderAnimationParams(
209                 @Px int borderWidthPx,
210                 @NonNull BorderBoundsBuilder boundsBuilder,
211                 @NonNull View targetView) {
212             mBorderWidthPx = borderWidthPx;
213             mBoundsBuilder = boundsBuilder;
214             mTargetView = targetView;
215         }
216 
setProgress(float progress)217         private void setProgress(float progress) {
218             mAnimationProgress = progress;
219         }
220 
getBorderWidth()221         private float getBorderWidth() {
222             return mBorderWidthPx * mAnimationProgress;
223         }
224 
getAlignmentAdjustment()225         float getAlignmentAdjustment() {
226             // Outset the border by half the width to create an outwards-growth animation
227             return (-getBorderWidth() / 2f) + getAlignmentAdjustmentInset();
228         }
229 
230 
onShowBorder()231         void onShowBorder() {
232             if (mLayoutChangeListener == null) {
233                 mLayoutChangeListener =
234                         (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
235                             onShowBorder();
236                             mTargetView.invalidate();
237                         };
238                 mTargetView.addOnLayoutChangeListener(mLayoutChangeListener);
239             }
240             mBoundsBuilder.updateBorderBounds(mBorderBounds);
241         }
242 
onHideBorder()243         void onHideBorder() {
244             if (mLayoutChangeListener != null) {
245                 mTargetView.removeOnLayoutChangeListener(mLayoutChangeListener);
246                 mLayoutChangeListener = null;
247             }
248         }
249 
getAlignmentAdjustmentInset()250         abstract int getAlignmentAdjustmentInset();
251 
getRadiusAdjustment()252         abstract float getRadiusAdjustment();
253     }
254 
255     /**
256      * Use an instance of this {@link BorderAnimationParams} if the border can be drawn outside the
257      * target view's bounds without any additional logic.
258      */
259     public static final class SimpleParams extends BorderAnimationParams {
260 
SimpleParams( @x int borderWidthPx, @NonNull BorderBoundsBuilder boundsBuilder, @NonNull View targetView)261         public SimpleParams(
262                 @Px int borderWidthPx,
263                 @NonNull BorderBoundsBuilder boundsBuilder,
264                 @NonNull View targetView) {
265             super(borderWidthPx, boundsBuilder, targetView);
266         }
267 
268         @Override
getAlignmentAdjustmentInset()269         int getAlignmentAdjustmentInset() {
270             return 0;
271         }
272 
273         @Override
getRadiusAdjustment()274         float getRadiusAdjustment() {
275             return -getAlignmentAdjustment();
276         }
277     }
278 
279     /**
280      * Use an instance of this {@link BorderAnimationParams} if the border would other be clipped by
281      * the target view's bound.
282      * <p>
283      * Note: using these params will set the scales and pivots of the
284      * container and content views, however will only reset the scales back to 1.
285      */
286     public static final class ScalingParams extends BorderAnimationParams {
287 
288         @NonNull private final View mContentView;
289 
290         /**
291          * @param targetView the view that will be drawing the border. this view will be scaled up
292          *                   to make room for the border
293          * @param contentView the view around which the border will be drawn. this view will be
294          *                    scaled down reciprocally to keep its original size and location.
295          */
ScalingParams( @x int borderWidthPx, @NonNull BorderBoundsBuilder boundsBuilder, @NonNull View targetView, @NonNull View contentView)296         public ScalingParams(
297                 @Px int borderWidthPx,
298                 @NonNull BorderBoundsBuilder boundsBuilder,
299                 @NonNull View targetView,
300                 @NonNull View contentView) {
301             super(borderWidthPx, boundsBuilder, targetView);
302             mContentView = contentView;
303         }
304 
305         @Override
onShowBorder()306         void onShowBorder() {
307             super.onShowBorder();
308             float width = mTargetView.getWidth();
309             float height = mTargetView.getHeight();
310             // Scale up just enough to make room for the border. Fail fast and fix the scaling
311             // onLayout.
312             float scaleX = width == 0 ? 1f : 1f + ((2 * mBorderWidthPx) / width);
313             float scaleY = height == 0 ? 1f : 1f + ((2 * mBorderWidthPx) / height);
314 
315             mTargetView.setPivotX(width / 2);
316             mTargetView.setPivotY(height / 2);
317             mTargetView.setScaleX(scaleX);
318             mTargetView.setScaleY(scaleY);
319 
320             mContentView.setPivotX(mContentView.getWidth() / 2f);
321             mContentView.setPivotY(mContentView.getHeight() / 2f);
322             mContentView.setScaleX(1f / scaleX);
323             mContentView.setScaleY(1f / scaleY);
324         }
325 
326         @Override
onHideBorder()327         void onHideBorder() {
328             super.onHideBorder();
329             mTargetView.setPivotX(mTargetView.getWidth());
330             mTargetView.setPivotY(mTargetView.getHeight());
331             mTargetView.setScaleX(1f);
332             mTargetView.setScaleY(1f);
333 
334             mContentView.setPivotX(mContentView.getWidth() / 2f);
335             mContentView.setPivotY(mContentView.getHeight() / 2f);
336             mContentView.setScaleX(1f);
337             mContentView.setScaleY(1f);
338         }
339 
340         @Override
getAlignmentAdjustmentInset()341         int getAlignmentAdjustmentInset() {
342             // Inset the border since we are scaling the container up
343             return mBorderWidthPx;
344         }
345 
346         @Override
getRadiusAdjustment()347         float getRadiusAdjustment() {
348             // Increase the radius since we are scaling the container up
349             return getAlignmentAdjustment();
350         }
351     }
352 }
353