• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * 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.graphics.Canvas
21 import android.graphics.Color
22 import android.graphics.Paint
23 import android.graphics.Rect
24 import android.view.View
25 import android.view.View.OnLayoutChangeListener
26 import android.view.animation.Interpolator
27 import androidx.annotation.Px
28 import androidx.core.animation.doOnEnd
29 import androidx.core.animation.doOnStart
30 import com.android.app.animation.Interpolators
31 import com.android.launcher3.anim.AnimatedFloat
32 import kotlin.math.roundToInt
33 
34 /**
35  * Utility class for drawing a rounded-rect border around a view.
36  *
37  * To use this class:
38  * 1. Create an instance in the target view. NOTE: The border will animate outwards from the
39  *    provided border bounds.
40  * 2. Override the target view's [View.draw] method and call [drawBorder] after
41  *    `super.draw(canvas)`.
42  * 3. Call [buildAnimator] and start the animation or call [setBorderVisibility] where appropriate.
43  */
44 class BorderAnimator
45 private constructor(
46     @field:Px @param:Px private val borderRadiusPx: Int,
47     @ColorInt borderColor: Int,
48     private val borderAnimationParams: BorderAnimationParams,
49     private val appearanceDurationMs: Long,
50     private val disappearanceDurationMs: Long,
51     private val interpolator: Interpolator,
52 ) {
53     private val borderAnimationProgress = AnimatedFloat { _ -> updateOutline() }
54     private val borderPaint =
55         Paint(Paint.ANTI_ALIAS_FLAG).apply {
56             color = borderColor
57             style = Paint.Style.STROKE
58             alpha = 0
59         }
60     private var runningBorderAnimation: Animator? = null
61 
62     companion object {
63         const val DEFAULT_BORDER_COLOR = Color.WHITE
64         private const val DEFAULT_APPEARANCE_ANIMATION_DURATION_MS = 300L
65         private const val DEFAULT_DISAPPEARANCE_ANIMATION_DURATION_MS = 133L
66         private val DEFAULT_INTERPOLATOR = Interpolators.EMPHASIZED_DECELERATE
67 
68         /**
69          * Creates a BorderAnimator that simply draws the border outside the bound of the target
70          * view.
71          *
72          * Use this method if the border can be drawn outside the target view's bounds without any
73          * additional logic.
74          *
75          * @param borderRadiusPx the radius of the border's corners, in pixels
76          * @param borderWidthPx the width of the border, in pixels
77          * @param boundsBuilder callback to update the border bounds
78          * @param targetView the view that will be drawing the border
79          * @param borderColor the border's color
80          * @param appearanceDurationMs appearance animation duration, in milliseconds
81          * @param disappearanceDurationMs disappearance animation duration, in milliseconds
82          * @param interpolator animation interpolator
83          */
84         @JvmOverloads
85         @JvmStatic
86         fun createSimpleBorderAnimator(
87             @Px borderRadiusPx: Int,
88             @Px borderWidthPx: Int,
89             boundsBuilder: (Rect) -> Unit,
90             targetView: View,
91             @ColorInt borderColor: Int = DEFAULT_BORDER_COLOR,
92             appearanceDurationMs: Long = DEFAULT_APPEARANCE_ANIMATION_DURATION_MS,
93             disappearanceDurationMs: Long = DEFAULT_DISAPPEARANCE_ANIMATION_DURATION_MS,
94             interpolator: Interpolator = DEFAULT_INTERPOLATOR,
95         ): BorderAnimator {
96             return BorderAnimator(
97                 borderRadiusPx,
98                 borderColor,
99                 SimpleParams(borderWidthPx, boundsBuilder, targetView),
100                 appearanceDurationMs,
101                 disappearanceDurationMs,
102                 interpolator,
103             )
104         }
105 
106         /**
107          * Creates a BorderAnimator that scales the target and content views to draw the border
108          * within the target's bounds without obscuring the content.
109          *
110          * Use this method if the border would otherwise be clipped by the target view's bound.
111          *
112          * Note: using this method will set the scales and pivots of the container and content
113          * views, however will only reset the scales back to 1.
114          *
115          * @param borderRadiusPx the radius of the border's corners, in pixels
116          * @param borderWidthPx the width of the border, in pixels
117          * @param borderStrokePx the stroke width used to paint the border, in pixels. If smaller
118          *     than border width, it gets drawn at the outside edge of the border.
119          * @param boundsBuilder callback to update the border bounds
120          * @param targetView the view that will be drawing the border
121          * @param contentView the view around which the border will be drawn. this view will be
122          *   scaled down reciprocally to keep its original size and location.
123          * @param borderColor the border's color
124          * @param appearanceDurationMs appearance animation duration, in milliseconds
125          * @param disappearanceDurationMs disappearance animation duration, in milliseconds
126          * @param interpolator animation interpolator
127          */
128         @JvmOverloads
129         @JvmStatic
130         fun createScalingBorderAnimator(
131             @Px borderRadiusPx: Int,
132             @Px borderWidthPx: Int,
133             @Px borderStrokePx: Int,
134             boundsBuilder: (rect: Rect?) -> Unit,
135             targetView: View,
136             contentView: View,
137             @ColorInt borderColor: Int = DEFAULT_BORDER_COLOR,
138             appearanceDurationMs: Long = DEFAULT_APPEARANCE_ANIMATION_DURATION_MS,
139             disappearanceDurationMs: Long = DEFAULT_DISAPPEARANCE_ANIMATION_DURATION_MS,
140             interpolator: Interpolator = DEFAULT_INTERPOLATOR,
141         ): BorderAnimator {
142             return BorderAnimator(
143                 borderRadiusPx,
144                 borderColor,
145                 ScalingParams(
146                     borderWidthPx,
147                     borderStrokePx,
148                     boundsBuilder,
149                     targetView,
150                     contentView,
151                 ),
152                 appearanceDurationMs,
153                 disappearanceDurationMs,
154                 interpolator,
155             )
156         }
157     }
158 
159     private fun updateOutline() {
160         val interpolatedProgress = interpolator.getInterpolation(borderAnimationProgress.value)
161         borderAnimationParams.animationProgress = interpolatedProgress
162         borderPaint.alpha = (255 * interpolatedProgress).roundToInt()
163         borderPaint.strokeWidth = borderAnimationParams.borderStroke
164         borderAnimationParams.targetView.invalidate()
165     }
166 
167     /**
168      * Draws the border on the given canvas.
169      *
170      * Call this method in the target view's [View.draw] method after calling super.
171      */
172     fun drawBorder(canvas: Canvas) {
173         with(borderAnimationParams) {
174             val radius = borderRadiusPx + radiusAdjustment
175             canvas.drawRoundRect(
176                 /* left= */ borderBounds.left + alignmentAdjustment,
177                 /* top= */ borderBounds.top + alignmentAdjustment,
178                 /* right= */ borderBounds.right - alignmentAdjustment,
179                 /* bottom= */ borderBounds.bottom - alignmentAdjustment,
180                 /* rx= */ radius,
181                 /* ry= */ radius,
182                 /* paint= */ borderPaint,
183             )
184         }
185     }
186 
187     /** Builds the border appearance/disappearance animation. */
188     fun buildAnimator(isAppearing: Boolean): Animator {
189         return borderAnimationProgress.animateToValue(if (isAppearing) 1f else 0f).apply {
190             duration = if (isAppearing) appearanceDurationMs else disappearanceDurationMs
191             doOnStart {
192                 runningBorderAnimation?.cancel()
193                 runningBorderAnimation = this
194                 borderAnimationParams.onShowBorder()
195             }
196             doOnEnd {
197                 runningBorderAnimation = null
198                 if (!isAppearing) {
199                     borderAnimationParams.onHideBorder()
200                 }
201             }
202         }
203     }
204 
205     /** Shows/hides the border, optionally with an animation. */
206     fun setBorderVisibility(visible: Boolean, animated: Boolean) {
207         if (animated) {
208             buildAnimator(visible).start()
209             return
210         }
211         runningBorderAnimation?.end()
212         if (visible) {
213             borderAnimationParams.onShowBorder()
214         }
215         borderAnimationProgress.updateValue(if (visible) 1f else 0f)
216         if (!visible) {
217             borderAnimationParams.onHideBorder()
218         }
219     }
220 
221     /** Params for handling different target view layout situations. */
222     private abstract class BorderAnimationParams(
223         @field:Px @param:Px val borderWidthPx: Int,
224         @field:Px @param:Px val borderStrokePx: Int,
225         private val boundsBuilder: (rect: Rect) -> Unit,
226         val targetView: View,
227     ) {
228         val borderBounds = Rect()
229         var animationProgress = 0f
230         private var layoutChangeListener: OnLayoutChangeListener? = null
231 
232         abstract val alignmentAdjustmentInset: Int
233         abstract val radiusAdjustment: Float
234 
235         val borderStroke: Float
236             get() = borderStrokePx * animationProgress
237 
238         val alignmentAdjustment: Float
239             // Outset the border by half the width to create an outwards-growth animation
240             get() = -borderStroke / 2f + alignmentAdjustmentInset
241 
242         open fun onShowBorder() {
243             if (layoutChangeListener == null) {
244                 layoutChangeListener = OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
245                     onShowBorder()
246                     targetView.invalidate()
247                 }
248                 targetView.addOnLayoutChangeListener(layoutChangeListener)
249             }
250             boundsBuilder(borderBounds)
251         }
252 
253         open fun onHideBorder() {
254             if (layoutChangeListener != null) {
255                 targetView.removeOnLayoutChangeListener(layoutChangeListener)
256                 layoutChangeListener = null
257             }
258         }
259     }
260 
261     /** BorderAnimationParams that simply draws the border outside the bounds of the target view. */
262     private class SimpleParams(
263         @Px borderWidthPx: Int,
264         boundsBuilder: (Rect) -> Unit,
265         targetView: View,
266     ) : BorderAnimationParams(borderWidthPx, borderWidthPx, boundsBuilder, targetView) {
267         override val alignmentAdjustmentInset = 0
268         override val radiusAdjustment: Float
269             get() = -alignmentAdjustment
270     }
271 
272     /**
273      * BorderAnimationParams that scales the target and content views to draw the border within the
274      * target's bounds without obscuring the content.
275      */
276     private class ScalingParams(
277         @Px borderWidthPx: Int,
278         @Px borderStrokePx: Int,
279         boundsBuilder: (rect: Rect?) -> Unit,
280         targetView: View,
281         private val contentView: View,
282     ) : BorderAnimationParams(borderWidthPx, borderStrokePx, boundsBuilder, targetView) {
283         // Inset the border since we are scaling the container up
284         override val alignmentAdjustmentInset = borderStrokePx
285         override val radiusAdjustment: Float
286             // Increase the radius since we are scaling the container up
287             get() = alignmentAdjustment
288 
289         override fun onShowBorder() {
290             super.onShowBorder()
291             val tvWidth = targetView.width.toFloat()
292             val tvHeight = targetView.height.toFloat()
293             // Scale up just enough to make room for the border. Fail fast and fix the scaling
294             // onLayout.
295             val newScaleX = if (tvWidth == 0f) 1f else 1f + 2 * borderWidthPx / tvWidth
296             val newScaleY = if (tvHeight == 0f) 1f else 1f + 2 * borderWidthPx / tvHeight
297             with(targetView) {
298                 pivotX = width / 2f
299                 pivotY = height / 2f
300                 scaleX = newScaleX
301                 scaleY = newScaleY
302             }
303             with(contentView) {
304                 pivotX = width / 2f
305                 pivotY = height / 2f
306                 scaleX = 1f / newScaleX
307                 scaleY = 1f / newScaleY
308             }
309         }
310 
311         override fun onHideBorder() {
312             super.onHideBorder()
313             with(targetView) {
314                 pivotX = width.toFloat()
315                 pivotY = height.toFloat()
316                 scaleX = 1f
317                 scaleY = 1f
318             }
319             with(contentView) {
320                 pivotX = width / 2f
321                 pivotY = height / 2f
322                 scaleX = 1f
323                 scaleY = 1f
324             }
325         }
326     }
327 }
328