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.annotation.ColorInt; 20 import android.annotation.Nullable; 21 import android.graphics.Canvas; 22 import android.graphics.Paint; 23 import android.graphics.Rect; 24 import android.view.animation.Interpolator; 25 26 import androidx.annotation.NonNull; 27 import androidx.annotation.Px; 28 29 import com.android.launcher3.Utilities; 30 import com.android.launcher3.anim.AnimatedFloat; 31 import com.android.launcher3.anim.AnimatorListeners; 32 import com.android.launcher3.anim.Interpolators; 33 34 /** 35 * Utility class for drawing a rounded-rect border around a view. 36 * <p> 37 * To use this class: 38 * 1. Create an instance in the target view. 39 * 2. Override the target view's {@link android.view.View#draw(Canvas)} method and call 40 * {@link BorderAnimator#drawBorder(Canvas)} after {@code super.draw(canvas)}. 41 * 3. Call {@link BorderAnimator#buildAnimator(boolean)} and start the animation or call 42 * {@link BorderAnimator#setBorderVisible(boolean)} where appropriate. 43 */ 44 public final class BorderAnimator { 45 46 public static final int DEFAULT_BORDER_COLOR = 0xffffffff; 47 48 private static final long DEFAULT_APPEARANCE_ANIMATION_DURATION_MS = 300; 49 private static final long DEFAULT_DISAPPEARANCE_ANIMATION_DURATION_MS = 133; 50 private static final Interpolator DEFAULT_INTERPOLATOR = Interpolators.EMPHASIZED_DECELERATE; 51 52 @NonNull private final AnimatedFloat mBorderAnimationProgress = new AnimatedFloat( 53 this::updateOutline); 54 @NonNull private final Rect mBorderBounds = new Rect(); 55 @NonNull private final BorderBoundsBuilder mBorderBoundsBuilder; 56 @Px private final int mBorderWidthPx; 57 @Px private final int mBorderRadiusPx; 58 @NonNull private final Runnable mInvalidateViewCallback; 59 private final long mAppearanceDurationMs; 60 private final long mDisappearanceDurationMs; 61 @NonNull private final Interpolator mInterpolator; 62 @NonNull private final Paint mBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 63 64 private int mAlignmentAdjustment; 65 66 @Nullable private Animator mRunningBorderAnimation; 67 BorderAnimator( @onNull BorderBoundsBuilder borderBoundsBuilder, int borderWidthPx, int borderRadiusPx, @ColorInt int borderColor, @NonNull Runnable invalidateViewCallback)68 public BorderAnimator( 69 @NonNull BorderBoundsBuilder borderBoundsBuilder, 70 int borderWidthPx, 71 int borderRadiusPx, 72 @ColorInt int borderColor, 73 @NonNull Runnable invalidateViewCallback) { 74 this(borderBoundsBuilder, 75 borderWidthPx, 76 borderRadiusPx, 77 borderColor, 78 invalidateViewCallback, 79 DEFAULT_APPEARANCE_ANIMATION_DURATION_MS, 80 DEFAULT_DISAPPEARANCE_ANIMATION_DURATION_MS, 81 DEFAULT_INTERPOLATOR); 82 } 83 BorderAnimator( @onNull BorderBoundsBuilder borderBoundsBuilder, int borderWidthPx, int borderRadiusPx, @ColorInt int borderColor, @NonNull Runnable invalidateViewCallback, long appearanceDurationMs, long disappearanceDurationMs, @NonNull Interpolator interpolator)84 public BorderAnimator( 85 @NonNull BorderBoundsBuilder borderBoundsBuilder, 86 int borderWidthPx, 87 int borderRadiusPx, 88 @ColorInt int borderColor, 89 @NonNull Runnable invalidateViewCallback, 90 long appearanceDurationMs, 91 long disappearanceDurationMs, 92 @NonNull Interpolator interpolator) { 93 mBorderBoundsBuilder = borderBoundsBuilder; 94 mBorderWidthPx = borderWidthPx; 95 mBorderRadiusPx = borderRadiusPx; 96 mInvalidateViewCallback = invalidateViewCallback; 97 mAppearanceDurationMs = appearanceDurationMs; 98 mDisappearanceDurationMs = disappearanceDurationMs; 99 mInterpolator = interpolator; 100 101 mBorderPaint.setColor(borderColor); 102 mBorderPaint.setStyle(Paint.Style.STROKE); 103 mBorderPaint.setAlpha(0); 104 } 105 updateOutline()106 private void updateOutline() { 107 float interpolatedProgress = mInterpolator.getInterpolation( 108 mBorderAnimationProgress.value); 109 mAlignmentAdjustment = (int) Utilities.mapBoundToRange( 110 mBorderAnimationProgress.value, 111 /* lowerBound= */ 0f, 112 /* upperBound= */ 1f, 113 /* toMin= */ 0f, 114 /* toMax= */ (float) (mBorderWidthPx / 2f), 115 mInterpolator); 116 117 mBorderPaint.setAlpha(Math.round(255 * interpolatedProgress)); 118 mBorderPaint.setStrokeWidth(Math.round(mBorderWidthPx * interpolatedProgress)); 119 mInvalidateViewCallback.run(); 120 } 121 122 /** 123 * Draws the border on the given canvas. 124 * <p> 125 * Call this method in the target view's {@link android.view.View#draw(Canvas)} method after 126 * calling super. 127 */ drawBorder(Canvas canvas)128 public void drawBorder(Canvas canvas) { 129 canvas.drawRoundRect( 130 /* left= */ mBorderBounds.left + mAlignmentAdjustment, 131 /* top= */ mBorderBounds.top + mAlignmentAdjustment, 132 /* right= */ mBorderBounds.right - mAlignmentAdjustment, 133 /* bottom= */ mBorderBounds.bottom - mAlignmentAdjustment, 134 /* rx= */ mBorderRadiusPx - mAlignmentAdjustment, 135 /* ry= */ mBorderRadiusPx - mAlignmentAdjustment, 136 /* paint= */ mBorderPaint); 137 } 138 139 /** 140 * Builds the border appearance/disappearance animation. 141 */ 142 @NonNull buildAnimator(boolean isAppearing)143 public Animator buildAnimator(boolean isAppearing) { 144 mBorderBoundsBuilder.updateBorderBounds(mBorderBounds); 145 mRunningBorderAnimation = mBorderAnimationProgress.animateToValue(isAppearing ? 1f : 0f); 146 mRunningBorderAnimation.setDuration( 147 isAppearing ? mAppearanceDurationMs : mDisappearanceDurationMs); 148 149 mRunningBorderAnimation.addListener( 150 AnimatorListeners.forEndCallback(() -> mRunningBorderAnimation = null)); 151 152 return mRunningBorderAnimation; 153 } 154 155 /** 156 * Immediately shows/hides the border without an animation. 157 * 158 * To animate the appearance/disappearance, see {@link BorderAnimator#buildAnimator(boolean)} 159 */ setBorderVisible(boolean visible)160 public void setBorderVisible(boolean visible) { 161 if (mRunningBorderAnimation != null) { 162 mRunningBorderAnimation.end(); 163 } 164 mBorderAnimationProgress.updateValue(visible ? 1f : 0f); 165 } 166 167 /** 168 * Callback to update the border bounds when building this animation. 169 */ 170 public interface BorderBoundsBuilder { 171 172 /** 173 * Sets the given rect to the most up-to-date bounds. 174 */ updateBorderBounds(Rect rect)175 void updateBorderBounds(Rect rect); 176 } 177 } 178