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 boundsBuilder callback to update the border bounds 118 * @param targetView the view that will be drawing the border 119 * @param contentView the view around which the border will be drawn. this view will be 120 * scaled down reciprocally to keep its original size and location. 121 * @param borderColor the border's color 122 * @param appearanceDurationMs appearance animation duration, in milliseconds 123 * @param disappearanceDurationMs disappearance animation duration, in milliseconds 124 * @param interpolator animation interpolator 125 */ 126 @JvmOverloads 127 @JvmStatic 128 fun createScalingBorderAnimator( 129 @Px borderRadiusPx: Int, 130 @Px borderWidthPx: Int, 131 boundsBuilder: (rect: Rect?) -> Unit, 132 targetView: View, 133 contentView: View, 134 @ColorInt borderColor: Int = DEFAULT_BORDER_COLOR, 135 appearanceDurationMs: Long = DEFAULT_APPEARANCE_ANIMATION_DURATION_MS, 136 disappearanceDurationMs: Long = DEFAULT_DISAPPEARANCE_ANIMATION_DURATION_MS, 137 interpolator: Interpolator = DEFAULT_INTERPOLATOR, 138 ): BorderAnimator { 139 return BorderAnimator( 140 borderRadiusPx, 141 borderColor, 142 ScalingParams(borderWidthPx, boundsBuilder, targetView, contentView), 143 appearanceDurationMs, 144 disappearanceDurationMs, 145 interpolator, 146 ) 147 } 148 } 149 150 private fun updateOutline() { 151 val interpolatedProgress = interpolator.getInterpolation(borderAnimationProgress.value) 152 borderAnimationParams.animationProgress = interpolatedProgress 153 borderPaint.alpha = (255 * interpolatedProgress).roundToInt() 154 borderPaint.strokeWidth = borderAnimationParams.borderWidth 155 borderAnimationParams.targetView.invalidate() 156 } 157 158 /** 159 * Draws the border on the given canvas. 160 * 161 * Call this method in the target view's [View.draw] method after calling super. 162 */ 163 fun drawBorder(canvas: Canvas) { 164 with(borderAnimationParams) { 165 val radius = borderRadiusPx + radiusAdjustment 166 canvas.drawRoundRect( 167 /* left= */ borderBounds.left + alignmentAdjustment, 168 /* top= */ borderBounds.top + alignmentAdjustment, 169 /* right= */ borderBounds.right - alignmentAdjustment, 170 /* bottom= */ borderBounds.bottom - alignmentAdjustment, 171 /* rx= */ radius, 172 /* ry= */ radius, 173 /* paint= */ borderPaint 174 ) 175 } 176 } 177 178 /** Builds the border appearance/disappearance animation. */ 179 fun buildAnimator(isAppearing: Boolean): Animator { 180 return borderAnimationProgress.animateToValue(if (isAppearing) 1f else 0f).apply { 181 duration = if (isAppearing) appearanceDurationMs else disappearanceDurationMs 182 doOnStart { 183 runningBorderAnimation?.cancel() 184 runningBorderAnimation = this 185 borderAnimationParams.onShowBorder() 186 } 187 doOnEnd { 188 runningBorderAnimation = null 189 if (!isAppearing) { 190 borderAnimationParams.onHideBorder() 191 } 192 } 193 } 194 } 195 196 /** Shows/hides the border, optionally with an animation. */ 197 fun setBorderVisibility(visible: Boolean, animated: Boolean) { 198 if (animated) { 199 buildAnimator(visible).start() 200 return 201 } 202 runningBorderAnimation?.end() 203 if (visible) { 204 borderAnimationParams.onShowBorder() 205 } 206 borderAnimationProgress.updateValue(if (visible) 1f else 0f) 207 if (!visible) { 208 borderAnimationParams.onHideBorder() 209 } 210 } 211 212 /** Params for handling different target view layout situations. */ 213 private abstract class BorderAnimationParams( 214 @field:Px @param:Px val borderWidthPx: Int, 215 private val boundsBuilder: (rect: Rect) -> Unit, 216 val targetView: View, 217 ) { 218 val borderBounds = Rect() 219 var animationProgress = 0f 220 private var layoutChangeListener: OnLayoutChangeListener? = null 221 222 abstract val alignmentAdjustmentInset: Int 223 abstract val radiusAdjustment: Float 224 225 val borderWidth: Float 226 get() = borderWidthPx * animationProgress 227 228 val alignmentAdjustment: Float 229 // Outset the border by half the width to create an outwards-growth animation 230 get() = -borderWidth / 2f + alignmentAdjustmentInset 231 232 open fun onShowBorder() { 233 if (layoutChangeListener == null) { 234 layoutChangeListener = OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> 235 onShowBorder() 236 targetView.invalidate() 237 } 238 targetView.addOnLayoutChangeListener(layoutChangeListener) 239 } 240 boundsBuilder(borderBounds) 241 } 242 243 open fun onHideBorder() { 244 if (layoutChangeListener != null) { 245 targetView.removeOnLayoutChangeListener(layoutChangeListener) 246 layoutChangeListener = null 247 } 248 } 249 } 250 251 /** BorderAnimationParams that simply draws the border outside the bounds of the target view. */ 252 private class SimpleParams( 253 @Px borderWidthPx: Int, 254 boundsBuilder: (Rect) -> Unit, 255 targetView: View, 256 ) : BorderAnimationParams(borderWidthPx, boundsBuilder, targetView) { 257 override val alignmentAdjustmentInset = 0 258 override val radiusAdjustment: Float 259 get() = -alignmentAdjustment 260 } 261 262 /** 263 * BorderAnimationParams that scales the target and content views to draw the border within the 264 * target's bounds without obscuring the content. 265 */ 266 private class ScalingParams( 267 @Px borderWidthPx: Int, 268 boundsBuilder: (rect: Rect?) -> Unit, 269 targetView: View, 270 private val contentView: View, 271 ) : BorderAnimationParams(borderWidthPx, boundsBuilder, targetView) { 272 // Inset the border since we are scaling the container up 273 override val alignmentAdjustmentInset = borderWidthPx 274 override val radiusAdjustment: Float 275 // Increase the radius since we are scaling the container up 276 get() = alignmentAdjustment 277 278 override fun onShowBorder() { 279 super.onShowBorder() 280 val tvWidth = targetView.width.toFloat() 281 val tvHeight = targetView.height.toFloat() 282 // Scale up just enough to make room for the border. Fail fast and fix the scaling 283 // onLayout. 284 val newScaleX = if (tvWidth == 0f) 1f else 1f + 2 * borderWidthPx / tvWidth 285 val newScaleY = if (tvHeight == 0f) 1f else 1f + 2 * borderWidthPx / tvHeight 286 with(targetView) { 287 pivotX = width / 2f 288 pivotY = height / 2f 289 scaleX = newScaleX 290 scaleY = newScaleY 291 } 292 with(contentView) { 293 pivotX = width / 2f 294 pivotY = height / 2f 295 scaleX = 1f / newScaleX 296 scaleY = 1f / newScaleY 297 } 298 } 299 300 override fun onHideBorder() { 301 super.onHideBorder() 302 with(targetView) { 303 pivotX = width.toFloat() 304 pivotY = height.toFloat() 305 scaleX = 1f 306 scaleY = 1f 307 } 308 with(contentView) { 309 pivotX = width / 2f 310 pivotY = height / 2f 311 scaleX = 1f 312 scaleY = 1f 313 } 314 } 315 } 316 } 317