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