• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 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.wm.shell.windowdecor
18 
19 import android.animation.Animator
20 import android.animation.AnimatorSet
21 import android.animation.ObjectAnimator
22 import android.view.View
23 import android.view.View.ALPHA
24 import android.view.View.SCALE_X
25 import android.view.View.SCALE_Y
26 import android.view.View.TRANSLATION_Y
27 import android.view.View.TRANSLATION_Z
28 import android.view.ViewGroup
29 import android.view.accessibility.AccessibilityEvent
30 import android.widget.Button
31 import androidx.core.animation.doOnEnd
32 import androidx.core.view.children
33 import com.android.wm.shell.R
34 import com.android.wm.shell.shared.animation.Interpolators
35 
36 /** Animates the Handle Menu opening. */
37 class HandleMenuAnimator(
38     private val handleMenu: View,
39     private val menuWidth: Int,
40     private val captionHeight: Float
41 ) {
42     companion object {
43         // Open animation constants
44         private const val MENU_Y_TRANSLATION_OPEN_DURATION: Long = 150
45         private const val HEADER_NONFREEFORM_SCALE_OPEN_DURATION: Long = 150
46         private const val HEADER_FREEFORM_SCALE_OPEN_DURATION: Long = 217
47         private const val HEADER_ELEVATION_OPEN_DURATION: Long = 83
48         private const val HEADER_CONTENT_ALPHA_OPEN_DURATION: Long = 100
49         private const val BODY_SCALE_OPEN_DURATION: Long = 180
50         private const val BODY_ALPHA_OPEN_DURATION: Long = 150
51         private const val BODY_ELEVATION_OPEN_DURATION: Long = 83
52         private const val BODY_CONTENT_ALPHA_OPEN_DURATION: Long = 167
53 
54         private const val ELEVATION_OPEN_DELAY: Long = 33
55         private const val HEADER_CONTENT_ALPHA_OPEN_DELAY: Long = 67
56         private const val BODY_SCALE_OPEN_DELAY: Long = 50
57         private const val BODY_ALPHA_OPEN_DELAY: Long = 133
58 
59         private const val HALF_INITIAL_SCALE: Float = 0.5f
60         private const val NONFREEFORM_HEADER_INITIAL_SCALE_X: Float = 0.6f
61         private const val NONFREEFORM_HEADER_INITIAL_SCALE_Y: Float = 0.05f
62 
63         // Close animation constants
64         private const val HEADER_CLOSE_DELAY: Long = 20
65         private const val HEADER_CLOSE_DURATION: Long = 50
66         private const val HEADER_CONTENT_OPACITY_CLOSE_DELAY: Long = 25
67         private const val HEADER_CONTENT_OPACITY_CLOSE_DURATION: Long = 25
68         private const val BODY_CLOSE_DURATION: Long = 50
69     }
70 
71     private val animators: MutableList<Animator> = mutableListOf()
72     private var runningAnimation: AnimatorSet? = null
73 
74     private val appInfoPill: ViewGroup = handleMenu.requireViewById(R.id.app_info_pill)
75     private val windowingPill: ViewGroup = handleMenu.requireViewById(R.id.windowing_pill)
76     private val moreActionsPill: ViewGroup = handleMenu.requireViewById(R.id.more_actions_pill)
77     private val openInAppOrBrowserPill: ViewGroup =
78         handleMenu.requireViewById(R.id.open_in_app_or_browser_pill)
79 
80     /** Animates the opening of the handle menu. */
animateOpennull81     fun animateOpen() {
82         prepareMenuForAnimation()
83         appInfoPillExpand()
84         animateAppInfoPillOpen()
85         animateWindowingPillOpen()
86         animateMoreActionsPillOpen()
87         animateOpenInAppOrBrowserPill()
88         runAnimations {
89             appInfoPill.post {
90                 appInfoPill.requireViewById<View>(R.id.collapse_menu_button).sendAccessibilityEvent(
91                     AccessibilityEvent.TYPE_VIEW_FOCUSED)
92             }
93         }
94     }
95 
96     /**
97      * Animates the opening of the handle menu. The caption handle in full screen and split screen
98      * will expand until it assumes the shape of the app info pill. Then, the other two pills will
99      * appear.
100      */
animateCaptionHandleExpandToOpennull101     fun animateCaptionHandleExpandToOpen() {
102         prepareMenuForAnimation()
103         captionHandleExpandIntoAppInfoPill()
104         animateAppInfoPillOpen()
105         animateWindowingPillOpen()
106         animateMoreActionsPillOpen()
107         animateOpenInAppOrBrowserPill()
108         runAnimations {
109             appInfoPill.post {
110                 appInfoPill.requireViewById<View>(R.id.collapse_menu_button).sendAccessibilityEvent(
111                     AccessibilityEvent.TYPE_VIEW_FOCUSED)
112             }
113         }
114     }
115 
116     /**
117      * Animates the closing of the handle menu. The windowing and more actions pill vanish. Then,
118      * the app info pill will collapse into the shape of the caption handle in full screen and split
119      * screen.
120      *
121      * @param after runs after the animation finishes.
122      */
animateCollapseIntoHandleClosenull123     fun animateCollapseIntoHandleClose(after: () -> Unit) {
124         appInfoCollapseToHandle()
125         animateAppInfoPillFadeOut()
126         windowingPillClose()
127         moreActionsPillClose()
128         openInAppOrBrowserPillClose()
129         runAnimations(after)
130     }
131 
132     /**
133      * Animates the closing of the handle menu. The windowing and more actions pill vanish. Then,
134      * the app info pill will collapse into the shape of the caption handle in full screen and split
135      * screen.
136      *
137      * @param after runs after animation finishes.
138      *
139      */
animateClosenull140     fun animateClose(after: () -> Unit) {
141         appInfoPillCollapse()
142         animateAppInfoPillFadeOut()
143         windowingPillClose()
144         moreActionsPillClose()
145         openInAppOrBrowserPillClose()
146         runAnimations(after)
147     }
148 
149     /**
150      * Prepares the handle menu for animation. Presets the opacity of necessary menu components.
151      * Presets pivots of handle menu and body pills for scaling animation.
152      */
prepareMenuForAnimationnull153     private fun prepareMenuForAnimation() {
154         // Preset opacity
155         appInfoPill.children.forEach { it.alpha = 0f }
156         windowingPill.alpha = 0f
157         moreActionsPill.alpha = 0f
158         openInAppOrBrowserPill.alpha = 0f
159 
160         // Setup pivots.
161         handleMenu.pivotX = menuWidth / 2f
162         handleMenu.pivotY = 0f
163 
164         windowingPill.pivotX = menuWidth / 2f
165         windowingPill.pivotY = appInfoPill.measuredHeight.toFloat()
166 
167         moreActionsPill.pivotX = menuWidth / 2f
168         moreActionsPill.pivotY = appInfoPill.measuredHeight.toFloat()
169 
170         openInAppOrBrowserPill.pivotX = menuWidth / 2f
171         openInAppOrBrowserPill.pivotY = appInfoPill.measuredHeight.toFloat()
172     }
173 
animateAppInfoPillOpennull174     private fun animateAppInfoPillOpen() {
175         // Header Elevation Animation
176         animators +=
177             ObjectAnimator.ofFloat(appInfoPill, TRANSLATION_Z, 1f).apply {
178                 startDelay = ELEVATION_OPEN_DELAY
179                 duration = HEADER_ELEVATION_OPEN_DURATION
180             }
181 
182         // Content Opacity Animation
183         appInfoPill.children.forEach {
184             animators +=
185                 ObjectAnimator.ofFloat(it, ALPHA, 1f).apply {
186                     startDelay = HEADER_CONTENT_ALPHA_OPEN_DELAY
187                     duration = HEADER_CONTENT_ALPHA_OPEN_DURATION
188                 }
189         }
190     }
191 
captionHandleExpandIntoAppInfoPillnull192     private fun captionHandleExpandIntoAppInfoPill() {
193         // Header scaling animation
194         animators +=
195             ObjectAnimator.ofFloat(appInfoPill, SCALE_X, NONFREEFORM_HEADER_INITIAL_SCALE_X, 1f)
196                 .apply { duration = HEADER_NONFREEFORM_SCALE_OPEN_DURATION }
197 
198         animators +=
199             ObjectAnimator.ofFloat(appInfoPill, SCALE_Y, NONFREEFORM_HEADER_INITIAL_SCALE_Y, 1f)
200                 .apply { duration = HEADER_NONFREEFORM_SCALE_OPEN_DURATION }
201 
202         // Downward y-translation animation
203         val yStart: Float = -captionHeight / 2
204         animators +=
205             ObjectAnimator.ofFloat(handleMenu, TRANSLATION_Y, yStart, 0f).apply {
206                 duration = MENU_Y_TRANSLATION_OPEN_DURATION
207             }
208     }
209 
appInfoPillExpandnull210     private fun appInfoPillExpand() {
211         // Header scaling animation
212         animators +=
213             ObjectAnimator.ofFloat(appInfoPill, SCALE_X, HALF_INITIAL_SCALE, 1f).apply {
214                 duration = HEADER_FREEFORM_SCALE_OPEN_DURATION
215             }
216 
217         animators +=
218             ObjectAnimator.ofFloat(appInfoPill, SCALE_Y, HALF_INITIAL_SCALE, 1f).apply {
219                 duration = HEADER_FREEFORM_SCALE_OPEN_DURATION
220             }
221     }
222 
animateWindowingPillOpennull223     private fun animateWindowingPillOpen() {
224         // Windowing X & Y Scaling Animation
225         animators +=
226             ObjectAnimator.ofFloat(windowingPill, SCALE_X, HALF_INITIAL_SCALE, 1f).apply {
227                 startDelay = BODY_SCALE_OPEN_DELAY
228                 duration = BODY_SCALE_OPEN_DURATION
229             }
230 
231         animators +=
232             ObjectAnimator.ofFloat(windowingPill, SCALE_Y, HALF_INITIAL_SCALE, 1f).apply {
233                 startDelay = BODY_SCALE_OPEN_DELAY
234                 duration = BODY_SCALE_OPEN_DURATION
235             }
236 
237         // Windowing Opacity Animation
238         animators +=
239             ObjectAnimator.ofFloat(windowingPill, ALPHA, 1f).apply {
240                 startDelay = BODY_ALPHA_OPEN_DELAY
241                 duration = BODY_ALPHA_OPEN_DURATION
242             }
243 
244         // Windowing Elevation Animation
245         animators +=
246             ObjectAnimator.ofFloat(windowingPill, TRANSLATION_Z, 1f).apply {
247                 startDelay = ELEVATION_OPEN_DELAY
248                 duration = BODY_ELEVATION_OPEN_DURATION
249             }
250 
251         // Windowing Content Opacity Animation
252         windowingPill.children.forEach {
253             animators +=
254                 ObjectAnimator.ofFloat(it, ALPHA, 1f).apply {
255                     startDelay = BODY_ALPHA_OPEN_DELAY
256                     duration = BODY_CONTENT_ALPHA_OPEN_DURATION
257                     interpolator = Interpolators.FAST_OUT_SLOW_IN
258                 }
259         }
260     }
261 
animateMoreActionsPillOpennull262     private fun animateMoreActionsPillOpen() {
263         // More Actions X & Y Scaling Animation
264         animators +=
265             ObjectAnimator.ofFloat(moreActionsPill, SCALE_X, HALF_INITIAL_SCALE, 1f).apply {
266                 startDelay = BODY_SCALE_OPEN_DELAY
267                 duration = BODY_SCALE_OPEN_DURATION
268             }
269 
270         animators +=
271             ObjectAnimator.ofFloat(moreActionsPill, SCALE_Y, HALF_INITIAL_SCALE, 1f).apply {
272                 startDelay = BODY_SCALE_OPEN_DELAY
273                 duration = BODY_SCALE_OPEN_DURATION
274             }
275 
276         // More Actions Opacity Animation
277         animators +=
278             ObjectAnimator.ofFloat(moreActionsPill, ALPHA, 1f).apply {
279                 startDelay = BODY_ALPHA_OPEN_DELAY
280                 duration = BODY_ALPHA_OPEN_DURATION
281             }
282 
283         // More Actions Elevation Animation
284         animators +=
285             ObjectAnimator.ofFloat(moreActionsPill, TRANSLATION_Z, 1f).apply {
286                 startDelay = ELEVATION_OPEN_DELAY
287                 duration = BODY_ELEVATION_OPEN_DURATION
288             }
289 
290         // More Actions Content Opacity Animation
291         moreActionsPill.children.forEach {
292             animators +=
293                     ObjectAnimator.ofFloat(it, ALPHA, 1f).apply {
294                         startDelay = BODY_ALPHA_OPEN_DELAY
295                         duration = BODY_CONTENT_ALPHA_OPEN_DURATION
296                         interpolator = Interpolators.FAST_OUT_SLOW_IN
297                     }
298         }
299     }
300 
animateOpenInAppOrBrowserPillnull301     private fun animateOpenInAppOrBrowserPill() {
302         // Open in Browser X & Y Scaling Animation
303         animators +=
304                 ObjectAnimator.ofFloat(openInAppOrBrowserPill, SCALE_X, HALF_INITIAL_SCALE, 1f).apply {
305                     startDelay = BODY_SCALE_OPEN_DELAY
306                     duration = BODY_SCALE_OPEN_DURATION
307                 }
308 
309         animators +=
310                 ObjectAnimator.ofFloat(openInAppOrBrowserPill, SCALE_Y, HALF_INITIAL_SCALE, 1f).apply {
311                     startDelay = BODY_SCALE_OPEN_DELAY
312                     duration = BODY_SCALE_OPEN_DURATION
313                 }
314 
315         // Open in Browser Opacity Animation
316         animators +=
317                 ObjectAnimator.ofFloat(openInAppOrBrowserPill, ALPHA, 1f).apply {
318                     startDelay = BODY_ALPHA_OPEN_DELAY
319                     duration = BODY_ALPHA_OPEN_DURATION
320                 }
321 
322         // Open in Browser Elevation Animation
323         animators +=
324                 ObjectAnimator.ofFloat(openInAppOrBrowserPill, TRANSLATION_Z, 1f).apply {
325                     startDelay = ELEVATION_OPEN_DELAY
326                     duration = BODY_ELEVATION_OPEN_DURATION
327                 }
328 
329         // Open in Browser Button Opacity Animation
330         val button = openInAppOrBrowserPill.requireViewById<View>(R.id.open_in_app_or_browser_button)
331         animators +=
332                 ObjectAnimator.ofFloat(button, ALPHA, 1f).apply {
333                     startDelay = BODY_ALPHA_OPEN_DELAY
334                     duration = BODY_CONTENT_ALPHA_OPEN_DURATION
335                     interpolator = Interpolators.FAST_OUT_SLOW_IN
336                 }
337     }
338 
appInfoPillCollapsenull339     private fun appInfoPillCollapse() {
340         // Header scaling animation
341         animators +=
342             ObjectAnimator.ofFloat(appInfoPill, SCALE_X, 0f).apply {
343                 startDelay = HEADER_CLOSE_DELAY
344                 duration = HEADER_CLOSE_DURATION
345             }
346 
347         animators +=
348             ObjectAnimator.ofFloat(appInfoPill, SCALE_Y, 0f).apply {
349                 startDelay = HEADER_CLOSE_DELAY
350                 duration = HEADER_CLOSE_DURATION
351             }
352     }
353 
appInfoCollapseToHandlenull354     private fun appInfoCollapseToHandle() {
355         // Header X & Y Scaling Animation
356         animators +=
357             ObjectAnimator.ofFloat(appInfoPill, SCALE_X, NONFREEFORM_HEADER_INITIAL_SCALE_X).apply {
358                 startDelay = HEADER_CLOSE_DELAY
359                 duration = HEADER_CLOSE_DURATION
360             }
361 
362         animators +=
363             ObjectAnimator.ofFloat(appInfoPill, SCALE_Y, NONFREEFORM_HEADER_INITIAL_SCALE_Y).apply {
364                 startDelay = HEADER_CLOSE_DELAY
365                 duration = HEADER_CLOSE_DURATION
366             }
367         // Upward y-translation animation
368         val yStart: Float = -captionHeight / 2
369         animators +=
370             ObjectAnimator.ofFloat(appInfoPill, TRANSLATION_Y, yStart).apply {
371                 startDelay = HEADER_CLOSE_DELAY
372                 duration = HEADER_CLOSE_DURATION
373             }
374     }
375 
animateAppInfoPillFadeOutnull376     private fun animateAppInfoPillFadeOut() {
377         // Header Content Opacity Animation
378         appInfoPill.children.forEach {
379             animators +=
380                 ObjectAnimator.ofFloat(it, ALPHA, 0f).apply {
381                     startDelay = HEADER_CONTENT_OPACITY_CLOSE_DELAY
382                     duration = HEADER_CONTENT_OPACITY_CLOSE_DURATION
383                 }
384         }
385     }
386 
windowingPillClosenull387     private fun windowingPillClose() {
388         // Windowing X & Y Scaling Animation
389         animators +=
390             ObjectAnimator.ofFloat(windowingPill, SCALE_X, HALF_INITIAL_SCALE).apply {
391                 duration = BODY_CLOSE_DURATION
392             }
393 
394         animators +=
395             ObjectAnimator.ofFloat(windowingPill, SCALE_Y, HALF_INITIAL_SCALE).apply {
396                 duration = BODY_CLOSE_DURATION
397             }
398 
399         // windowing Animation
400         animators +=
401             ObjectAnimator.ofFloat(windowingPill, ALPHA, 0f).apply {
402                 duration = BODY_CLOSE_DURATION
403             }
404 
405         animators +=
406             ObjectAnimator.ofFloat(windowingPill, ALPHA, 0f).apply {
407                 duration = BODY_CLOSE_DURATION
408             }
409     }
410 
moreActionsPillClosenull411     private fun moreActionsPillClose() {
412         // More Actions X & Y Scaling Animation
413         animators +=
414             ObjectAnimator.ofFloat(moreActionsPill, SCALE_X, HALF_INITIAL_SCALE).apply {
415                 duration = BODY_CLOSE_DURATION
416             }
417 
418         animators +=
419             ObjectAnimator.ofFloat(moreActionsPill, SCALE_Y, HALF_INITIAL_SCALE).apply {
420                 duration = BODY_CLOSE_DURATION
421             }
422 
423         // More Actions Opacity Animation
424         animators +=
425             ObjectAnimator.ofFloat(moreActionsPill, ALPHA, 0f).apply {
426                 duration = BODY_CLOSE_DURATION
427             }
428 
429         animators +=
430             ObjectAnimator.ofFloat(moreActionsPill, ALPHA, 0f).apply {
431                 duration = BODY_CLOSE_DURATION
432             }
433 
434         // upward more actions pill y-translation animation
435         val yStart: Float = -captionHeight / 2
436         animators +=
437             ObjectAnimator.ofFloat(moreActionsPill, TRANSLATION_Y, yStart).apply {
438                 duration = BODY_CLOSE_DURATION
439             }
440     }
441 
openInAppOrBrowserPillClosenull442     private fun openInAppOrBrowserPillClose() {
443         // Open in Browser X & Y Scaling Animation
444         animators +=
445                 ObjectAnimator.ofFloat(openInAppOrBrowserPill, SCALE_X, HALF_INITIAL_SCALE).apply {
446                     duration = BODY_CLOSE_DURATION
447                 }
448 
449         animators +=
450                 ObjectAnimator.ofFloat(openInAppOrBrowserPill, SCALE_Y, HALF_INITIAL_SCALE).apply {
451                     duration = BODY_CLOSE_DURATION
452                 }
453 
454         // Open in Browser Opacity Animation
455         animators +=
456                 ObjectAnimator.ofFloat(openInAppOrBrowserPill, ALPHA, 0f).apply {
457                     duration = BODY_CLOSE_DURATION
458                 }
459 
460         animators +=
461                 ObjectAnimator.ofFloat(openInAppOrBrowserPill, ALPHA, 0f).apply {
462                     duration = BODY_CLOSE_DURATION
463                 }
464 
465         // Upward Open in Browser y-translation Animation
466         val yStart: Float = -captionHeight / 2
467         animators +=
468                 ObjectAnimator.ofFloat(openInAppOrBrowserPill, TRANSLATION_Y, yStart).apply {
469                     duration = BODY_CLOSE_DURATION
470                 }
471     }
472 
473     /**
474      * Runs the list of hide animators concurrently.
475      *
476      * @param after runs after animation finishes.
477      */
runAnimationsnull478     private fun runAnimations(after: (() -> Unit)? = null) {
479         runningAnimation?.apply {
480             // Remove all listeners, so that the after function isn't triggered upon cancel.
481             removeAllListeners()
482             // If an animation runs while running animation is triggered, gracefully cancel.
483             cancel()
484         }
485 
486         runningAnimation = AnimatorSet().apply {
487             playTogether(animators)
488             animators.clear()
489             doOnEnd {
490                 after?.invoke()
491                 runningAnimation = null
492             }
493             start()
494         }
495     }
496 }
497