• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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