mirror of https://github.com/tasks/tasks
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
664 lines
25 KiB
Kotlin
664 lines
25 KiB
Kotlin
/*
|
|
* Copyright 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 org.tasks.compose.drawer
|
|
|
|
import android.content.Context
|
|
import android.graphics.PixelFormat
|
|
import android.os.Build
|
|
import android.view.Gravity
|
|
import android.view.KeyEvent
|
|
import android.view.View
|
|
import android.view.ViewTreeObserver
|
|
import android.view.WindowManager
|
|
import android.window.OnBackInvokedCallback
|
|
import android.window.OnBackInvokedDispatcher
|
|
import androidx.annotation.DoNotInline
|
|
import androidx.annotation.RequiresApi
|
|
import androidx.compose.animation.core.TweenSpec
|
|
import androidx.compose.animation.core.animateFloatAsState
|
|
import androidx.compose.foundation.Canvas
|
|
import androidx.compose.foundation.gestures.Orientation
|
|
import androidx.compose.foundation.gestures.detectTapGestures
|
|
import androidx.compose.foundation.gestures.draggable
|
|
import androidx.compose.foundation.layout.Box
|
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
|
import androidx.compose.foundation.layout.Column
|
|
import androidx.compose.foundation.layout.ColumnScope
|
|
import androidx.compose.foundation.layout.PaddingValues
|
|
import androidx.compose.foundation.layout.WindowInsets
|
|
import androidx.compose.foundation.layout.fillMaxSize
|
|
import androidx.compose.foundation.layout.fillMaxWidth
|
|
import androidx.compose.foundation.layout.imePadding
|
|
import androidx.compose.foundation.layout.offset
|
|
import androidx.compose.foundation.layout.widthIn
|
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
import androidx.compose.material3.Surface
|
|
import androidx.compose.material3.contentColorFor
|
|
import androidx.compose.runtime.Composable
|
|
import androidx.compose.runtime.CompositionContext
|
|
import androidx.compose.runtime.DisposableEffect
|
|
import androidx.compose.runtime.Immutable
|
|
import androidx.compose.runtime.LaunchedEffect
|
|
import androidx.compose.runtime.SideEffect
|
|
import androidx.compose.runtime.getValue
|
|
import androidx.compose.runtime.mutableStateOf
|
|
import androidx.compose.runtime.remember
|
|
import androidx.compose.runtime.rememberCompositionContext
|
|
import androidx.compose.runtime.rememberCoroutineScope
|
|
import androidx.compose.runtime.rememberUpdatedState
|
|
import androidx.compose.runtime.saveable.rememberSaveable
|
|
import androidx.compose.runtime.setValue
|
|
import androidx.compose.ui.Alignment
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.graphics.Color
|
|
import androidx.compose.ui.graphics.Shape
|
|
import androidx.compose.ui.graphics.isSpecified
|
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|
import androidx.compose.ui.input.pointer.pointerInput
|
|
import androidx.compose.ui.layout.onSizeChanged
|
|
import androidx.compose.ui.platform.AbstractComposeView
|
|
import androidx.compose.ui.platform.LocalDensity
|
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
|
import androidx.compose.ui.platform.LocalView
|
|
import androidx.compose.ui.platform.ViewRootForInspector
|
|
import androidx.compose.ui.semantics.clearAndSetSemantics
|
|
import androidx.compose.ui.semantics.popup
|
|
import androidx.compose.ui.semantics.semantics
|
|
import androidx.compose.ui.unit.Dp
|
|
import androidx.compose.ui.unit.IntOffset
|
|
import androidx.compose.ui.unit.LayoutDirection
|
|
import androidx.compose.ui.window.SecureFlagPolicy
|
|
import androidx.lifecycle.findViewTreeLifecycleOwner
|
|
import androidx.lifecycle.findViewTreeViewModelStoreOwner
|
|
import androidx.lifecycle.setViewTreeLifecycleOwner
|
|
import androidx.lifecycle.setViewTreeViewModelStoreOwner
|
|
import androidx.savedstate.findViewTreeSavedStateRegistryOwner
|
|
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
|
|
import kotlinx.coroutines.launch
|
|
import org.tasks.compose.drawer.SheetValue.Expanded
|
|
import org.tasks.compose.drawer.SheetValue.Hidden
|
|
import org.tasks.compose.drawer.SheetValue.PartiallyExpanded
|
|
import java.util.UUID
|
|
import kotlin.math.max
|
|
|
|
/**
|
|
* <a href="https://m3.material.io/components/bottom-sheets/overview" class="external" target="_blank">Material Design modal bottom sheet</a>.
|
|
*
|
|
* Modal bottom sheets are used as an alternative to inline menus or simple dialogs on mobile,
|
|
* especially when offering a long list of action items, or when items require longer descriptions
|
|
* and icons. Like dialogs, modal bottom sheets appear in front of app content, disabling all other
|
|
* app functionality when they appear, and remaining on screen until confirmed, dismissed, or a
|
|
* required action has been taken.
|
|
*
|
|
* ![Bottom sheet image](https://developer.android.com/images/reference/androidx/compose/material3/bottom_sheet.png)
|
|
*
|
|
* A simple example of a modal bottom sheet looks like this:
|
|
*
|
|
* @sample androidx.compose.material3.samples.ModalBottomSheetSample
|
|
*
|
|
* @param onDismissRequest Executes when the user clicks outside of the bottom sheet, after sheet
|
|
* animates to [Hidden].
|
|
* @param modifier Optional [Modifier] for the bottom sheet.
|
|
* @param sheetState The state of the bottom sheet.
|
|
* @param sheetMaxWidth [Dp] that defines what the maximum width the sheet will take.
|
|
* Pass in [Dp.Unspecified] for a sheet that spans the entire screen width.
|
|
* @param shape The shape of the bottom sheet.
|
|
* @param containerColor The color used for the background of this bottom sheet
|
|
* @param contentColor The preferred color for content inside this bottom sheet. Defaults to either
|
|
* the matching content color for [containerColor], or to the current [LocalContentColor] if
|
|
* [containerColor] is not a color from the theme.
|
|
* @param tonalElevation The tonal elevation of this bottom sheet.
|
|
* @param scrimColor Color of the scrim that obscures content when the bottom sheet is open.
|
|
* @param dragHandle Optional visual marker to swipe the bottom sheet.
|
|
* @param windowInsets window insets to be passed to the bottom sheet window via [PaddingValues]
|
|
* params.
|
|
* @param properties [ModalBottomSheetProperties] for further customization of this
|
|
* modal bottom sheet's behavior.
|
|
* @param content The content to be displayed inside the bottom sheet.
|
|
*/
|
|
@Composable
|
|
@ExperimentalMaterial3Api
|
|
fun ModalBottomSheet(
|
|
onDismissRequest: () -> Unit,
|
|
modifier: Modifier = Modifier,
|
|
sheetState: SheetState = rememberModalBottomSheetState(),
|
|
sheetMaxWidth: Dp = BottomSheetDefaults.SheetMaxWidth,
|
|
shape: Shape = BottomSheetDefaults.ExpandedShape,
|
|
containerColor: Color = BottomSheetDefaults.ContainerColor,
|
|
contentColor: Color = contentColorFor(containerColor),
|
|
tonalElevation: Dp = BottomSheetDefaults.Elevation,
|
|
scrimColor: Color = BottomSheetDefaults.ScrimColor,
|
|
dragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() },
|
|
windowInsets: WindowInsets = BottomSheetDefaults.windowInsets,
|
|
properties: ModalBottomSheetProperties = ModalBottomSheetDefaults.properties(),
|
|
content: @Composable ColumnScope.() -> Unit,
|
|
) {
|
|
// b/291735717 Remove this once deprecated methods without density are removed
|
|
val density = LocalDensity.current
|
|
SideEffect {
|
|
sheetState.density = density
|
|
}
|
|
val scope = rememberCoroutineScope()
|
|
val animateToDismiss: () -> Unit = {
|
|
if (sheetState.anchoredDraggableState.confirmValueChange(Hidden)) {
|
|
scope.launch { sheetState.hide() }.invokeOnCompletion {
|
|
if (!sheetState.isVisible) {
|
|
onDismissRequest()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
val settleToDismiss: (velocity: Float) -> Unit = {
|
|
scope.launch { sheetState.settle(it) }.invokeOnCompletion {
|
|
if (!sheetState.isVisible) onDismissRequest()
|
|
}
|
|
}
|
|
|
|
ModalBottomSheetPopup(
|
|
properties = properties,
|
|
onDismissRequest = {
|
|
// if (sheetState.currentValue == Expanded && sheetState.hasPartiallyExpandedState) {
|
|
// scope.launch { sheetState.partialExpand() }
|
|
// } else { // Is expanded without collapsed state or is collapsed.
|
|
scope.launch { sheetState.hide() }.invokeOnCompletion { onDismissRequest() }
|
|
// }
|
|
},
|
|
windowInsets = windowInsets,
|
|
) {
|
|
BoxWithConstraints(Modifier.fillMaxSize()) {
|
|
val fullHeight = constraints.maxHeight
|
|
Scrim(
|
|
color = scrimColor,
|
|
onDismissRequest = animateToDismiss,
|
|
visible = sheetState.targetValue != Hidden
|
|
)
|
|
Surface(
|
|
modifier = modifier
|
|
.widthIn(max = sheetMaxWidth)
|
|
.fillMaxWidth()
|
|
.align(Alignment.TopCenter)
|
|
.offset {
|
|
IntOffset(
|
|
0,
|
|
sheetState
|
|
.requireOffset()
|
|
.toInt()
|
|
)
|
|
}
|
|
.nestedScroll(
|
|
remember(sheetState) {
|
|
ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection(
|
|
sheetState = sheetState,
|
|
orientation = Orientation.Vertical,
|
|
onFling = settleToDismiss
|
|
)
|
|
}
|
|
)
|
|
.draggable(
|
|
state = sheetState.anchoredDraggableState.draggableState,
|
|
orientation = Orientation.Vertical,
|
|
enabled = sheetState.isVisible,
|
|
startDragImmediately = sheetState.anchoredDraggableState.isAnimationRunning,
|
|
onDragStopped = { settleToDismiss(it) }
|
|
)
|
|
.modalBottomSheetAnchors(
|
|
sheetState = sheetState,
|
|
fullHeight = fullHeight.toFloat()
|
|
),
|
|
shape = shape,
|
|
color = containerColor,
|
|
contentColor = contentColor,
|
|
tonalElevation = tonalElevation,
|
|
) {
|
|
Column(Modifier.fillMaxWidth()) {
|
|
if (dragHandle != null) {
|
|
Box(
|
|
Modifier
|
|
.align(Alignment.CenterHorizontally)
|
|
) {
|
|
dragHandle()
|
|
}
|
|
}
|
|
content()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (sheetState.hasExpandedState) {
|
|
LaunchedEffect(sheetState) {
|
|
sheetState.show()
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Properties used to customize the behavior of a [ModalBottomSheet].
|
|
*
|
|
* @param securePolicy Policy for setting [WindowManager.LayoutParams.FLAG_SECURE] on the bottom
|
|
* sheet's window.
|
|
* @param isFocusable Whether the modal bottom sheet is focusable. When true,
|
|
* the modal bottom sheet will receive IME events and key presses, such as when
|
|
* the back button is pressed.
|
|
* @param shouldDismissOnBackPress Whether the modal bottom sheet can be dismissed by pressing
|
|
* the back button. If true, pressing the back button will call onDismissRequest.
|
|
* Note that [isFocusable] must be set to true in order to receive key events such as
|
|
* the back button - if the modal bottom sheet is not focusable then this property does nothing.
|
|
*/
|
|
@ExperimentalMaterial3Api
|
|
class ModalBottomSheetProperties(
|
|
val securePolicy: SecureFlagPolicy,
|
|
val isFocusable: Boolean,
|
|
val shouldDismissOnBackPress: Boolean
|
|
) {
|
|
override fun equals(other: Any?): Boolean {
|
|
if (this === other) return true
|
|
if (other !is ModalBottomSheetProperties) return false
|
|
|
|
if (securePolicy != other.securePolicy) return false
|
|
if (isFocusable != other.isFocusable) return false
|
|
if (shouldDismissOnBackPress != other.shouldDismissOnBackPress) return false
|
|
|
|
return true
|
|
}
|
|
|
|
override fun hashCode(): Int {
|
|
var result = securePolicy.hashCode()
|
|
result = 31 * result + isFocusable.hashCode()
|
|
result = 31 * result + shouldDismissOnBackPress.hashCode()
|
|
return result
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Default values for [ModalBottomSheet]
|
|
*/
|
|
@Immutable
|
|
@ExperimentalMaterial3Api
|
|
object ModalBottomSheetDefaults {
|
|
/**
|
|
* Properties used to customize the behavior of a [ModalBottomSheet].
|
|
*
|
|
* @param securePolicy Policy for setting [WindowManager.LayoutParams.FLAG_SECURE] on the bottom
|
|
* sheet's window.
|
|
* @param isFocusable Whether the modal bottom sheet is focusable. When true,
|
|
* the modal bottom sheet will receive IME events and key presses, such as when
|
|
* the back button is pressed.
|
|
* @param shouldDismissOnBackPress Whether the modal bottom sheet can be dismissed by pressing
|
|
* the back button. If true, pressing the back button will call onDismissRequest.
|
|
* Note that [isFocusable] must be set to true in order to receive key events such as
|
|
* the back button - if the modal bottom sheet is not focusable then this property does nothing.
|
|
*/
|
|
fun properties(
|
|
securePolicy: SecureFlagPolicy = SecureFlagPolicy.Inherit,
|
|
isFocusable: Boolean = true,
|
|
shouldDismissOnBackPress: Boolean = true
|
|
) = ModalBottomSheetProperties(securePolicy, isFocusable, shouldDismissOnBackPress)
|
|
}
|
|
|
|
/**
|
|
* Create and [remember] a [SheetState] for [ModalBottomSheet].
|
|
*
|
|
* @param skipPartiallyExpanded Whether the partially expanded state, if the sheet is tall enough,
|
|
* should be skipped. If true, the sheet will always expand to the [Expanded] state and move to the
|
|
* [Hidden] state when hiding the sheet, either programmatically or by user interaction.
|
|
* @param confirmValueChange Optional callback invoked to confirm or veto a pending state change.
|
|
*/
|
|
@Composable
|
|
@ExperimentalMaterial3Api
|
|
fun rememberModalBottomSheetState(
|
|
skipPartiallyExpanded: Boolean = false,
|
|
confirmValueChange: (SheetValue) -> Boolean = { true },
|
|
) = rememberSheetState(skipPartiallyExpanded, confirmValueChange, Hidden)
|
|
|
|
@Composable
|
|
private fun Scrim(
|
|
color: Color,
|
|
onDismissRequest: () -> Unit,
|
|
visible: Boolean
|
|
) {
|
|
if (color.isSpecified) {
|
|
val alpha by animateFloatAsState(
|
|
targetValue = if (visible) 1f else 0f,
|
|
animationSpec = TweenSpec()
|
|
)
|
|
val dismissSheet = if (visible) {
|
|
Modifier
|
|
.pointerInput(onDismissRequest) {
|
|
detectTapGestures {
|
|
onDismissRequest()
|
|
}
|
|
}
|
|
.clearAndSetSemantics {}
|
|
} else {
|
|
Modifier
|
|
}
|
|
Canvas(
|
|
Modifier
|
|
.fillMaxSize()
|
|
.then(dismissSheet)
|
|
) {
|
|
drawRect(color = color, alpha = alpha)
|
|
}
|
|
}
|
|
}
|
|
|
|
@ExperimentalMaterial3Api
|
|
private fun Modifier.modalBottomSheetAnchors(
|
|
sheetState: SheetState,
|
|
fullHeight: Float
|
|
) = onSizeChanged { sheetSize ->
|
|
|
|
val newAnchors = DraggableAnchors {
|
|
Hidden at fullHeight
|
|
if (sheetSize.height > (fullHeight / 2) && !sheetState.skipPartiallyExpanded) {
|
|
PartiallyExpanded at fullHeight / 2f
|
|
}
|
|
if (sheetSize.height != 0) {
|
|
Expanded at max(0f, fullHeight - sheetSize.height)
|
|
}
|
|
}
|
|
|
|
val newTarget = when (sheetState.anchoredDraggableState.targetValue) {
|
|
Hidden -> Hidden
|
|
PartiallyExpanded, Expanded -> {
|
|
val hasPartiallyExpandedState = newAnchors.hasAnchorFor(PartiallyExpanded)
|
|
val newTarget = if (hasPartiallyExpandedState) PartiallyExpanded
|
|
else if (newAnchors.hasAnchorFor(Expanded)) Expanded else Hidden
|
|
newTarget
|
|
}
|
|
}
|
|
|
|
sheetState.anchoredDraggableState.updateAnchors(newAnchors, newTarget)
|
|
}
|
|
|
|
/**
|
|
* Popup specific for modal bottom sheet.
|
|
*/
|
|
@OptIn(ExperimentalMaterial3Api::class)
|
|
@Composable
|
|
internal fun ModalBottomSheetPopup(
|
|
properties: ModalBottomSheetProperties,
|
|
onDismissRequest: () -> Unit,
|
|
windowInsets: WindowInsets,
|
|
content: @Composable () -> Unit,
|
|
) {
|
|
val view = LocalView.current
|
|
val id = rememberSaveable { UUID.randomUUID() }
|
|
val parentComposition = rememberCompositionContext()
|
|
val currentContent by rememberUpdatedState(content)
|
|
val layoutDirection = LocalLayoutDirection.current
|
|
val modalBottomSheetWindow = remember {
|
|
ModalBottomSheetWindow(
|
|
properties = properties,
|
|
onDismissRequest = onDismissRequest,
|
|
composeView = view,
|
|
saveId = id
|
|
).apply {
|
|
setCustomContent(
|
|
parent = parentComposition,
|
|
content = {
|
|
Box(
|
|
Modifier
|
|
.semantics { this.popup() }
|
|
.windowInsetsPadding(windowInsets)
|
|
.then(
|
|
// TODO(b/290893168): Figure out a solution for APIs < 30.
|
|
if (Build.VERSION.SDK_INT >= 33)
|
|
Modifier.imePadding()
|
|
else Modifier
|
|
)
|
|
) {
|
|
currentContent()
|
|
}
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
DisposableEffect(modalBottomSheetWindow) {
|
|
modalBottomSheetWindow.show()
|
|
modalBottomSheetWindow.superSetLayoutDirection(layoutDirection)
|
|
onDispose {
|
|
modalBottomSheetWindow.disposeComposition()
|
|
modalBottomSheetWindow.dismiss()
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Custom compose view for [ModalBottomSheet] */
|
|
@OptIn(ExperimentalMaterial3Api::class)
|
|
private class ModalBottomSheetWindow(
|
|
private val properties: ModalBottomSheetProperties,
|
|
private var onDismissRequest: () -> Unit,
|
|
private val composeView: View,
|
|
saveId: UUID
|
|
) :
|
|
AbstractComposeView(composeView.context),
|
|
ViewTreeObserver.OnGlobalLayoutListener,
|
|
ViewRootForInspector {
|
|
|
|
private var backCallback: Any? = null
|
|
|
|
init {
|
|
id = android.R.id.content
|
|
// Set up view owners
|
|
setViewTreeLifecycleOwner(composeView.findViewTreeLifecycleOwner())
|
|
setViewTreeViewModelStoreOwner(composeView.findViewTreeViewModelStoreOwner())
|
|
setViewTreeSavedStateRegistryOwner(composeView.findViewTreeSavedStateRegistryOwner())
|
|
setTag(androidx.compose.ui.R.id.compose_view_saveable_id_tag, "Popup:$saveId")
|
|
// Enable children to draw their shadow by not clipping them
|
|
clipChildren = false
|
|
}
|
|
|
|
private val windowManager =
|
|
composeView.context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
|
|
|
private val displayWidth: Int
|
|
get() = context.resources.displayMetrics.widthPixels
|
|
|
|
private val params: WindowManager.LayoutParams =
|
|
WindowManager.LayoutParams().apply {
|
|
// Position bottom sheet from the bottom of the screen
|
|
gravity = Gravity.BOTTOM or Gravity.START
|
|
// Application panel window
|
|
type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL
|
|
// Fill up the entire app view
|
|
width = displayWidth
|
|
height = WindowManager.LayoutParams.MATCH_PARENT
|
|
|
|
// Format of screen pixels
|
|
format = PixelFormat.TRANSLUCENT
|
|
// Title used as fallback for a11y services
|
|
// TODO: Provide bottom sheet window resource
|
|
title = composeView.context.resources.getString(
|
|
androidx.compose.ui.R.string.default_popup_window_title
|
|
)
|
|
// Get the Window token from the parent view
|
|
token = composeView.applicationWindowToken
|
|
|
|
// Flags specific to modal bottom sheet.
|
|
flags = flags and (
|
|
WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES or
|
|
WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
|
|
).inv()
|
|
|
|
flags = flags or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
|
|
|
|
// Security flag
|
|
val secureFlagEnabled =
|
|
properties.securePolicy.shouldApplySecureFlag(composeView.isFlagSecureEnabled())
|
|
if (secureFlagEnabled) {
|
|
flags = flags or WindowManager.LayoutParams.FLAG_SECURE
|
|
} else {
|
|
flags = flags and (WindowManager.LayoutParams.FLAG_SECURE.inv())
|
|
}
|
|
|
|
// Focusable
|
|
if (!properties.isFocusable) {
|
|
flags = flags or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
|
} else {
|
|
flags = flags and (WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE.inv())
|
|
}
|
|
}
|
|
|
|
private var content: @Composable () -> Unit by mutableStateOf({})
|
|
|
|
override var shouldCreateCompositionOnAttachedToWindow: Boolean = false
|
|
private set
|
|
|
|
@Composable
|
|
override fun Content() {
|
|
content()
|
|
}
|
|
|
|
fun setCustomContent(
|
|
parent: CompositionContext? = null,
|
|
content: @Composable () -> Unit
|
|
) {
|
|
parent?.let { setParentCompositionContext(it) }
|
|
this.content = content
|
|
shouldCreateCompositionOnAttachedToWindow = true
|
|
}
|
|
|
|
fun show() {
|
|
windowManager.addView(this, params)
|
|
}
|
|
|
|
fun dismiss() {
|
|
setViewTreeLifecycleOwner(null)
|
|
setViewTreeSavedStateRegistryOwner(null)
|
|
composeView.viewTreeObserver.removeOnGlobalLayoutListener(this)
|
|
windowManager.removeViewImmediate(this)
|
|
}
|
|
|
|
/**
|
|
* Taken from PopupWindow. Calls [onDismissRequest] when back button is pressed.
|
|
*/
|
|
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
|
if (event.keyCode == KeyEvent.KEYCODE_BACK && properties.shouldDismissOnBackPress) {
|
|
if (keyDispatcherState == null) {
|
|
return super.dispatchKeyEvent(event)
|
|
}
|
|
if (event.action == KeyEvent.ACTION_DOWN && event.repeatCount == 0) {
|
|
val state = keyDispatcherState
|
|
state?.startTracking(event, this)
|
|
return true
|
|
} else if (event.action == KeyEvent.ACTION_UP) {
|
|
val state = keyDispatcherState
|
|
if (state != null && state.isTracking(event) && !event.isCanceled) {
|
|
onDismissRequest()
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return super.dispatchKeyEvent(event)
|
|
}
|
|
|
|
override fun onAttachedToWindow() {
|
|
super.onAttachedToWindow()
|
|
|
|
maybeRegisterBackCallback()
|
|
}
|
|
|
|
override fun onDetachedFromWindow() {
|
|
super.onDetachedFromWindow()
|
|
|
|
maybeUnregisterBackCallback()
|
|
}
|
|
|
|
private fun maybeRegisterBackCallback() {
|
|
if (!properties.shouldDismissOnBackPress || Build.VERSION.SDK_INT < 33) {
|
|
return
|
|
}
|
|
if (backCallback == null) {
|
|
backCallback = Api33Impl.createBackCallback(onDismissRequest)
|
|
}
|
|
Api33Impl.maybeRegisterBackCallback(this, backCallback)
|
|
}
|
|
|
|
private fun maybeUnregisterBackCallback() {
|
|
if (Build.VERSION.SDK_INT >= 33) {
|
|
Api33Impl.maybeUnregisterBackCallback(this, backCallback)
|
|
}
|
|
backCallback = null
|
|
}
|
|
|
|
override fun onGlobalLayout() {
|
|
// No-op
|
|
}
|
|
|
|
override fun setLayoutDirection(layoutDirection: Int) {
|
|
// Do nothing. ViewRootImpl will call this method attempting to set the layout direction
|
|
// from the context's locale, but we have one already from the parent composition.
|
|
}
|
|
|
|
// Sets the "real" layout direction for our content that we obtain from the parent composition.
|
|
fun superSetLayoutDirection(layoutDirection: LayoutDirection) {
|
|
val direction = when (layoutDirection) {
|
|
LayoutDirection.Ltr -> android.util.LayoutDirection.LTR
|
|
LayoutDirection.Rtl -> android.util.LayoutDirection.RTL
|
|
}
|
|
super.setLayoutDirection(direction)
|
|
}
|
|
|
|
@RequiresApi(33)
|
|
private object Api33Impl {
|
|
@JvmStatic
|
|
@DoNotInline
|
|
fun createBackCallback(onDismissRequest: () -> Unit) =
|
|
OnBackInvokedCallback(onDismissRequest)
|
|
|
|
@JvmStatic
|
|
@DoNotInline
|
|
fun maybeRegisterBackCallback(view: View, backCallback: Any?) {
|
|
if (backCallback is OnBackInvokedCallback) {
|
|
view.findOnBackInvokedDispatcher()?.registerOnBackInvokedCallback(
|
|
OnBackInvokedDispatcher.PRIORITY_OVERLAY,
|
|
backCallback
|
|
)
|
|
}
|
|
}
|
|
|
|
@JvmStatic
|
|
@DoNotInline
|
|
fun maybeUnregisterBackCallback(view: View, backCallback: Any?) {
|
|
if (backCallback is OnBackInvokedCallback) {
|
|
view.findOnBackInvokedDispatcher()?.unregisterOnBackInvokedCallback(backCallback)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Taken from AndroidPopup.android.kt
|
|
private fun View.isFlagSecureEnabled(): Boolean {
|
|
val windowParams = rootView.layoutParams as? WindowManager.LayoutParams
|
|
if (windowParams != null) {
|
|
return (windowParams.flags and WindowManager.LayoutParams.FLAG_SECURE) != 0
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Taken from AndroidPopup.android.kt
|
|
private fun SecureFlagPolicy.shouldApplySecureFlag(isSecureFlagSetOnParent: Boolean): Boolean {
|
|
return when (this) {
|
|
SecureFlagPolicy.SecureOff -> false
|
|
SecureFlagPolicy.SecureOn -> true
|
|
SecureFlagPolicy.Inherit -> isSecureFlagSetOnParent
|
|
}
|
|
}
|