Skip to content

Instantly share code, notes, and snippets.

@KONFeature
Created June 28, 2022 21:34
Show Gist options
  • Select an option

  • Save KONFeature/2f84436e1c0a1926505cac934d470f90 to your computer and use it in GitHub Desktop.

Select an option

Save KONFeature/2f84436e1c0a1926505cac934d470f90 to your computer and use it in GitHub Desktop.

Revisions

  1. KONFeature created this gist Jun 28, 2022.
    105 changes: 105 additions & 0 deletions ComposeOverlayViewService.kt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,105 @@
    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 Quentin Nivelais
    */
    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
    )
    }
    9 changes: 9 additions & 0 deletions MyComposeDraggableOverlayService.kt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,9 @@
    class MyComposeOverlayService : ComposeOverlayViewService() {

    override fun onBind(intent: Intent): IBinder? = null

    @Composable
    override fun Content() = OverlayDraggableContainer {
    Text("My super component")
    }
    }
    57 changes: 57 additions & 0 deletions ViewReadyService.kt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,57 @@
    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 Quentin Nivelais
    */
    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
    }