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