Skip to content

Instantly share code, notes, and snippets.

@KONFeature
Created June 28, 2022 21:34
Show Gist options
  • Save KONFeature/2f84436e1c0a1926505cac934d470f90 to your computer and use it in GitHub Desktop.
Save KONFeature/2f84436e1c0a1926505cac934d470f90 to your computer and use it in GitHub Desktop.
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 simple draggable box that can be …
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
)
}
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 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
}
@martinGele
Copy link

I cannot find the library for ViewTreeSavedStateRegistryOwner for some reason it's not working, can you copy and paste the libraries used in your project

@LichtHong
Copy link

LichtHong commented Sep 3, 2022

你好,很棒的代码,它对我很有帮助,但是我发现一个小问题,OverlayDraggableContainer 的 Offset 需要判断 Gravity

我是这样做的

val x = if (layoutParams.gravity and Gravity.END xor Gravity.END != 0) { overlayOffset.x + dragAmount.x } else { overlayOffset.x - dragAmount.x }
val y = if (layoutParams.gravity and Gravity.BOTTOM xor Gravity.BOTTOM != 0) { overlayOffset.y + dragAmount.y } else { overlayOffset.y - dragAmount.y }
overlayOffset = Offset(x,y)
layoutParams.apply {
this.x = overlayOffset.x.roundToInt()
this.y = overlayOffset.y.roundToInt()
}

@shirish87
Copy link

This is an incredibly useful gist, @KONFeature! Would you mind attaching a license to it? Thanks 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment