Created
March 15, 2023 04:33
-
-
Save luongvo/5a2837b8cc44661e7ea7c12946799d4a to your computer and use it in GitHub Desktop.
FullscreenPopup
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package eu.wewox.modalsheet | |
import android.annotation.SuppressLint | |
import android.app.Activity | |
import android.content.Context | |
import android.content.ContextWrapper | |
import android.view.KeyEvent | |
import android.view.View | |
import android.view.ViewGroup | |
import androidx.compose.foundation.layout.Box | |
import androidx.compose.runtime.Composable | |
import androidx.compose.runtime.CompositionContext | |
import androidx.compose.runtime.DisposableEffect | |
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.rememberUpdatedState | |
import androidx.compose.runtime.saveable.rememberSaveable | |
import androidx.compose.runtime.setValue | |
import androidx.compose.ui.Modifier | |
import androidx.compose.ui.R | |
import androidx.compose.ui.platform.AbstractComposeView | |
import androidx.compose.ui.platform.LocalView | |
import androidx.compose.ui.platform.ViewRootForInspector | |
import androidx.compose.ui.semantics.popup | |
import androidx.compose.ui.semantics.semantics | |
import androidx.core.view.children | |
import androidx.lifecycle.ViewTreeLifecycleOwner | |
import androidx.lifecycle.ViewTreeViewModelStoreOwner | |
import androidx.savedstate.findViewTreeSavedStateRegistryOwner | |
import androidx.savedstate.setViewTreeSavedStateRegistryOwner | |
import java.util.UUID | |
/** | |
* Opens a popup with the given content. | |
* The popup is visible as long as it is part of the composition hierarchy. | |
* | |
* Note: This is highly reduced version of the official Popup composable with some changes: | |
* * Fixes an issue with action mode (copy-paste) menu, see https://issuetracker.google.com/issues/216662636 | |
* * Adds the view to the decor view of the window, instead of the window itself. | |
* * Do not have properties, as Popup is laid out as fullscreen. | |
* | |
* @param onDismiss Executes when the user clicks outside of the popup. | |
* @param content The content to be displayed inside the popup. | |
*/ | |
@ExperimentalSheetApi | |
@Composable | |
internal fun FullscreenPopup( | |
onDismiss: (() -> Unit)? = null, | |
content: @Composable () -> Unit | |
) { | |
val view = LocalView.current | |
val parentComposition = rememberCompositionContext() | |
val currentContent by rememberUpdatedState(content) | |
val popupId = rememberSaveable { UUID.randomUUID() } | |
val popupLayout = remember { | |
PopupLayout( | |
onDismiss = onDismiss, | |
composeView = view, | |
popupId = popupId | |
).apply { | |
setContent(parentComposition) { | |
Box(Modifier.semantics { this.popup() }) { | |
currentContent() | |
} | |
} | |
} | |
} | |
DisposableEffect(popupLayout) { | |
popupLayout.show() | |
popupLayout.updateParameters( | |
onDismiss = onDismiss | |
) | |
onDispose { | |
popupLayout.disposeComposition() | |
// Remove the window | |
popupLayout.dismiss() | |
} | |
} | |
SideEffect { | |
popupLayout.updateParameters( | |
onDismiss = onDismiss | |
) | |
} | |
} | |
/** | |
* The layout the popup uses to display its content. | |
*/ | |
@SuppressLint("ViewConstructor") | |
private class PopupLayout( | |
private var onDismiss: (() -> Unit)?, | |
composeView: View, | |
popupId: UUID | |
) : AbstractComposeView(composeView.context), | |
ViewRootForInspector { | |
private val decorView = findOwner<Activity>(composeView.context)?.window?.decorView as ViewGroup | |
override val subCompositionView: AbstractComposeView get() = this | |
init { | |
id = android.R.id.content | |
ViewTreeLifecycleOwner.set(this, ViewTreeLifecycleOwner.get(composeView)) | |
ViewTreeViewModelStoreOwner.set(this, ViewTreeViewModelStoreOwner.get(composeView)) | |
setViewTreeSavedStateRegistryOwner(composeView.findViewTreeSavedStateRegistryOwner()) | |
// Set unique id for AbstractComposeView. This allows state restoration for the state | |
// defined inside the Popup via rememberSaveable() | |
setTag(R.id.compose_view_saveable_id_tag, "Popup:$popupId") | |
setTag(R.id.consume_window_insets_tag, false) | |
} | |
private var content: @Composable () -> Unit by mutableStateOf({}) | |
override var shouldCreateCompositionOnAttachedToWindow: Boolean = false | |
private set | |
fun show() { | |
// Place popup above all current views | |
z = decorView.children.maxOf { it.z } + 1 | |
decorView.addView( | |
this, | |
0, | |
MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) | |
) | |
requestFocus() | |
} | |
fun setContent(parent: CompositionContext, content: @Composable () -> Unit) { | |
setParentCompositionContext(parent) | |
this.content = content | |
shouldCreateCompositionOnAttachedToWindow = true | |
} | |
@Composable | |
override fun Content() { | |
content() | |
} | |
@Suppress("ReturnCount") | |
override fun dispatchKeyEvent(event: KeyEvent): Boolean { | |
if (event.keyCode == KeyEvent.KEYCODE_BACK && onDismiss != null) { | |
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) { | |
onDismiss?.invoke() | |
return true | |
} | |
} | |
} | |
return super.dispatchKeyEvent(event) | |
} | |
fun updateParameters( | |
onDismiss: (() -> Unit)? | |
) { | |
this.onDismiss = onDismiss | |
} | |
fun dismiss() { | |
ViewTreeLifecycleOwner.set(this, null) | |
decorView.removeView(this) | |
} | |
} | |
private inline fun <reified T> findOwner(context: Context): T? { | |
var innerContext = context | |
while (innerContext is ContextWrapper) { | |
if (innerContext is T) { | |
return innerContext | |
} | |
innerContext = innerContext.baseContext | |
} | |
return null | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment