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.systemui.util 18 19 import android.app.Dialog 20 import android.view.View 21 import android.view.ViewGroup 22 import android.view.ViewGroup.LayoutParams.MATCH_PARENT 23 import android.widget.FrameLayout 24 import android.window.OnBackInvokedDispatcher 25 import com.android.systemui.animation.back.BackAnimationSpec 26 import com.android.systemui.animation.back.BackTransformation 27 import com.android.systemui.animation.back.applyTo 28 import com.android.systemui.animation.back.floatingSystemSurfacesForSysUi 29 import com.android.systemui.animation.back.onBackAnimationCallbackFrom 30 import com.android.systemui.animation.back.registerOnBackInvokedCallbackOnViewAttached 31 import com.android.systemui.animation.view.LaunchableFrameLayout 32 33 /** 34 * Register on the Dialog's [OnBackInvokedDispatcher] an animation using the [BackAnimationSpec]. 35 * The [BackTransformation] will be applied on the [targetView]. 36 */ 37 @JvmOverloads 38 fun Dialog.registerAnimationOnBackInvoked( 39 targetView: View, 40 backAnimationSpec: BackAnimationSpec = 41 BackAnimationSpec.floatingSystemSurfacesForSysUi( 42 displayMetrics = targetView.resources.displayMetrics, 43 ), 44 ) { 45 targetView.registerOnBackInvokedCallbackOnViewAttached( 46 onBackInvokedDispatcher = onBackInvokedDispatcher, 47 onBackAnimationCallback = 48 onBackAnimationCallbackFrom( 49 backAnimationSpec = backAnimationSpec, 50 displayMetrics = targetView.resources.displayMetrics, 51 onBackProgressed = { backTransformation -> backTransformation.applyTo(targetView) }, 52 onBackInvoked = { dismiss() }, 53 ), 54 ) 55 } 56 57 /** 58 * Make the dialog window (and therefore its DecorView) fullscreen to make it possible to animate 59 * outside its bounds. No-op if the dialog is already fullscreen. 60 * 61 * <p>Returns null if the dialog is already fullscreen. Otherwise, returns a pair containing a view 62 * and a layout listener. The new view matches the original dialog DecorView in size, position, and 63 * background. This new view will be a child of the modified, transparent, fullscreen DecorView. The 64 * layout listener is listening to changes to the modified DecorView. It is the responsibility of 65 * the caller to deregister the listener when the dialog is dismissed. 66 */ 67 fun Dialog.maybeForceFullscreen(): Pair<LaunchableFrameLayout, View.OnLayoutChangeListener>? { 68 // Create the dialog so that its onCreate() method is called, which usually sets the dialog 69 // content. 70 create() 71 72 val window = window!! 73 val decorView = window.decorView as ViewGroup 74 75 val isWindowFullscreen = 76 window.attributes.width == MATCH_PARENT && window.attributes.height == MATCH_PARENT 77 if (isWindowFullscreen) { 78 return null 79 } 80 81 // We will make the dialog window (and therefore its DecorView) fullscreen to make it possible 82 // to animate outside its bounds. 83 // 84 // Before that, we add a new View as a child of the DecorView with the same size and gravity as 85 // that DecorView, then we add all original children of the DecorView to that new View. Finally 86 // we remove the background of the DecorView and add it to the new View, then we make the 87 // DecorView fullscreen. This new View now acts as a fake (non fullscreen) window. 88 // 89 // On top of that, we also add a fullscreen transparent background between the DecorView and the 90 // view that we added so that we can dismiss the dialog when this view is clicked. This is 91 // necessary because DecorView overrides onTouchEvent and therefore we can't set the click 92 // listener directly on the (now fullscreen) DecorView. 93 val fullscreenTransparentBackground = FrameLayout(context) 94 decorView.addView( 95 fullscreenTransparentBackground, 96 0 /* index */, 97 FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) 98 ) 99 100 val dialogContentWithBackground = LaunchableFrameLayout(context) 101 dialogContentWithBackground.background = decorView.background 102 103 // Make the window background transparent. Note that setting the window (or DecorView) 104 // background drawable to null leads to issues with background color (not being transparent) or 105 // with insets that are not refreshed. Therefore we need to set it to something not null, hence 106 // we are using android.R.color.transparent here. 107 window.setBackgroundDrawableResource(android.R.color.transparent) 108 109 // Close the dialog when clicking outside of it. 110 fullscreenTransparentBackground.setOnClickListener { dismiss() } 111 dialogContentWithBackground.isClickable = true 112 113 // Make sure the transparent and dialog backgrounds are not focusable by accessibility 114 // features. 115 fullscreenTransparentBackground.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO 116 dialogContentWithBackground.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO 117 118 fullscreenTransparentBackground.addView( 119 dialogContentWithBackground, 120 FrameLayout.LayoutParams( 121 window.attributes.width, 122 window.attributes.height, 123 window.attributes.gravity 124 ) 125 ) 126 127 // Move all original children of the DecorView to the new View we just added. 128 for (i in 1 until decorView.childCount) { 129 val view = decorView.getChildAt(1) 130 decorView.removeViewAt(1) 131 dialogContentWithBackground.addView(view) 132 } 133 134 // Make the window fullscreen and add a layout listener to ensure it stays fullscreen. 135 window.setLayout(MATCH_PARENT, MATCH_PARENT) 136 val decorViewLayoutListener = 137 View.OnLayoutChangeListener { 138 v, 139 left, 140 top, 141 right, 142 bottom, 143 oldLeft, 144 oldTop, 145 oldRight, 146 oldBottom -> 147 if ( 148 window.attributes.width != MATCH_PARENT || window.attributes.height != MATCH_PARENT 149 ) { 150 // The dialog size changed, copy its size to dialogContentWithBackground and make 151 // the dialog window full screen again. 152 val layoutParams = dialogContentWithBackground.layoutParams 153 layoutParams.width = window.attributes.width 154 layoutParams.height = window.attributes.height 155 dialogContentWithBackground.layoutParams = layoutParams 156 window.setLayout(MATCH_PARENT, MATCH_PARENT) 157 } 158 } 159 decorView.addOnLayoutChangeListener(decorViewLayoutListener) 160 161 return dialogContentWithBackground to decorViewLayoutListener 162 } 163