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