@@ -7,16 +7,10 @@ import android.view.View
77import android.view.ViewOutlineProvider
88import android.view.Window
99import android.view.WindowManager
10- import android.window.BackEvent
11- import android.window.OnBackAnimationCallback
12- import android.window.OnBackInvokedCallback
13- import android.window.OnBackInvokedDispatcher
1410import androidx.activity.BackEventCompat
1511import androidx.activity.ComponentDialog
16- import androidx.activity.addCallback
12+ import androidx.activity.compose.PredictiveBackHandler
1713import androidx.activity.setViewTreeOnBackPressedDispatcherOwner
18- import androidx.annotation.DoNotInline
19- import androidx.annotation.RequiresApi
2014import androidx.appcompat.view.ContextThemeWrapper
2115import androidx.compose.foundation.isSystemInDarkTheme
2216import androidx.compose.foundation.layout.Box
@@ -29,7 +23,6 @@ import androidx.compose.runtime.getValue
2923import androidx.compose.runtime.mutableStateOf
3024import androidx.compose.runtime.remember
3125import androidx.compose.runtime.rememberCompositionContext
32- import androidx.compose.runtime.rememberCoroutineScope
3326import androidx.compose.runtime.rememberUpdatedState
3427import androidx.compose.runtime.saveable.rememberSaveable
3528import androidx.compose.runtime.setValue
@@ -54,16 +47,7 @@ import androidx.lifecycle.setViewTreeViewModelStoreOwner
5447import androidx.savedstate.findViewTreeSavedStateRegistryOwner
5548import androidx.savedstate.setViewTreeSavedStateRegistryOwner
5649import java.util.UUID
57- import java.util.concurrent.CancellationException
58- import kotlinx.coroutines.CoroutineScope
59- import kotlinx.coroutines.channels.BufferOverflow.SUSPEND
60- import kotlinx.coroutines.channels.Channel
61- import kotlinx.coroutines.channels.Channel.Factory.BUFFERED
6250import kotlinx.coroutines.flow.Flow
63- import kotlinx.coroutines.flow.consumeAsFlow
64- import kotlinx.coroutines.flow.flowOf
65- import kotlinx.coroutines.flow.onCompletion
66- import kotlinx.coroutines.launch
6751
6852@Composable
6953internal fun ModalSheetDialog (
@@ -79,18 +63,16 @@ internal fun ModalSheetDialog(
7963 val dialogId = rememberSaveable { UUID .randomUUID() }
8064 val darkThemeEnabled = isSystemInDarkTheme()
8165 val currentOnPredictiveBack = rememberUpdatedState(onPredictiveBack)
82- val scope = rememberCoroutineScope()
8366
8467 val dialog = remember(view, density) {
8568 ModalSheetDialogWrapper (
86- currentOnPredictiveBack,
87- view,
88- scope,
89- securePolicy,
90- layoutDirection,
91- density,
92- dialogId,
93- darkThemeEnabled,
69+ onPredictiveBack = currentOnPredictiveBack,
70+ composeView = view,
71+ securePolicy = securePolicy,
72+ layoutDirection = layoutDirection,
73+ density = density,
74+ dialogId = dialogId,
75+ darkThemeEnabled = darkThemeEnabled,
9476 ).apply {
9577 setContent(composition) {
9678 Box (
@@ -124,10 +106,8 @@ private class ModalSheetDialogLayout(
124106 context : Context ,
125107 override val window : Window ,
126108 private val onPredictiveBack : State <suspend (Flow <BackEventCompat >) -> Unit >,
127- private val scope : CoroutineScope ,
128109) : AbstractComposeView(context), DialogWindowProvider {
129110 private var content: @Composable () -> Unit by mutableStateOf({})
130- private var backCallback: Any? = null
131111 override var shouldCreateCompositionOnAttachedToWindow: Boolean = false
132112 private set
133113
@@ -140,111 +120,9 @@ private class ModalSheetDialogLayout(
140120
141121 @Composable
142122 override fun Content () {
123+ PredictiveBackHandler (onBack = onPredictiveBack.value)
143124 content()
144125 }
145-
146- override fun onAttachedToWindow () {
147- super .onAttachedToWindow()
148- maybeRegisterBackCallback()
149- }
150-
151- override fun onDetachedFromWindow () {
152- super .onDetachedFromWindow()
153- maybeUnregisterBackCallback()
154- }
155-
156- private fun maybeRegisterBackCallback () {
157- if (Build .VERSION .SDK_INT < 33 ) return
158- if (backCallback == null ) {
159- backCallback = when {
160- Build .VERSION .SDK_INT >= 34 -> Api34Impl .createBackCallback(onPredictiveBack, scope)
161- else -> Api33Impl .createBackCallback(onPredictiveBack, scope)
162- }
163- }
164- Api33Impl .maybeRegisterBackCallback(this , backCallback)
165- }
166-
167- private fun maybeUnregisterBackCallback () {
168- if (Build .VERSION .SDK_INT >= 33 ) {
169- Api33Impl .maybeUnregisterBackCallback(this , backCallback)
170- }
171- backCallback = null
172- }
173-
174- @RequiresApi(34 )
175- private object Api34Impl {
176- @JvmStatic
177- @DoNotInline
178- fun createBackCallback (
179- currentOnBack : State <suspend (Flow <BackEventCompat >) -> Unit >,
180- scope : CoroutineScope ,
181- ) = object : OnBackAnimationCallback {
182- var onBackInstance: OnBackInstance ? = null
183-
184- override fun onBackStarted (backEvent : BackEvent ) {
185- onBackInstance?.cancel()
186- onBackInstance = OnBackInstance (scope, true , currentOnBack.value)
187- }
188-
189- override fun onBackProgressed (backEvent : BackEvent ) {
190- onBackInstance?.send(BackEventCompat (backEvent))
191- }
192-
193- override fun onBackInvoked () {
194- onBackInstance?.apply {
195- if (! isPredictiveBack) {
196- cancel()
197- onBackInstance = null
198- }
199- }
200- if (onBackInstance == null ) {
201- onBackInstance = OnBackInstance (scope, false , currentOnBack.value)
202- }
203- onBackInstance?.close()
204- onBackInstance?.isPredictiveBack = false
205- }
206-
207- override fun onBackCancelled () {
208- onBackInstance?.cancel()
209- onBackInstance = null
210- onBackInstance?.isPredictiveBack = false
211- }
212- }
213- }
214-
215- @RequiresApi(33 )
216- private object Api33Impl {
217- @JvmStatic
218- @DoNotInline
219- fun createBackCallback (
220- currentOnBack : State <suspend (Flow <BackEventCompat >) -> Unit >,
221- scope : CoroutineScope ,
222- ) {
223- OnBackInvokedCallback {
224- scope.launch {
225- currentOnBack.value.invoke(flowOf())
226- }
227- }
228- }
229-
230- @JvmStatic
231- @DoNotInline
232- fun maybeRegisterBackCallback (view : View , backCallback : Any? ) {
233- if (backCallback !is OnBackInvokedCallback ) return
234- val dispatcher = view.findOnBackInvokedDispatcher() ? : return
235- dispatcher.registerOnBackInvokedCallback(
236- OnBackInvokedDispatcher .PRIORITY_OVERLAY ,
237- backCallback,
238- )
239- }
240-
241- @JvmStatic
242- @DoNotInline
243- fun maybeUnregisterBackCallback (view : View , backCallback : Any? ) {
244- if (backCallback !is OnBackInvokedCallback ) return
245- view.findOnBackInvokedDispatcher()?.unregisterOnBackInvokedCallback(backCallback)
246- }
247- }
248126}
249127
250128// Fork of androidx.compose.ui.window.DialogWrapper.
@@ -253,7 +131,6 @@ private class ModalSheetDialogLayout(
253131internal class ModalSheetDialogWrapper (
254132 onPredictiveBack : State <suspend (Flow <BackEventCompat >) -> Unit >,
255133 private val composeView : View ,
256- scope : CoroutineScope ,
257134 securePolicy : SecureFlagPolicy ,
258135 layoutDirection : LayoutDirection ,
259136 density : Density ,
@@ -274,10 +151,9 @@ internal class ModalSheetDialogWrapper(
274151 window.setBackgroundDrawableResource(android.R .color.transparent)
275152 WindowCompat .setDecorFitsSystemWindows(window, false )
276153 dialogLayout = ModalSheetDialogLayout (
277- context,
278- window,
279- onPredictiveBack,
280- scope,
154+ context = context,
155+ window = window,
156+ onPredictiveBack = onPredictiveBack,
281157 ).apply {
282158 // Set unique id for AbstractComposeView. This allows state restoration for the state
283159 // defined inside the Dialog via rememberSaveable()
@@ -310,17 +186,6 @@ internal class ModalSheetDialogWrapper(
310186 dialogLayout.setViewTreeOnBackPressedDispatcherOwner(this )
311187 // Initial setup
312188 updateParameters(securePolicy, layoutDirection, darkThemeEnabled)
313-
314- // Due to how the onDismissRequest callback works
315- // (it enforces a just-in-time decision on whether to update the state to hide the dialog)
316- // we need to unconditionally add a callback here that is always enabled,
317- // meaning we'll never get a system UI controlled predictive back animation
318- // for these dialogs
319- onBackPressedDispatcher.addCallback(this ) {
320- scope.launch {
321- onPredictiveBack.value.invoke(flowOf())
322- }
323- }
324189 }
325190
326191 private fun setLayoutDirection (layoutDirection : LayoutDirection ) {
@@ -383,35 +248,6 @@ internal class ModalSheetDialogWrapper(
383248 }
384249}
385250
386- private class OnBackInstance (
387- scope : CoroutineScope ,
388- var isPredictiveBack : Boolean ,
389- onBack : suspend (progress: Flow <BackEventCompat >) -> Unit ,
390- ) {
391- val channel = Channel <BackEventCompat >(capacity = BUFFERED , onBufferOverflow = SUSPEND )
392- val job = scope.launch {
393- var completed = false
394- onBack(
395- channel.consumeAsFlow().onCompletion {
396- completed = true
397- },
398- )
399- check(completed) {
400- " You must collect the progress flow"
401- }
402- }
403-
404- fun send (backEvent : BackEventCompat ) = channel.trySend(backEvent)
405-
406- // idempotent if invoked more than once
407- fun close () = channel.close()
408-
409- fun cancel () {
410- channel.cancel(CancellationException (" onBack cancelled" ))
411- job.cancel()
412- }
413- }
414-
415251internal fun View.isFlagSecureEnabled (): Boolean {
416252 val windowParams = rootView.layoutParams as ? WindowManager .LayoutParams
417253 if (windowParams != null ) {
0 commit comments