• 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.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