1 /*
2  * Copyright (C) 2022 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.animation
18 
19 import android.view.GhostView
20 import android.view.View
21 import android.view.ViewGroup
22 import android.view.ViewRootImpl
23 import com.android.internal.jank.InteractionJankMonitor
24 
25 /** A [DialogLaunchAnimator.Controller] that can animate a [View] from/to a dialog. */
26 class ViewDialogLaunchAnimatorController
27 internal constructor(
28     private val source: View,
29     override val cuj: DialogCuj?,
30 ) : DialogLaunchAnimator.Controller {
31     override val viewRoot: ViewRootImpl?
32         get() = source.viewRootImpl
33 
34     override val sourceIdentity: Any = source
35 
36     override fun startDrawingInOverlayOf(viewGroup: ViewGroup) {
37         // Delay the calls to `source.setVisibility()` during the animation. This must be called
38         // before `GhostView.addGhost()` is called because the latter will change the *transition*
39         // visibility, which won't be blocked and will affect the normal View visibility that is
40         // saved by `setShouldBlockVisibilityChanges()` for a later restoration.
41         (source as? LaunchableView)?.setShouldBlockVisibilityChanges(true)
42 
43         // Create a temporary ghost of the source (which will make it invisible) and add it
44         // to the host dialog.
45         GhostView.addGhost(source, viewGroup)
46     }
47 
48     override fun stopDrawingInOverlay() {
49         // Note: here we should remove the ghost from the overlay, but in practice this is
50         // already done by the launch controller created below.
51 
52         if (source is LaunchableView) {
53             // Make sure we allow the source to change its visibility again and restore its previous
54             // value.
55             source.setShouldBlockVisibilityChanges(false)
56         } else {
57             // We made the source invisible earlier, so let's make it visible again.
58             source.visibility = View.VISIBLE
59         }
60     }
61 
62     override fun createLaunchController(): LaunchAnimator.Controller {
63         val delegate = GhostedViewLaunchAnimatorController(source)
64         return object : LaunchAnimator.Controller by delegate {
65             override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
66                 // Remove the temporary ghost added by [startDrawingInOverlayOf]. Another
67                 // ghost (that ghosts only the source content, and not its background) will
68                 // be added right after this by the delegate and will be animated.
69                 GhostView.removeGhost(source)
70                 delegate.onLaunchAnimationStart(isExpandingFullyAbove)
71             }
72 
73             override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
74                 delegate.onLaunchAnimationEnd(isExpandingFullyAbove)
75 
76                 // At this point the view visibility is restored by the delegate, so we delay the
77                 // visibility changes again and make it invisible while the dialog is shown.
78                 if (source is LaunchableView) {
79                     source.setShouldBlockVisibilityChanges(true)
80                     source.setTransitionVisibility(View.INVISIBLE)
81                 } else {
82                     source.visibility = View.INVISIBLE
83                 }
84             }
85         }
86     }
87 
88     override fun createExitController(): LaunchAnimator.Controller {
89         return GhostedViewLaunchAnimatorController(source)
90     }
91 
92     override fun shouldAnimateExit(): Boolean {
93         // The source should be invisible by now, if it's not then something else changed
94         // its visibility and we probably don't want to run the animation.
95         if (source.visibility != View.INVISIBLE) {
96             return false
97         }
98 
99         return source.isAttachedToWindow && ((source.parent as? View)?.isShown ?: true)
100     }
101 
102     override fun onExitAnimationCancelled() {
103         if (source is LaunchableView) {
104             // Make sure we allow the source to change its visibility again.
105             source.setShouldBlockVisibilityChanges(false)
106         } else {
107             // If the view is invisible it's probably because of us, so we make it visible
108             // again.
109             if (source.visibility == View.INVISIBLE) {
110                 source.visibility = View.VISIBLE
111             }
112         }
113     }
114 
115     override fun jankConfigurationBuilder(): InteractionJankMonitor.Configuration.Builder? {
116         val type = cuj?.cujType ?: return null
117         return InteractionJankMonitor.Configuration.Builder.withView(type, source)
118     }
119 }
120