1 /* 2 * Copyright (C) 2021 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 17 package com.android.systemui.statusbar.events 18 19 import android.content.Context 20 import android.graphics.Rect 21 import android.view.ContextThemeWrapper 22 import android.view.Gravity 23 import android.view.LayoutInflater 24 import android.view.View 25 import android.view.View.MeasureSpec.AT_MOST 26 import android.view.ViewGroup.LayoutParams.MATCH_PARENT 27 import android.view.ViewGroup.LayoutParams.WRAP_CONTENT 28 import android.widget.FrameLayout 29 import androidx.core.animation.Animator 30 import androidx.core.animation.AnimatorListenerAdapter 31 import androidx.core.animation.AnimatorSet 32 import androidx.core.animation.ValueAnimator 33 import com.android.internal.annotations.VisibleForTesting 34 import com.android.systemui.res.R 35 import com.android.systemui.statusbar.phone.StatusBarContentInsetsChangedListener 36 import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider 37 import com.android.systemui.statusbar.window.StatusBarWindowController 38 import com.android.systemui.util.animation.AnimationUtil.Companion.frames 39 import javax.inject.Inject 40 import kotlin.math.roundToInt 41 42 /** 43 * Controls the view for system event animations. 44 */ 45 class SystemEventChipAnimationController @Inject constructor( 46 private val context: Context, 47 private val statusBarWindowController: StatusBarWindowController, 48 private val contentInsetsProvider: StatusBarContentInsetsProvider 49 ) : SystemStatusAnimationCallback { 50 51 private lateinit var animationWindowView: FrameLayout 52 private lateinit var themedContext: ContextThemeWrapper 53 54 private var currentAnimatedView: BackgroundAnimatableView? = null 55 56 // Left for LTR, Right for RTL 57 private var animationDirection = LEFT 58 59 @VisibleForTesting var chipBounds = Rect() 60 private val chipWidth get() = chipBounds.width() 61 private val chipRight get() = chipBounds.right 62 private val chipLeft get() = chipBounds.left 63 private var chipMinWidth = context.resources.getDimensionPixelSize( 64 R.dimen.ongoing_appops_chip_min_animation_width) 65 66 private val dotSize = context.resources.getDimensionPixelSize( 67 R.dimen.ongoing_appops_dot_diameter) 68 // Use during animation so that multiple animators can update the drawing rect 69 private var animRect = Rect() 70 71 // TODO: move to dagger 72 @VisibleForTesting var initialized = false 73 74 /** 75 * Give the chip controller a chance to inflate and configure the chip view before we start 76 * animating 77 */ prepareChipAnimationnull78 fun prepareChipAnimation(viewCreator: ViewCreator) { 79 if (!initialized) { 80 init() 81 } 82 animationDirection = if (animationWindowView.isLayoutRtl) RIGHT else LEFT 83 84 // Initialize the animated view 85 val insets = contentInsetsProvider.getStatusBarContentInsetsForCurrentRotation() 86 currentAnimatedView = viewCreator(themedContext).also { 87 animationWindowView.addView( 88 it.view, 89 layoutParamsDefault( 90 if (animationWindowView.isLayoutRtl) insets.left 91 else insets.right)) 92 it.view.alpha = 0f 93 // For some reason, the window view's measured width is always 0 here, so use the 94 // parent (status bar) 95 it.view.measure( 96 View.MeasureSpec.makeMeasureSpec( 97 (animationWindowView.parent as View).width, AT_MOST), 98 View.MeasureSpec.makeMeasureSpec( 99 (animationWindowView.parent as View).height, AT_MOST)) 100 101 updateChipBounds(it, contentInsetsProvider.getStatusBarContentAreaForCurrentRotation()) 102 } 103 } 104 onSystemEventAnimationBeginnull105 override fun onSystemEventAnimationBegin(): Animator { 106 initializeAnimRect() 107 108 val alphaIn = ValueAnimator.ofFloat(0f, 1f).apply { 109 startDelay = 7.frames 110 duration = 5.frames 111 interpolator = null 112 addUpdateListener { currentAnimatedView?.view?.alpha = animatedValue as Float } 113 } 114 currentAnimatedView?.contentView?.alpha = 0f 115 val contentAlphaIn = ValueAnimator.ofFloat(0f, 1f).apply { 116 startDelay = 10.frames 117 duration = 10.frames 118 interpolator = null 119 addUpdateListener { currentAnimatedView?.contentView?.alpha = animatedValue as Float } 120 } 121 val moveIn = ValueAnimator.ofInt(chipMinWidth, chipWidth).apply { 122 startDelay = 7.frames 123 duration = 23.frames 124 interpolator = STATUS_BAR_X_MOVE_IN 125 addUpdateListener { updateAnimatedViewBoundsWidth(animatedValue as Int) } 126 } 127 val animSet = AnimatorSet() 128 animSet.playTogether(alphaIn, contentAlphaIn, moveIn) 129 return animSet 130 } 131 onSystemEventAnimationFinishnull132 override fun onSystemEventAnimationFinish(hasPersistentDot: Boolean): Animator { 133 initializeAnimRect() 134 val finish = if (hasPersistentDot) { 135 createMoveOutAnimationForDot() 136 } else { 137 createMoveOutAnimationDefault() 138 } 139 140 finish.addListener(object : AnimatorListenerAdapter() { 141 override fun onAnimationEnd(animation: Animator) { 142 animationWindowView.removeView(currentAnimatedView!!.view) 143 } 144 }) 145 146 return finish 147 } 148 createMoveOutAnimationForDotnull149 private fun createMoveOutAnimationForDot(): Animator { 150 val width1 = ValueAnimator.ofInt(chipWidth, chipMinWidth).apply { 151 duration = 9.frames 152 interpolator = STATUS_CHIP_WIDTH_TO_DOT_KEYFRAME_1 153 addUpdateListener { 154 updateAnimatedViewBoundsWidth(animatedValue as Int) 155 } 156 } 157 158 val width2 = ValueAnimator.ofInt(chipMinWidth, dotSize).apply { 159 startDelay = 9.frames 160 duration = 20.frames 161 interpolator = STATUS_CHIP_WIDTH_TO_DOT_KEYFRAME_2 162 addUpdateListener { 163 updateAnimatedViewBoundsWidth(animatedValue as Int) 164 } 165 } 166 167 val keyFrame1Height = dotSize * 2 168 val chipVerticalCenter = chipBounds.top + chipBounds.height() / 2 169 val height1 = ValueAnimator.ofInt(chipBounds.height(), keyFrame1Height).apply { 170 startDelay = 8.frames 171 duration = 6.frames 172 interpolator = STATUS_CHIP_HEIGHT_TO_DOT_KEYFRAME_1 173 addUpdateListener { 174 updateAnimatedViewBoundsHeight(animatedValue as Int, chipVerticalCenter) 175 } 176 } 177 178 val height2 = ValueAnimator.ofInt(keyFrame1Height, dotSize).apply { 179 startDelay = 14.frames 180 duration = 15.frames 181 interpolator = STATUS_CHIP_HEIGHT_TO_DOT_KEYFRAME_2 182 addUpdateListener { 183 updateAnimatedViewBoundsHeight(animatedValue as Int, chipVerticalCenter) 184 } 185 } 186 187 // Move the chip view to overlap exactly with the privacy dot. The chip displays by default 188 // exactly adjacent to the dot, so we can just move over by the diameter of the dot itself 189 val moveOut = ValueAnimator.ofInt(0, dotSize).apply { 190 startDelay = 3.frames 191 duration = 11.frames 192 interpolator = STATUS_CHIP_MOVE_TO_DOT 193 addUpdateListener { 194 // If RTL, we can just invert the move 195 val amt = if (animationDirection == LEFT) { 196 animatedValue as Int 197 } else { 198 -(animatedValue as Int) 199 } 200 updateAnimatedBoundsX(amt) 201 } 202 } 203 204 val animSet = AnimatorSet() 205 animSet.playTogether(width1, width2, height1, height2, moveOut) 206 return animSet 207 } 208 createMoveOutAnimationDefaultnull209 private fun createMoveOutAnimationDefault(): Animator { 210 val alphaOut = ValueAnimator.ofFloat(1f, 0f).apply { 211 startDelay = 6.frames 212 duration = 6.frames 213 interpolator = null 214 addUpdateListener { currentAnimatedView?.view?.alpha = animatedValue as Float } 215 } 216 217 val contentAlphaOut = ValueAnimator.ofFloat(1f, 0f).apply { 218 duration = 5.frames 219 interpolator = null 220 addUpdateListener { currentAnimatedView?.contentView?.alpha = animatedValue as Float } 221 } 222 223 val moveOut = ValueAnimator.ofInt(chipWidth, chipMinWidth).apply { 224 duration = 23.frames 225 interpolator = STATUS_BAR_X_MOVE_OUT 226 addUpdateListener { 227 currentAnimatedView?.apply { 228 updateAnimatedViewBoundsWidth(animatedValue as Int) 229 } 230 } 231 } 232 233 val animSet = AnimatorSet() 234 animSet.playTogether(alphaOut, contentAlphaOut, moveOut) 235 return animSet 236 } 237 initnull238 fun init() { 239 initialized = true 240 themedContext = ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings) 241 animationWindowView = LayoutInflater.from(themedContext) 242 .inflate(R.layout.system_event_animation_window, null) as FrameLayout 243 // Matches status_bar.xml 244 val height = themedContext.resources.getDimensionPixelSize(R.dimen.status_bar_height) 245 val lp = FrameLayout.LayoutParams(MATCH_PARENT, height) 246 lp.gravity = Gravity.END or Gravity.TOP 247 statusBarWindowController.addViewToWindow(animationWindowView, lp) 248 animationWindowView.clipToPadding = false 249 animationWindowView.clipChildren = false 250 251 // Use contentInsetsProvider rather than configuration controller, since we only care 252 // about status bar dimens 253 contentInsetsProvider.addCallback(object : StatusBarContentInsetsChangedListener { 254 override fun onStatusBarContentInsetsChanged() { 255 val newContentArea = contentInsetsProvider 256 .getStatusBarContentAreaForCurrentRotation() 257 updateDimens(newContentArea) 258 259 // If we are currently animating, we have to re-solve for the chip bounds. If we're 260 // not animating then [prepareChipAnimation] will take care of it for us 261 currentAnimatedView?.let { 262 updateChipBounds(it, newContentArea) 263 // Since updateCurrentAnimatedView can only be called during an animation, we 264 // have to create a dummy animator here to apply the new chip bounds 265 val animator = ValueAnimator.ofInt(0, 1).setDuration(0) 266 animator.addUpdateListener { updateCurrentAnimatedView() } 267 animator.start() 268 } 269 } 270 }) 271 } 272 273 /** Announces [contentDescriptions] for accessibility. */ announceForAccessibilitynull274 fun announceForAccessibility(contentDescriptions: String) { 275 currentAnimatedView?.view?.announceForAccessibility(contentDescriptions) 276 } 277 updateDimensnull278 private fun updateDimens(contentArea: Rect) { 279 val lp = animationWindowView.layoutParams as FrameLayout.LayoutParams 280 lp.height = contentArea.height() 281 282 animationWindowView.layoutParams = lp 283 } 284 285 /** 286 * Use the current status bar content area and the current chip's measured size to update 287 * the animation rect and chipBounds. This method can be called at any time and will update 288 * the current animation values properly during e.g. a rotation. 289 */ updateChipBoundsnull290 private fun updateChipBounds(chip: BackgroundAnimatableView, contentArea: Rect) { 291 // decide which direction we're animating from, and then set some screen coordinates 292 val chipTop = contentArea.top + (contentArea.height() - chip.view.measuredHeight) / 2 293 val chipBottom = chipTop + chip.view.measuredHeight 294 val chipRight: Int 295 val chipLeft: Int 296 297 when (animationDirection) { 298 LEFT -> { 299 chipRight = contentArea.right 300 chipLeft = contentArea.right - chip.chipWidth 301 } 302 else /* RIGHT */ -> { 303 chipLeft = contentArea.left 304 chipRight = contentArea.left + chip.chipWidth 305 } 306 } 307 chipBounds = Rect(chipLeft, chipTop, chipRight, chipBottom) 308 animRect.set(chipBounds) 309 } 310 layoutParamsDefaultnull311 private fun layoutParamsDefault(marginEnd: Int): FrameLayout.LayoutParams = 312 FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).also { 313 it.gravity = Gravity.END or Gravity.CENTER_VERTICAL 314 it.marginEnd = marginEnd 315 } 316 initializeAnimRectnull317 private fun initializeAnimRect() = animRect.set(chipBounds) 318 319 320 /** 321 * To be called during an animation, sets the width and updates the current animated chip view 322 */ 323 private fun updateAnimatedViewBoundsWidth(width: Int) { 324 when (animationDirection) { 325 LEFT -> { 326 animRect.set((chipRight - width), animRect.top, chipRight, animRect.bottom) 327 } else /* RIGHT */ -> { 328 animRect.set(chipLeft, animRect.top, (chipLeft + width), animRect.bottom) 329 } 330 } 331 332 updateCurrentAnimatedView() 333 } 334 335 /** 336 * To be called during an animation, updates the animation rect and sends the update to the chip 337 */ updateAnimatedViewBoundsHeightnull338 private fun updateAnimatedViewBoundsHeight(height: Int, verticalCenter: Int) { 339 animRect.set( 340 animRect.left, 341 verticalCenter - (height.toFloat() / 2).roundToInt(), 342 animRect.right, 343 verticalCenter + (height.toFloat() / 2).roundToInt()) 344 345 updateCurrentAnimatedView() 346 } 347 348 /** 349 * To be called during an animation, updates the animation rect offset and updates the chip 350 */ updateAnimatedBoundsXnull351 private fun updateAnimatedBoundsX(translation: Int) { 352 currentAnimatedView?.view?.translationX = translation.toFloat() 353 } 354 355 /** 356 * To be called during an animation. Sets the chip rect to animRect 357 */ updateCurrentAnimatedViewnull358 private fun updateCurrentAnimatedView() { 359 currentAnimatedView?.setBoundsForAnimation( 360 animRect.left, animRect.top, animRect.right, animRect.bottom 361 ) 362 } 363 } 364 365 /** 366 * Chips should provide a view that can be animated with something better than a fade-in 367 */ 368 interface BackgroundAnimatableView { 369 val view: View // Since this can't extend View, add a view prop 370 get() = this as View 371 val contentView: View? // This will be alpha faded during appear and disappear animation 372 get() = null 373 val chipWidth: Int 374 get() = view.measuredWidth setBoundsForAnimationnull375 fun setBoundsForAnimation(l: Int, t: Int, r: Int, b: Int) 376 } 377 378 // Animation directions 379 private const val LEFT = 1 380 private const val RIGHT = 2 381