|
| 1 | +package com.maf.custom.views.gradient_button |
| 2 | + |
| 3 | +import android.os.Bundle |
| 4 | +import android.view.View |
| 5 | +import androidx.compose.runtime.MonotonicFrameClock |
| 6 | +import androidx.compose.runtime.PausableMonotonicFrameClock |
| 7 | +import androidx.compose.runtime.Recomposer |
| 8 | +import androidx.compose.ui.InternalComposeUiApi |
| 9 | +import androidx.compose.ui.platform.AbstractComposeView |
| 10 | +import androidx.compose.ui.platform.AndroidUiDispatcher |
| 11 | +import androidx.compose.ui.platform.compositionContext |
| 12 | +import androidx.lifecycle.* |
| 13 | +import androidx.savedstate.SavedStateRegistry |
| 14 | +import androidx.savedstate.SavedStateRegistryController |
| 15 | +import androidx.savedstate.SavedStateRegistryOwner |
| 16 | +import androidx.savedstate.setViewTreeSavedStateRegistryOwner |
| 17 | +import kotlinx.coroutines.* |
| 18 | +import kotlinx.coroutines.android.asCoroutineDispatcher |
| 19 | + |
| 20 | +private class ComposeLayoutPreviewHelper(val view: AbstractComposeView) { |
| 21 | + |
| 22 | + private val fakeSavedStateRegistryOwner = object : SavedStateRegistryOwner { |
| 23 | + private val lifecycle = LifecycleRegistry(this) |
| 24 | + private val controller = SavedStateRegistryController.create(this).apply { |
| 25 | + performRestore(Bundle()) |
| 26 | + } |
| 27 | + |
| 28 | + init { |
| 29 | + // Starts the recomposition. |
| 30 | + lifecycle.currentState = Lifecycle.State.RESUMED |
| 31 | + } |
| 32 | + |
| 33 | + override val savedStateRegistry: SavedStateRegistry |
| 34 | + get() = controller.savedStateRegistry |
| 35 | + |
| 36 | + override fun getLifecycle(): Lifecycle = lifecycle |
| 37 | + } |
| 38 | + |
| 39 | + private val fakeViewModelStoreOwner = object : ViewModelStoreOwner { |
| 40 | + private val viewModelStore = ViewModelStore() |
| 41 | + |
| 42 | + override fun getViewModelStore() = viewModelStore |
| 43 | + } |
| 44 | + |
| 45 | + init { |
| 46 | + val stateRegistryOwner = fakeSavedStateRegistryOwner |
| 47 | + val viewModelStoreOwner = fakeViewModelStoreOwner |
| 48 | + ViewTreeLifecycleOwner.set(view, stateRegistryOwner) |
| 49 | + view.setViewTreeSavedStateRegistryOwner(stateRegistryOwner) |
| 50 | + ViewTreeViewModelStoreOwner.set(view, viewModelStoreOwner) |
| 51 | + } |
| 52 | + |
| 53 | + @OptIn(DelicateCoroutinesApi::class, InternalComposeUiApi::class) |
| 54 | + fun createAndInstallWindowRecomposer( |
| 55 | + rootView: View = view, |
| 56 | + lifecycleOwner: LifecycleOwner = fakeSavedStateRegistryOwner |
| 57 | + ): Recomposer { |
| 58 | + val newRecomposer = createViewTreeRecomposer(lifecycleOwner = lifecycleOwner) |
| 59 | + rootView.compositionContext = newRecomposer |
| 60 | + |
| 61 | + // If the Recomposer shuts down, unregister it so that a future request for a window |
| 62 | + // recomposer will consult the factory for a new one. |
| 63 | + val unsetJob = GlobalScope.launch( |
| 64 | + rootView.handler.asCoroutineDispatcher("windowRecomposer cleanup").immediate |
| 65 | + ) { |
| 66 | + try { |
| 67 | + newRecomposer.join() |
| 68 | + } finally { |
| 69 | + // Unset if the view is detached. (See below for the attach state change listener.) |
| 70 | + // Since this is in a finally in this coroutine, even if this job is cancelled we |
| 71 | + // will resume on the window's UI thread and perform this manipulation there. |
| 72 | + val viewTagRecomposer = rootView.compositionContext |
| 73 | + if (viewTagRecomposer === newRecomposer) { |
| 74 | + rootView.compositionContext = null |
| 75 | + } |
| 76 | + } |
| 77 | + } |
| 78 | + |
| 79 | + // If the root view is detached, cancel the await for recomposer shutdown above. |
| 80 | + // This will also unset the tag reference to this recomposer during its cleanup. |
| 81 | + rootView.addOnAttachStateChangeListener( |
| 82 | + object : View.OnAttachStateChangeListener { |
| 83 | + override fun onViewAttachedToWindow(v: View) {} |
| 84 | + override fun onViewDetachedFromWindow(v: View) { |
| 85 | + v.removeOnAttachStateChangeListener(this) |
| 86 | + // cancel the job to clean up the view tags. |
| 87 | + // this will happen immediately since unsetJob is on an immediate dispatcher |
| 88 | + // for this view's UI thread instead of waiting for the recomposer to join. |
| 89 | + // NOTE: This does NOT cancel the returned recomposer itself, as it may be |
| 90 | + // a shared-instance recomposer that should remain running/is reused elsewhere. |
| 91 | + unsetJob.cancel() |
| 92 | + } |
| 93 | + } |
| 94 | + ) |
| 95 | + return newRecomposer |
| 96 | + } |
| 97 | + |
| 98 | + private fun createViewTreeRecomposer( |
| 99 | + lifecycleOwner: LifecycleOwner |
| 100 | + ): Recomposer { |
| 101 | + val currentThreadContext = AndroidUiDispatcher.CurrentThread |
| 102 | + val pausableClock = currentThreadContext[MonotonicFrameClock]?.let { |
| 103 | + PausableMonotonicFrameClock(it).apply { pause() } |
| 104 | + } |
| 105 | + val contextWithClock = currentThreadContext + (pausableClock ?: Dispatchers.Unconfined) |
| 106 | + val recomposer = Recomposer(contextWithClock) |
| 107 | + val runRecomposeScope = CoroutineScope(contextWithClock) |
| 108 | + |
| 109 | + lifecycleOwner.lifecycle.addObserver( |
| 110 | + LifecycleEventObserver { _, event -> |
| 111 | + when (event) { |
| 112 | + Lifecycle.Event.ON_CREATE -> |
| 113 | + // Undispatched launch since we've configured this scope |
| 114 | + // to be on the UI thread |
| 115 | + runRecomposeScope.launch(start = CoroutineStart.UNDISPATCHED) { |
| 116 | + recomposer.runRecomposeAndApplyChanges() |
| 117 | + } |
| 118 | + Lifecycle.Event.ON_START -> pausableClock?.resume() |
| 119 | + Lifecycle.Event.ON_STOP -> pausableClock?.pause() |
| 120 | + Lifecycle.Event.ON_DESTROY -> { |
| 121 | + recomposer.cancel() |
| 122 | + } |
| 123 | + Lifecycle.Event.ON_RESUME, |
| 124 | + Lifecycle.Event.ON_PAUSE, |
| 125 | + Lifecycle.Event.ON_ANY -> { |
| 126 | + // Do nothing. |
| 127 | + } |
| 128 | + } |
| 129 | + } |
| 130 | + ) |
| 131 | + |
| 132 | + return recomposer |
| 133 | + } |
| 134 | +} |
| 135 | + |
| 136 | +fun AbstractComposeView.setupEditMode() { |
| 137 | + ComposeLayoutPreviewHelper(this).createAndInstallWindowRecomposer() |
| 138 | +} |
0 commit comments