Skip to content

Instantly share code, notes, and snippets.

@kishan-vadoliya
Created October 20, 2023 09:04
Show Gist options
  • Save kishan-vadoliya/9fbd1e3c1590de1e4a1a830c5d4edb3f to your computer and use it in GitHub Desktop.
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…
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
)
}
class MyComposeOverlayService : ComposeOverlayViewService() {
override fun onBind(intent: Intent): IBinder? = null
@Composable
override fun Content() = OverlayDraggableContainer {
Text("My super component")
}
}
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