/* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.util import android.app.Dialog import android.view.View import android.view.ViewGroup import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.widget.FrameLayout import android.window.OnBackInvokedDispatcher import com.android.systemui.animation.back.BackAnimationSpec import com.android.systemui.animation.back.BackTransformation import com.android.systemui.animation.back.applyTo import com.android.systemui.animation.back.floatingSystemSurfacesForSysUi import com.android.systemui.animation.back.onBackAnimationCallbackFrom import com.android.systemui.animation.back.registerOnBackInvokedCallbackOnViewAttached import com.android.systemui.animation.view.LaunchableFrameLayout /** * Register on the Dialog's [OnBackInvokedDispatcher] an animation using the [BackAnimationSpec]. * The [BackTransformation] will be applied on the [targetView]. */ @JvmOverloads fun Dialog.registerAnimationOnBackInvoked( targetView: View, backAnimationSpec: BackAnimationSpec = BackAnimationSpec.floatingSystemSurfacesForSysUi( displayMetrics = targetView.resources.displayMetrics, ), ) { targetView.registerOnBackInvokedCallbackOnViewAttached( onBackInvokedDispatcher = onBackInvokedDispatcher, onBackAnimationCallback = onBackAnimationCallbackFrom( backAnimationSpec = backAnimationSpec, displayMetrics = targetView.resources.displayMetrics, onBackProgressed = { backTransformation -> backTransformation.applyTo(targetView) }, onBackInvoked = { dismiss() }, ), ) } /** * Make the dialog window (and therefore its DecorView) fullscreen to make it possible to animate * outside its bounds. No-op if the dialog is already fullscreen. * *

Returns null if the dialog is already fullscreen. Otherwise, returns a pair containing a view * and a layout listener. The new view matches the original dialog DecorView in size, position, and * background. This new view will be a child of the modified, transparent, fullscreen DecorView. The * layout listener is listening to changes to the modified DecorView. It is the responsibility of * the caller to deregister the listener when the dialog is dismissed. */ fun Dialog.maybeForceFullscreen(): Pair? { // Create the dialog so that its onCreate() method is called, which usually sets the dialog // content. create() val window = window!! val decorView = window.decorView as ViewGroup val isWindowFullscreen = window.attributes.width == MATCH_PARENT && window.attributes.height == MATCH_PARENT if (isWindowFullscreen) { return null } // We will make the dialog window (and therefore its DecorView) fullscreen to make it possible // to animate outside its bounds. // // Before that, we add a new View as a child of the DecorView with the same size and gravity as // that DecorView, then we add all original children of the DecorView to that new View. Finally // we remove the background of the DecorView and add it to the new View, then we make the // DecorView fullscreen. This new View now acts as a fake (non fullscreen) window. // // On top of that, we also add a fullscreen transparent background between the DecorView and the // view that we added so that we can dismiss the dialog when this view is clicked. This is // necessary because DecorView overrides onTouchEvent and therefore we can't set the click // listener directly on the (now fullscreen) DecorView. val fullscreenTransparentBackground = FrameLayout(context) decorView.addView( fullscreenTransparentBackground, 0 /* index */, FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) ) val dialogContentWithBackground = LaunchableFrameLayout(context) dialogContentWithBackground.background = decorView.background // Make the window background transparent. Note that setting the window (or DecorView) // background drawable to null leads to issues with background color (not being transparent) or // with insets that are not refreshed. Therefore we need to set it to something not null, hence // we are using android.R.color.transparent here. window.setBackgroundDrawableResource(android.R.color.transparent) // Close the dialog when clicking outside of it. fullscreenTransparentBackground.setOnClickListener { dismiss() } dialogContentWithBackground.isClickable = true // Make sure the transparent and dialog backgrounds are not focusable by accessibility // features. fullscreenTransparentBackground.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO dialogContentWithBackground.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO fullscreenTransparentBackground.addView( dialogContentWithBackground, FrameLayout.LayoutParams( window.attributes.width, window.attributes.height, window.attributes.gravity ) ) // Move all original children of the DecorView to the new View we just added. for (i in 1 until decorView.childCount) { val view = decorView.getChildAt(1) decorView.removeViewAt(1) dialogContentWithBackground.addView(view) } // Make the window fullscreen and add a layout listener to ensure it stays fullscreen. window.setLayout(MATCH_PARENT, MATCH_PARENT) val decorViewLayoutListener = View.OnLayoutChangeListener { v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom -> if ( window.attributes.width != MATCH_PARENT || window.attributes.height != MATCH_PARENT ) { // The dialog size changed, copy its size to dialogContentWithBackground and make // the dialog window full screen again. val layoutParams = dialogContentWithBackground.layoutParams layoutParams.width = window.attributes.width layoutParams.height = window.attributes.height dialogContentWithBackground.layoutParams = layoutParams window.setLayout(MATCH_PARENT, MATCH_PARENT) } } decorView.addOnLayoutChangeListener(decorViewLayoutListener) return dialogContentWithBackground to decorViewLayoutListener }