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.systemui.R 34 import com.android.systemui.flags.FeatureFlags 35 import com.android.systemui.flags.Flags 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 private val featureFlags: FeatureFlags 50 ) : SystemStatusAnimationCallback { 51 52 private lateinit var animationWindowView: FrameLayout 53 private lateinit var themedContext: ContextThemeWrapper 54 55 private var currentAnimatedView: BackgroundAnimatableView? = null 56 57 // Left for LTR, Right for RTL 58 private var animationDirection = LEFT 59 private 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 private 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.first 91 else insets.second)) 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 // decide which direction we're animating from, and then set some screen coordinates 102 val contentRect = contentInsetsProvider.getStatusBarContentAreaForCurrentRotation() 103 val chipTop = ((animationWindowView.parent as View).height - it.view.measuredHeight) / 2 104 val chipBottom = chipTop + it.view.measuredHeight 105 val chipRight: Int 106 val chipLeft: Int 107 when (animationDirection) { 108 LEFT -> { 109 chipRight = contentRect.right 110 chipLeft = contentRect.right - it.chipWidth 111 } 112 else /* RIGHT */ -> { 113 chipLeft = contentRect.left 114 chipRight = contentRect.left + it.chipWidth 115 } 116 } 117 chipBounds = Rect(chipLeft, chipTop, chipRight, chipBottom) 118 } 119 } 120 onSystemEventAnimationBeginnull121 override fun onSystemEventAnimationBegin(): Animator { 122 initializeAnimRect() 123 124 val alphaIn = ValueAnimator.ofFloat(0f, 1f).apply { 125 startDelay = 7.frames 126 duration = 5.frames 127 interpolator = null 128 addUpdateListener { currentAnimatedView?.view?.alpha = animatedValue as Float } 129 } 130 currentAnimatedView?.contentView?.alpha = 0f 131 val contentAlphaIn = ValueAnimator.ofFloat(0f, 1f).apply { 132 startDelay = 10.frames 133 duration = 10.frames 134 interpolator = null 135 addUpdateListener { currentAnimatedView?.contentView?.alpha = animatedValue as Float } 136 } 137 val moveIn = ValueAnimator.ofInt(chipMinWidth, chipWidth).apply { 138 startDelay = 7.frames 139 duration = 23.frames 140 interpolator = STATUS_BAR_X_MOVE_IN 141 addUpdateListener { updateAnimatedViewBoundsWidth(animatedValue as Int) } 142 } 143 val animSet = AnimatorSet() 144 animSet.playTogether(alphaIn, contentAlphaIn, moveIn) 145 return animSet 146 } 147 onSystemEventAnimationFinishnull148 override fun onSystemEventAnimationFinish(hasPersistentDot: Boolean): Animator { 149 initializeAnimRect() 150 val finish = if (hasPersistentDot) { 151 createMoveOutAnimationForDot() 152 } else { 153 createMoveOutAnimationDefault() 154 } 155 156 finish.addListener(object : AnimatorListenerAdapter() { 157 override fun onAnimationEnd(animation: Animator) { 158 animationWindowView.removeView(currentAnimatedView!!.view) 159 } 160 }) 161 162 return finish 163 } 164 createMoveOutAnimationForDotnull165 private fun createMoveOutAnimationForDot(): Animator { 166 val width1 = ValueAnimator.ofInt(chipWidth, chipMinWidth).apply { 167 duration = 9.frames 168 interpolator = STATUS_CHIP_WIDTH_TO_DOT_KEYFRAME_1 169 addUpdateListener { 170 updateAnimatedViewBoundsWidth(animatedValue as Int) 171 } 172 } 173 174 val width2 = ValueAnimator.ofInt(chipMinWidth, dotSize).apply { 175 startDelay = 9.frames 176 duration = 20.frames 177 interpolator = STATUS_CHIP_WIDTH_TO_DOT_KEYFRAME_2 178 addUpdateListener { 179 updateAnimatedViewBoundsWidth(animatedValue as Int) 180 } 181 } 182 183 val keyFrame1Height = dotSize * 2 184 val v = currentAnimatedView!!.view 185 val chipVerticalCenter = v.top + v.measuredHeight / 2 186 val height1 = ValueAnimator.ofInt( 187 currentAnimatedView!!.view.measuredHeight, keyFrame1Height).apply { 188 startDelay = 8.frames 189 duration = 6.frames 190 interpolator = STATUS_CHIP_HEIGHT_TO_DOT_KEYFRAME_1 191 addUpdateListener { 192 updateAnimatedViewBoundsHeight(animatedValue as Int, chipVerticalCenter) 193 } 194 } 195 196 val height2 = ValueAnimator.ofInt(keyFrame1Height, dotSize).apply { 197 startDelay = 14.frames 198 duration = 15.frames 199 interpolator = STATUS_CHIP_HEIGHT_TO_DOT_KEYFRAME_2 200 addUpdateListener { 201 updateAnimatedViewBoundsHeight(animatedValue as Int, chipVerticalCenter) 202 } 203 } 204 205 // Move the chip view to overlap exactly with the privacy dot. The chip displays by default 206 // exactly adjacent to the dot, so we can just move over by the diameter of the dot itself 207 val moveOut = ValueAnimator.ofInt(0, dotSize).apply { 208 startDelay = 3.frames 209 duration = 11.frames 210 interpolator = STATUS_CHIP_MOVE_TO_DOT 211 addUpdateListener { 212 // If RTL, we can just invert the move 213 val amt = if (animationDirection == LEFT) { 214 animatedValue as Int 215 } else { 216 -(animatedValue as Int) 217 } 218 updateAnimatedBoundsX(amt) 219 } 220 } 221 222 val animSet = AnimatorSet() 223 animSet.playTogether(width1, width2, height1, height2, moveOut) 224 return animSet 225 } 226 createMoveOutAnimationDefaultnull227 private fun createMoveOutAnimationDefault(): Animator { 228 val alphaOut = ValueAnimator.ofFloat(1f, 0f).apply { 229 startDelay = 6.frames 230 duration = 6.frames 231 interpolator = null 232 addUpdateListener { currentAnimatedView?.view?.alpha = animatedValue as Float } 233 } 234 235 val contentAlphaOut = ValueAnimator.ofFloat(1f, 0f).apply { 236 duration = 5.frames 237 interpolator = null 238 addUpdateListener { currentAnimatedView?.contentView?.alpha = animatedValue as Float } 239 } 240 241 val moveOut = ValueAnimator.ofInt(chipWidth, chipMinWidth).apply { 242 duration = 23.frames 243 interpolator = STATUS_BAR_X_MOVE_OUT 244 addUpdateListener { 245 currentAnimatedView?.apply { 246 updateAnimatedViewBoundsWidth(animatedValue as Int) 247 } 248 } 249 } 250 251 val animSet = AnimatorSet() 252 animSet.playTogether(alphaOut, contentAlphaOut, moveOut) 253 return animSet 254 } 255 initnull256 private fun init() { 257 initialized = true 258 themedContext = ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings) 259 animationWindowView = LayoutInflater.from(themedContext) 260 .inflate(R.layout.system_event_animation_window, null) as FrameLayout 261 val lp = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) 262 lp.gravity = Gravity.END or Gravity.CENTER_VERTICAL 263 statusBarWindowController.addViewToWindow(animationWindowView, lp) 264 animationWindowView.clipToPadding = false 265 animationWindowView.clipChildren = false 266 } 267 layoutParamsDefaultnull268 private fun layoutParamsDefault(marginEnd: Int): FrameLayout.LayoutParams = 269 FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).also { 270 it.gravity = Gravity.END or Gravity.CENTER_VERTICAL 271 it.marginEnd = marginEnd 272 } 273 initializeAnimRectnull274 private fun initializeAnimRect() = if (featureFlags.isEnabled(Flags.PLUG_IN_STATUS_BAR_CHIP)) { 275 animRect.set(chipBounds) 276 } else { 277 animRect.set( 278 chipLeft, 279 currentAnimatedView!!.view.top, 280 chipRight, 281 currentAnimatedView!!.view.bottom) 282 } 283 284 /** 285 * To be called during an animation, sets the width and updates the current animated chip view 286 */ updateAnimatedViewBoundsWidthnull287 private fun updateAnimatedViewBoundsWidth(width: Int) { 288 when (animationDirection) { 289 LEFT -> { 290 animRect.set((chipRight - width), animRect.top, chipRight, animRect.bottom) 291 } else /* RIGHT */ -> { 292 animRect.set(chipLeft, animRect.top, (chipLeft + width), animRect.bottom) 293 } 294 } 295 296 updateCurrentAnimatedView() 297 } 298 299 /** 300 * To be called during an animation, updates the animation rect and sends the update to the chip 301 */ updateAnimatedViewBoundsHeightnull302 private fun updateAnimatedViewBoundsHeight(height: Int, verticalCenter: Int) { 303 animRect.set( 304 animRect.left, 305 verticalCenter - (height.toFloat() / 2).roundToInt(), 306 animRect.right, 307 verticalCenter + (height.toFloat() / 2).roundToInt()) 308 309 updateCurrentAnimatedView() 310 } 311 312 /** 313 * To be called during an animation, updates the animation rect offset and updates the chip 314 */ updateAnimatedBoundsXnull315 private fun updateAnimatedBoundsX(translation: Int) { 316 currentAnimatedView?.view?.translationX = translation.toFloat() 317 } 318 319 /** 320 * To be called during an animation. Sets the chip rect to animRect 321 */ updateCurrentAnimatedViewnull322 private fun updateCurrentAnimatedView() { 323 currentAnimatedView?.setBoundsForAnimation( 324 animRect.left, animRect.top, animRect.right, animRect.bottom 325 ) 326 } 327 } 328 329 /** 330 * Chips should provide a view that can be animated with something better than a fade-in 331 */ 332 interface BackgroundAnimatableView { 333 val view: View // Since this can't extend View, add a view prop 334 get() = this as View 335 val contentView: View? // This will be alpha faded during appear and disappear animation 336 get() = null 337 val chipWidth: Int 338 get() = view.measuredWidth setBoundsForAnimationnull339 fun setBoundsForAnimation(l: Int, t: Int, r: Int, b: Int) 340 } 341 342 // Animation directions 343 private const val LEFT = 1 344 private const val RIGHT = 2 345