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