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