/*
* 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
}