1 /*
<lambda>null2 * Copyright 2020 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 androidx.compose.ui.window
18
19 import android.content.Context
20 import android.graphics.Outline
21 import android.os.Build
22 import android.view.ContextThemeWrapper
23 import android.view.Gravity
24 import android.view.KeyEvent
25 import android.view.MotionEvent
26 import android.view.View
27 import android.view.ViewGroup
28 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
29 import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
30 import android.view.ViewOutlineProvider
31 import android.view.Window
32 import android.view.WindowManager
33 import androidx.activity.ComponentDialog
34 import androidx.activity.addCallback
35 import androidx.annotation.DoNotInline
36 import androidx.annotation.RequiresApi
37 import androidx.compose.runtime.Composable
38 import androidx.compose.runtime.CompositionContext
39 import androidx.compose.runtime.DisposableEffect
40 import androidx.compose.runtime.Immutable
41 import androidx.compose.runtime.LaunchedEffect
42 import androidx.compose.runtime.SideEffect
43 import androidx.compose.runtime.getValue
44 import androidx.compose.runtime.mutableStateOf
45 import androidx.compose.runtime.remember
46 import androidx.compose.runtime.rememberCompositionContext
47 import androidx.compose.runtime.rememberUpdatedState
48 import androidx.compose.runtime.saveable.rememberSaveable
49 import androidx.compose.runtime.setValue
50 import androidx.compose.ui.Modifier
51 import androidx.compose.ui.R
52 import androidx.compose.ui.layout.Layout
53 import androidx.compose.ui.platform.AbstractComposeView
54 import androidx.compose.ui.platform.LocalDensity
55 import androidx.compose.ui.platform.LocalLayoutDirection
56 import androidx.compose.ui.platform.LocalView
57 import androidx.compose.ui.platform.ViewRootForInspector
58 import androidx.compose.ui.semantics.dialog
59 import androidx.compose.ui.semantics.semantics
60 import androidx.compose.ui.unit.Density
61 import androidx.compose.ui.unit.LayoutDirection
62 import androidx.compose.ui.unit.dp
63 import androidx.compose.ui.util.fastCoerceAtLeast
64 import androidx.compose.ui.util.fastForEach
65 import androidx.compose.ui.util.fastMap
66 import androidx.core.graphics.Insets
67 import androidx.core.view.OnApplyWindowInsetsListener
68 import androidx.core.view.ViewCompat
69 import androidx.core.view.WindowCompat
70 import androidx.core.view.WindowInsetsAnimationCompat
71 import androidx.core.view.WindowInsetsCompat
72 import androidx.lifecycle.findViewTreeLifecycleOwner
73 import androidx.lifecycle.findViewTreeViewModelStoreOwner
74 import androidx.lifecycle.setViewTreeLifecycleOwner
75 import androidx.lifecycle.setViewTreeViewModelStoreOwner
76 import androidx.savedstate.findViewTreeSavedStateRegistryOwner
77 import androidx.savedstate.setViewTreeSavedStateRegistryOwner
78 import java.util.UUID
79 import kotlin.math.max
80 import kotlin.math.roundToInt
81
82 /**
83 * Properties used to customize the behavior of a [Dialog].
84 *
85 * @property dismissOnBackPress whether the dialog can be dismissed by pressing the back or escape
86 * buttons. If true, pressing the back button will call onDismissRequest.
87 * @property dismissOnClickOutside whether the dialog can be dismissed by clicking outside the
88 * dialog's bounds. If true, clicking outside the dialog will call onDismissRequest.
89 * @property securePolicy Policy for setting [WindowManager.LayoutParams.FLAG_SECURE] on the
90 * dialog's window.
91 * @property usePlatformDefaultWidth Whether the width of the dialog's content should be limited to
92 * the platform default, which is smaller than the screen width. It is recommended to use
93 * [decorFitsSystemWindows] set to `false` when [usePlatformDefaultWidth] is false to support
94 * using the entire screen and avoiding UI glitches on some devices when the IME animates in.
95 * @property decorFitsSystemWindows Sets [WindowCompat.setDecorFitsSystemWindows] value. Set to
96 * `false` to use WindowInsets. If `false`, the
97 * [soft input mode][WindowManager.LayoutParams.softInputMode] will be changed to
98 * [WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE] on [Build.VERSION_CODES.R] and below and
99 * [WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING] on [Build.VERSION_CODES.S] and above.
100 * [Window.isFloating] will be `false` when `decorFitsSystemWindows` is `false`.
101 */
102 @Immutable
103 actual class DialogProperties(
104 actual val dismissOnBackPress: Boolean = true,
105 actual val dismissOnClickOutside: Boolean = true,
106 val securePolicy: SecureFlagPolicy = SecureFlagPolicy.Inherit,
107 actual val usePlatformDefaultWidth: Boolean = true,
108 val decorFitsSystemWindows: Boolean = true
109 ) {
110 actual constructor(
111 dismissOnBackPress: Boolean,
112 dismissOnClickOutside: Boolean,
113 usePlatformDefaultWidth: Boolean,
114 ) : this(
115 dismissOnBackPress = dismissOnBackPress,
116 dismissOnClickOutside = dismissOnClickOutside,
117 securePolicy = SecureFlagPolicy.Inherit,
118 usePlatformDefaultWidth = usePlatformDefaultWidth,
119 decorFitsSystemWindows = true
120 )
121
122 @Deprecated("Maintained for binary compatibility", level = DeprecationLevel.HIDDEN)
123 constructor(
124 dismissOnBackPress: Boolean = true,
125 dismissOnClickOutside: Boolean = true,
126 securePolicy: SecureFlagPolicy = SecureFlagPolicy.Inherit,
127 ) : this(
128 dismissOnBackPress = dismissOnBackPress,
129 dismissOnClickOutside = dismissOnClickOutside,
130 securePolicy = securePolicy,
131 usePlatformDefaultWidth = true,
132 decorFitsSystemWindows = true
133 )
134
135 override fun equals(other: Any?): Boolean {
136 if (this === other) return true
137 if (other !is DialogProperties) return false
138
139 if (dismissOnBackPress != other.dismissOnBackPress) return false
140 if (dismissOnClickOutside != other.dismissOnClickOutside) return false
141 if (securePolicy != other.securePolicy) return false
142 if (usePlatformDefaultWidth != other.usePlatformDefaultWidth) return false
143 if (decorFitsSystemWindows != other.decorFitsSystemWindows) return false
144
145 return true
146 }
147
148 override fun hashCode(): Int {
149 var result = dismissOnBackPress.hashCode()
150 result = 31 * result + dismissOnClickOutside.hashCode()
151 result = 31 * result + securePolicy.hashCode()
152 result = 31 * result + usePlatformDefaultWidth.hashCode()
153 result = 31 * result + decorFitsSystemWindows.hashCode()
154 return result
155 }
156 }
157
158 /**
159 * Opens a dialog with the given content.
160 *
161 * A dialog is a small window that prompts the user to make a decision or enter additional
162 * information. A dialog does not fill the screen and is normally used for modal events that require
163 * users to take an action before they can proceed.
164 *
165 * The dialog is visible as long as it is part of the composition hierarchy. In order to let the
166 * user dismiss the Dialog, the implementation of [onDismissRequest] should contain a way to remove
167 * the dialog from the composition hierarchy.
168 *
169 * Example usage:
170 *
171 * @sample androidx.compose.ui.samples.DialogSample
172 * @param onDismissRequest Executes when the user tries to dismiss the dialog.
173 * @param properties [DialogProperties] for further customization of this dialog's behavior.
174 * @param content The content to be displayed inside the dialog.
175 */
176 @Composable
177 actual fun Dialog(
178 onDismissRequest: () -> Unit,
179 properties: DialogProperties,
180 content: @Composable () -> Unit
181 ) {
182 val view = LocalView.current
183 val density = LocalDensity.current
184 val layoutDirection = LocalLayoutDirection.current
185 val composition = rememberCompositionContext()
186 val currentContent by rememberUpdatedState(content)
<lambda>null187 val dialogId = rememberSaveable { UUID.randomUUID() }
188 val dialog =
<lambda>null189 remember(view, density) {
190 DialogWrapper(onDismissRequest, properties, view, layoutDirection, density, dialogId)
191 .apply {
192 setContent(composition) {
193 DialogLayout(Modifier.semantics { dialog() }, currentContent)
194 }
195 }
196 }
197
<lambda>null198 LaunchedEffect(Unit) { dialog.show() }
199
<lambda>null200 DisposableEffect(dialog) {
201 onDispose {
202 dialog.dismiss()
203 dialog.disposeComposition()
204 }
205 }
206
<lambda>null207 SideEffect {
208 dialog.updateParameters(
209 onDismissRequest = onDismissRequest,
210 properties = properties,
211 layoutDirection = layoutDirection
212 )
213 }
214 }
215
216 /**
217 * Provides the underlying window of a dialog.
218 *
219 * Implemented by dialog's root layout.
220 */
221 interface DialogWindowProvider {
222 val window: Window
223 }
224
225 @Suppress("ViewConstructor")
226 private class DialogLayout(context: Context, override val window: Window) :
227 AbstractComposeView(context), DialogWindowProvider, OnApplyWindowInsetsListener {
228
229 private var content: @Composable () -> Unit by mutableStateOf({})
230
231 private var usePlatformDefaultWidth = false
232 private var decorFitsSystemWindows = false
233 private var hasCalledSetLayout = false
234
235 override var shouldCreateCompositionOnAttachedToWindow: Boolean = false
236 private set
237
<lambda>null238 init {
239 ViewCompat.setOnApplyWindowInsetsListener(this, this)
240 ViewCompat.setWindowInsetsAnimationCallback(
241 this,
242 object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
243 override fun onStart(
244 animation: WindowInsetsAnimationCompat,
245 bounds: WindowInsetsAnimationCompat.BoundsCompat
246 ): WindowInsetsAnimationCompat.BoundsCompat =
247 insetValue(bounds) { l, t, r, b -> bounds.inset(Insets.of(l, t, r, b)) }
248
249 override fun onProgress(
250 insets: WindowInsetsCompat,
251 runningAnimations: MutableList<WindowInsetsAnimationCompat>
252 ): WindowInsetsCompat =
253 insetValue(insets) { l, t, r, b -> insets.inset(l, t, r, b) }
254 }
255 )
256 }
257
updatePropertiesnull258 fun updateProperties(usePlatformDefaultWidth: Boolean, decorFitsSystemWindows: Boolean) {
259 val callSetLayout =
260 !hasCalledSetLayout ||
261 usePlatformDefaultWidth != this.usePlatformDefaultWidth ||
262 decorFitsSystemWindows != this.decorFitsSystemWindows
263 this.usePlatformDefaultWidth = usePlatformDefaultWidth
264 this.decorFitsSystemWindows = decorFitsSystemWindows
265
266 if (callSetLayout) {
267 val attrs = window.attributes
268 val measurementWidth = if (usePlatformDefaultWidth) WRAP_CONTENT else MATCH_PARENT
269 if (measurementWidth != attrs.width || !hasCalledSetLayout) {
270 // Always use WRAP_CONTENT for height. internalOnMeasure() will change
271 // it to MATCH_PARENT if it needs more height. If we use MATCH_PARENT here,
272 // and change to WRAP_CONTENT in internalOnMeasure(), the window size will
273 // be wrong on the first frame.
274 window.setLayout(measurementWidth, WRAP_CONTENT)
275 hasCalledSetLayout = true
276 }
277 }
278 }
279
internalOnMeasurenull280 override fun internalOnMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
281 val child = getChildAt(0)
282 if (child == null) {
283 super.internalOnMeasure(widthMeasureSpec, heightMeasureSpec)
284 return
285 }
286 val width = MeasureSpec.getSize(widthMeasureSpec)
287 val height = MeasureSpec.getSize(heightMeasureSpec)
288 val heightMode = MeasureSpec.getMode(heightMeasureSpec)
289 val targetHeight =
290 if (
291 heightMode == MeasureSpec.AT_MOST &&
292 !usePlatformDefaultWidth &&
293 !decorFitsSystemWindows &&
294 window.attributes.height == WRAP_CONTENT
295 ) {
296 // Any size larger than the WRAP_CONTENT to test to see if this is full-screen
297 // content.
298 height + 1
299 } else {
300 height
301 }
302
303 val horizontalPadding = paddingLeft + paddingRight
304 val verticalPadding = paddingTop + paddingBottom
305 val remainingWidth = (width - horizontalPadding).fastCoerceAtLeast(0)
306 val remainingHeight = (targetHeight - verticalPadding).fastCoerceAtLeast(0)
307
308 val widthMode = MeasureSpec.getMode(widthMeasureSpec)
309 val childWidthSpec =
310 if (widthMode == MeasureSpec.UNSPECIFIED) {
311 widthMeasureSpec
312 } else {
313 MeasureSpec.makeMeasureSpec(remainingWidth, MeasureSpec.AT_MOST)
314 }
315 val childHeightSpec =
316 if (heightMode == MeasureSpec.UNSPECIFIED) {
317 heightMeasureSpec
318 } else {
319 MeasureSpec.makeMeasureSpec(remainingHeight, MeasureSpec.AT_MOST)
320 }
321 child.measure(childWidthSpec, childHeightSpec)
322
323 // respect passed dimensions
324 val measuredWidth =
325 when (widthMode) {
326 MeasureSpec.EXACTLY -> width
327 MeasureSpec.AT_MOST -> minOf(width, child.measuredWidth + horizontalPadding)
328 else -> child.measuredWidth + horizontalPadding
329 }
330 val measuredHeight =
331 when (heightMode) {
332 MeasureSpec.EXACTLY -> height
333 MeasureSpec.AT_MOST -> minOf(height, child.measuredHeight + verticalPadding)
334 else -> child.measuredHeight + verticalPadding
335 }
336 setMeasuredDimension(measuredWidth, measuredHeight)
337
338 if (
339 !decorFitsSystemWindows &&
340 child.measuredHeight + verticalPadding > height &&
341 window.attributes.height == WRAP_CONTENT
342 ) {
343 // We're going to use the full screen, so don't put a background behind the system bars
344 window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
345 if (!usePlatformDefaultWidth) {
346 // The size of the window is too small with WRAP_CONTENT for height. Change it
347 // to use MATCH_PARENT to give as much room as possible
348 window.setLayout(MATCH_PARENT, MATCH_PARENT)
349 }
350 }
351 }
352
internalOnLayoutnull353 override fun internalOnLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
354 val child = getChildAt(0) ?: return
355
356 // center content
357 val hPadding = paddingLeft + paddingRight
358 val vPadding = paddingTop + paddingBottom
359 val width = right - left
360 val height = bottom - top
361 val childWidth = child.measuredWidth
362 val childHeight = child.measuredHeight
363
364 val extraWidth = width - childWidth - hPadding
365 val extraHeight = height - childHeight - vPadding
366
367 val l = paddingLeft + (extraWidth / 2)
368 val t = paddingTop + (extraHeight / 2)
369 val r = l + childWidth
370 val b = t + childHeight
371 child.layout(l, t, r, b)
372 }
373
setContentnull374 fun setContent(parent: CompositionContext, content: @Composable () -> Unit) {
375 setParentCompositionContext(parent)
376 this.content = content
377 shouldCreateCompositionOnAttachedToWindow = true
378 createComposition()
379 }
380
onApplyWindowInsetsnull381 override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat =
382 insetValue(insets) { l, t, r, b -> insets.inset(l, t, r, b) }
383
insetValuenull384 private inline fun <T> insetValue(
385 unchangedValue: T,
386 block: (left: Int, top: Int, right: Int, bottom: Int) -> T
387 ): T {
388 if (decorFitsSystemWindows) {
389 return unchangedValue
390 }
391 val child = getChildAt(0)
392 val left = maxOf(0, child.left)
393 val top = maxOf(0, child.top)
394 val right = maxOf(0, width - child.right)
395 val bottom = maxOf(0, height - child.bottom)
396 return if (left == 0 && top == 0 && right == 0 && bottom == 0) {
397 unchangedValue
398 } else {
399 block(left, top, right, bottom)
400 }
401 }
402
isInsideContentnull403 fun isInsideContent(event: MotionEvent): Boolean {
404 if (!event.x.isFinite() || !event.y.isFinite()) return false
405 val child = getChildAt(0) ?: return false
406 val left = left + child.left
407 val right = left + child.width
408 val top = top + child.top
409 val bottom = top + child.height
410 return event.x.roundToInt() in left..right && event.y.roundToInt() in top..bottom
411 }
412
413 @Composable
Contentnull414 override fun Content() {
415 content()
416 }
417 }
418
419 private class DialogWrapper(
420 private var onDismissRequest: () -> Unit,
421 private var properties: DialogProperties,
422 private val composeView: View,
423 layoutDirection: LayoutDirection,
424 density: Density,
425 dialogId: UUID
426 ) :
427 ComponentDialog(
428 /**
429 * [Window.setClipToOutline] is only available from 22+, but the style attribute exists
430 * on 21. So use a wrapped context that sets this attribute for compatibility back to 21.
431 */
432 ContextThemeWrapper(
433 composeView.context,
434 if (properties.decorFitsSystemWindows) {
435 R.style.DialogWindowTheme
436 } else {
437 R.style.FloatingDialogWindowTheme
438 }
439 )
440 ),
441 ViewRootForInspector {
442
443 private val dialogLayout: DialogLayout
444
445 // On systems older than Android S, there is a bug in the surface insets matrix math used by
446 // elevation, so high values of maxSupportedElevation break accessibility services: b/232788477.
447 private val maxSupportedElevation = 8.dp
448
449 private var isPressOutside = false
450
451 override val subCompositionView: AbstractComposeView
452 get() = dialogLayout
453
<lambda>null454 init {
455 val window = window ?: error("Dialog has no window")
456 window.requestFeature(Window.FEATURE_NO_TITLE)
457 window.setBackgroundDrawableResource(android.R.color.transparent)
458 WindowCompat.setDecorFitsSystemWindows(window, properties.decorFitsSystemWindows)
459 window.setGravity(Gravity.CENTER)
460 if (!properties.decorFitsSystemWindows) {
461 @Suppress("DEPRECATION")
462 window.addFlags(
463 WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR or
464 WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
465 )
466 val attrs = window.attributes
467 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
468 Api30Impl.setFitInsetsSides(attrs, 0)
469 Api30Impl.setFitInsetsTypes(attrs, 0)
470 }
471 window.attributes = attrs
472 }
473
474 dialogLayout =
475 DialogLayout(context, window).apply {
476 // Set unique id for AbstractComposeView. This allows state restoration for the
477 // state
478 // defined inside the Dialog via rememberSaveable()
479 setTag(R.id.compose_view_saveable_id_tag, "Dialog:$dialogId")
480 // Enable children to draw their shadow by not clipping them
481 clipChildren = false
482 // Allocate space for elevation
483 with(density) { elevation = maxSupportedElevation.toPx() }
484 // Simple outline to force window manager to allocate space for shadow.
485 // Note that the outline affects clickable area for the dismiss listener. In case of
486 // shapes like circle the area for dismiss might be to small (rectangular outline
487 // consuming clicks outside of the circle).
488 outlineProvider =
489 object : ViewOutlineProvider() {
490 override fun getOutline(view: View, result: Outline) {
491 result.setRect(0, 0, view.width, view.height)
492 // We set alpha to 0 to hide the view's shadow and let the composable to
493 // draw its own shadow. This still enables us to get the extra space
494 // needed in the surface.
495 result.alpha = 0f
496 }
497 }
498 }
499
500 /**
501 * Disables clipping for [this] and all its descendant [ViewGroup]s until we reach a
502 * [DialogLayout] (the [ViewGroup] containing the Compose hierarchy).
503 */
504 fun ViewGroup.disableClipping() {
505 clipChildren = false
506 if (this is DialogLayout) return
507 for (i in 0 until childCount) {
508 (getChildAt(i) as? ViewGroup)?.disableClipping()
509 }
510 }
511
512 // Turn of all clipping so shadows can be drawn outside the window
513 (window.decorView as? ViewGroup)?.disableClipping()
514
515 setContentView(dialogLayout)
516 dialogLayout.setViewTreeLifecycleOwner(composeView.findViewTreeLifecycleOwner())
517 dialogLayout.setViewTreeViewModelStoreOwner(composeView.findViewTreeViewModelStoreOwner())
518 dialogLayout.setViewTreeSavedStateRegistryOwner(
519 composeView.findViewTreeSavedStateRegistryOwner()
520 )
521
522 // Initial setup
523 updateParameters(onDismissRequest, properties, layoutDirection)
524
525 // Due to how the onDismissRequest callback works
526 // (it enforces a just-in-time decision on whether to update the state to hide the dialog)
527 // we need to unconditionally add a callback here that is always enabled,
528 // meaning we'll never get a system UI controlled predictive back animation
529 // for these dialogs
530 onBackPressedDispatcher.addCallback(this) {
531 if (properties.dismissOnBackPress) {
532 onDismissRequest()
533 }
534 }
535 }
536
onKeyUpnull537 override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
538 if (
539 properties.dismissOnBackPress &&
540 event.isTracking &&
541 !event.isCanceled &&
542 keyCode == KeyEvent.KEYCODE_ESCAPE
543 ) {
544 onDismissRequest()
545 return true
546 }
547 return super.onKeyUp(keyCode, event)
548 }
549
setLayoutDirectionnull550 private fun setLayoutDirection(layoutDirection: LayoutDirection) {
551 dialogLayout.layoutDirection =
552 when (layoutDirection) {
553 LayoutDirection.Ltr -> android.util.LayoutDirection.LTR
554 LayoutDirection.Rtl -> android.util.LayoutDirection.RTL
555 }
556 }
557
setContentnull558 fun setContent(parentComposition: CompositionContext, children: @Composable () -> Unit) {
559 dialogLayout.setContent(parentComposition, children)
560 }
561
setSecurePolicynull562 private fun setSecurePolicy(securePolicy: SecureFlagPolicy) {
563 val secureFlagEnabled =
564 securePolicy.shouldApplySecureFlag(composeView.isFlagSecureEnabled())
565 window!!.setFlags(
566 if (secureFlagEnabled) {
567 WindowManager.LayoutParams.FLAG_SECURE
568 } else {
569 WindowManager.LayoutParams.FLAG_SECURE.inv()
570 },
571 WindowManager.LayoutParams.FLAG_SECURE
572 )
573 }
574
updateParametersnull575 fun updateParameters(
576 onDismissRequest: () -> Unit,
577 properties: DialogProperties,
578 layoutDirection: LayoutDirection
579 ) {
580 this.onDismissRequest = onDismissRequest
581 this.properties = properties
582 setSecurePolicy(properties.securePolicy)
583 setLayoutDirection(layoutDirection)
584 val decorFitsSystemWindows = properties.decorFitsSystemWindows
585 dialogLayout.updateProperties(
586 usePlatformDefaultWidth = properties.usePlatformDefaultWidth,
587 decorFitsSystemWindows = decorFitsSystemWindows
588 )
589 setCanceledOnTouchOutside(properties.dismissOnClickOutside)
590 val window = window
591 if (window != null) {
592 val softInput =
593 when {
594 decorFitsSystemWindows ->
595 WindowManager.LayoutParams.SOFT_INPUT_ADJUST_UNSPECIFIED
596 Build.VERSION.SDK_INT < Build.VERSION_CODES.S ->
597 @Suppress("DEPRECATION") WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
598 else -> WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING
599 }
600 window.setSoftInputMode(softInput)
601 }
602 }
603
disposeCompositionnull604 fun disposeComposition() {
605 dialogLayout.disposeComposition()
606 }
607
onTouchEventnull608 override fun onTouchEvent(event: MotionEvent): Boolean {
609 var result = super.onTouchEvent(event)
610 if (properties.dismissOnClickOutside && !dialogLayout.isInsideContent(event)) {
611 when (event.actionMasked) {
612 MotionEvent.ACTION_DOWN -> {
613 isPressOutside = true
614 result = true
615 }
616 MotionEvent.ACTION_UP ->
617 if (isPressOutside) {
618 onDismissRequest()
619 result = true
620 isPressOutside = false
621 }
622 MotionEvent.ACTION_CANCEL -> isPressOutside = false
623 }
624 } else {
625 when (event.actionMasked) {
626 MotionEvent.ACTION_DOWN,
627 MotionEvent.ACTION_UP,
628 MotionEvent.ACTION_CANCEL -> isPressOutside = false
629 }
630 }
631
632 return result
633 }
634
cancelnull635 override fun cancel() {
636 // Prevents the dialog from dismissing itself
637 return
638 }
639 }
640
641 @Composable
DialogLayoutnull642 private fun DialogLayout(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
643 Layout(content = content, modifier = modifier) { measurables, constraints ->
644 var maxWidth = 0
645 var maxHeight = 0
646 val placeables =
647 measurables.fastMap {
648 it.measure(constraints).apply {
649 maxWidth = max(maxWidth, width)
650 maxHeight = max(maxHeight, height)
651 }
652 }
653 if (measurables.isEmpty()) {
654 maxWidth = constraints.minWidth
655 maxHeight = constraints.minHeight
656 }
657 layout(maxWidth, maxHeight) { placeables.fastForEach { it.placeRelative(0, 0) } }
658 }
659 }
660
661 @RequiresApi(30)
662 private object Api30Impl {
663 @DoNotInline
setFitInsetsSidesnull664 fun setFitInsetsSides(attrs: WindowManager.LayoutParams, sides: Int) {
665 attrs.setFitInsetsSides(sides)
666 }
667
668 @DoNotInline
setFitInsetsTypesnull669 fun setFitInsetsTypes(attrs: WindowManager.LayoutParams, types: Int) {
670 attrs.setFitInsetsTypes(types)
671 }
672 }
673