logickoder
logickoder

Reputation: 45

System-wide screen overlay a draggable UI element in Compose

My goal is to have a small UI element (24dp bubble) that is draggable and is shown over the system screens and other apps. Yes, like XRecorder bubbles.

I have successfully set up a ComposeView and used it with WindowManager to display it over other apps. The small view is also draggable.

The problem is I want to be able to drag it anywhere on the screen, but without blocking touch input outside of the square's area. The blocking happens when I set WindowManager Layout Params to MATCH_PARENT. If I don't, the bubble is clipped when dragged outside of the area of its initial position.

This is the Composable

@Composable
fun Overlay(
    modifier: Modifier = Modifier,
    move: (Int, Int) -> Unit,
) {
    var offsetX by remember { mutableFloatStateOf(0f) }
    var offsetY by remember { mutableFloatStateOf(0f) }

    val context = LocalContext.current
    val onClick: () -> Unit = remember {
        {
            context.startService(
                Intent(
                    ACTION_SCREENSHOT,
                    Uri.EMPTY,
                    context,
                    AppService::class.java
                )
            )
        }
    }
    IconButton(
        modifier = modifier
            .wrapContentSize()
            .offset {
                IntOffset(offsetX.roundToInt(), offsetY.roundToInt())
            }
            .background(
                color = MaterialTheme.colorScheme.primary,
                shape = CircleShape,
            )
            .pointerInput(Unit) {
                detectDragGestures { change, dragAmount ->
                    change.consume()
                    offsetX += dragAmount.x
                    offsetY += dragAmount.y
                    move(
                        dragAmount.x.roundToInt(),
                        dragAmount.y.roundToInt()
                    )
                }
            },
        onClick = onClick,
        content = {
            Icon(
                imageVector = Icons.Rounded.Screenshot,
                tint = MaterialTheme.colorScheme.onPrimary,
                contentDescription = "Take a screenshot"
            )
        }
    )


}

And this is the ComposeView:

// Create a ComposeView to host the overlay UI
        val view = ComposeView(this).also {
            // Set the content of the ComposeView to the Overlay composable function
            it.setContent {
                MaterialTheme {
                    Overlay { x, y ->
                        overlayParams.x += x
                        overlayParams.y += y
                        windowManager.updateViewLayout(view, overlayParams)
                    }
                }
            }
            // Trick the ComposeView into thinking we are tracking lifecycle events
            savedStateRegistryController.performRestore(null)
            handleLifecycleEvent(Lifecycle.Event.ON_CREATE)

            // Set the view tree lifecycle owner, view model store owner, and saved state registry owner
            it.setViewTreeLifecycleOwner(this)
            it.setViewTreeViewModelStoreOwner(this)
            it.setViewTreeSavedStateRegistryOwner(this)
        }

I also launch a foreground service and ask for ACTION_MANAGE_OVERLAY_PERMISSION ( Draw over other apps). enter image description here

Upvotes: 1

Views: 359

Answers (1)

liuwons
liuwons

Reputation: 61

You need to update the position of the ComposeView by calling WindowManger.updateViewLayout() method. Have a look at this implementation: AssistAccessibilityService.kt

in this example the position is updated here:

bubbleComposeView.setContent {
            Bubble(onRequestTranslate = { dx, dy ->
                Log.i("AssistAccessibilityService", "onRequestTranslate: $dx, $dy")
                position.x += dx
                position.y += dy
                windowManager.updateViewLayout(bubbleComposeView, getLayoutParams())
            }, onRequestFinished = {
                GlobalPreferences.saveInt(PREF_KEY_POSITION_X, position.x)
                GlobalPreferences.saveInt(PREF_KEY_POSITION_Y, position.y)
            }, onClick = {
                if (!panelVisible()) {
                    showPanel()
                }
                Log.i("AssistAccessibilityService", "onClick")
            }, modifier = Modifier.width(64.dp).height(64.dp))
        }

Upvotes: 0

Related Questions