Created
October 20, 2023 09:04
-
-
Save kishan-vadoliya/9fbd1e3c1590de1e4a1a830c5d4edb3f to your computer and use it in GitHub Desktop.
Jetpack Compose OverlayService. Service ready to display complete view (with the right context to access the window manager and layout inflater if needed, but also access to a saved state registry and a view model store owner). Then a compose overlay service, that use the first one to push a compose view as system overlay, and also proposing a s…
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
import android.content.Intent | |
import android.graphics.PixelFormat | |
import android.os.IBinder | |
import android.view.Gravity | |
import android.view.WindowManager | |
import androidx.compose.foundation.gestures.detectDragGestures | |
import androidx.compose.foundation.layout.Box | |
import androidx.compose.foundation.layout.BoxScope | |
import androidx.compose.runtime.Composable | |
import androidx.compose.runtime.getValue | |
import androidx.compose.runtime.mutableStateOf | |
import androidx.compose.runtime.setValue | |
import androidx.compose.ui.Modifier | |
import androidx.compose.ui.geometry.Offset | |
import androidx.compose.ui.input.pointer.consumeAllChanges | |
import androidx.compose.ui.input.pointer.pointerInput | |
import androidx.compose.ui.platform.ComposeView | |
import androidx.lifecycle.ViewTreeLifecycleOwner | |
import androidx.lifecycle.ViewTreeViewModelStoreOwner | |
import androidx.savedstate.setViewTreeSavedStateRegistryOwner | |
import kotlin.math.roundToInt | |
/** | |
* Service that is ready to display compose overlay view | |
* @author Kishan | |
*/ | |
abstract class ComposeOverlayViewService : ViewReadyService() { | |
// Build the layout param for our popup | |
private val layoutParams by lazy { | |
WindowManager.LayoutParams( | |
WindowManager.LayoutParams.WRAP_CONTENT, | |
WindowManager.LayoutParams.WRAP_CONTENT, | |
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, | |
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, | |
PixelFormat.TRANSLUCENT | |
).apply { | |
gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL | |
} | |
} | |
// The current offset of our overlay composable | |
private var overlayOffset by mutableStateOf(Offset.Zero) | |
// Access our window manager | |
private val windowManager by lazy { | |
overlayContext.getSystemService(WindowManager::class.java) | |
} | |
// Build our compose view | |
private val composeView by lazy { | |
ComposeView(overlayContext) | |
} | |
override fun onBind(intent: Intent): IBinder? = null | |
override fun onCreate() { | |
super.onCreate() | |
// Bound the compose lifecycle, view model and view tree saved state, into our view service | |
ViewTreeLifecycleOwner.set(composeView, this) | |
ViewTreeViewModelStoreOwner.set(composeView) { viewModelStore } | |
composeView.setViewTreeSavedStateRegistryOwner(this) | |
// Set the content of our compose view | |
composeView.setContent { Content() } | |
// Push the compose view into our window manager | |
windowManager.addView(composeView, layoutParams) | |
} | |
override fun onDestroy() { | |
super.onDestroy() | |
// Remove our compose view from the window manager | |
windowManager.removeView(composeView) | |
} | |
@Composable | |
abstract fun Content() | |
/** | |
* Draggable box container (not used by default, since not every overlay should be draggable) | |
*/ | |
@Composable | |
internal fun OverlayDraggableContainer(modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit) = | |
Box( | |
modifier = modifier.pointerInput(Unit) { | |
detectDragGestures { change, dragAmount -> | |
change.consumeAllChanges() | |
// Update our current offset | |
val newOffset = overlayOffset + dragAmount | |
overlayOffset = newOffset | |
// Update the layout params, and then the view | |
layoutParams.apply { | |
x = overlayOffset.x.roundToInt() | |
y = overlayOffset.y.roundToInt() | |
} | |
windowManager.updateViewLayout(composeView, layoutParams) | |
} | |
}, | |
content = content | |
) | |
} |
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
class MyComposeOverlayService : ComposeOverlayViewService() { | |
override fun onBind(intent: Intent): IBinder? = null | |
@Composable | |
override fun Content() = OverlayDraggableContainer { | |
Text("My super component") | |
} | |
} |
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
import android.content.Context | |
import android.hardware.display.DisplayManager | |
import android.view.Display | |
import android.view.WindowManager | |
import androidx.lifecycle.LifecycleService | |
import androidx.lifecycle.ViewModelStore | |
import androidx.lifecycle.ViewModelStoreOwner | |
import androidx.savedstate.SavedStateRegistry | |
import androidx.savedstate.SavedStateRegistryController | |
import androidx.savedstate.SavedStateRegistryOwner | |
/** | |
* Service that is ready to display view, provide a ui context on the primary screen, and all the tools needed to built a view with state managment, view model etc | |
* @author Kishan | |
*/ | |
abstract class ViewReadyService : LifecycleService(), SavedStateRegistryOwner, ViewModelStoreOwner { | |
/** | |
* Build our saved state registry controller | |
*/ | |
private val savedStateRegistryController: SavedStateRegistryController by lazy(LazyThreadSafetyMode.NONE) { | |
SavedStateRegistryController.create(this) | |
} | |
/** | |
* Build our view model store | |
*/ | |
private val internalViewModelStore: ViewModelStore by lazy { | |
ViewModelStore() | |
} | |
/** | |
* Context dedicated to the view | |
*/ | |
internal val overlayContext: Context by lazy { | |
// Get the default display | |
val defaultDisplay: Display = getSystemService(DisplayManager::class.java).getDisplay(Display.DEFAULT_DISPLAY) | |
// Create a display context, and then the window context | |
createDisplayContext(defaultDisplay) | |
.createWindowContext(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, null) | |
} | |
override fun onCreate() { | |
super.onCreate() | |
// Restore the last saved state registry | |
savedStateRegistryController.performRestore(null) | |
} | |
override fun onDestroy() { | |
super.onDestroy() | |
} | |
override val savedStateRegistry: SavedStateRegistry | |
get() = savedStateRegistryController.savedStateRegistry | |
override fun getViewModelStore(): ViewModelStore = internalViewModelStore | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment