feat: add overlay top/bottom snap toggle button
Allow users to move the assistant pill bar between top and bottom of the screen via a chevron button, preventing it from blocking chat input fields or other UI elements. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2c5d8fbf5d
commit
a3a37c0c67
2 changed files with 36 additions and 5 deletions
|
|
@ -87,6 +87,7 @@ class AssistantService : LifecycleService(), SavedStateRegistryOwner {
|
|||
private lateinit var screenCaptureManager: ScreenCaptureManager
|
||||
|
||||
private var showAccessibilityGuide by mutableStateOf(false)
|
||||
private var overlayAtTop by mutableStateOf(false)
|
||||
private var overlayView: View? = null
|
||||
private val windowManager by lazy { getSystemService(WINDOW_SERVICE) as WindowManager }
|
||||
|
||||
|
|
@ -224,6 +225,14 @@ class AssistantService : LifecycleService(), SavedStateRegistryOwner {
|
|||
stopSelf()
|
||||
}
|
||||
|
||||
private fun moveOverlayTo(top: Boolean) {
|
||||
val view = overlayView ?: return
|
||||
val lp = view.layoutParams as? WindowManager.LayoutParams ?: return
|
||||
lp.gravity = if (top) Gravity.TOP else Gravity.BOTTOM
|
||||
windowManager.updateViewLayout(view, lp)
|
||||
overlayAtTop = top
|
||||
}
|
||||
|
||||
private fun setOverlayVisible(visible: Boolean) {
|
||||
val view = overlayView ?: return
|
||||
val lp = view.layoutParams as? WindowManager.LayoutParams ?: return
|
||||
|
|
@ -252,15 +261,16 @@ class AssistantService : LifecycleService(), SavedStateRegistryOwner {
|
|||
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN,
|
||||
PixelFormat.TRANSLUCENT
|
||||
).apply {
|
||||
gravity = Gravity.BOTTOM
|
||||
gravity = if (overlayAtTop) Gravity.TOP else Gravity.BOTTOM
|
||||
}
|
||||
|
||||
val wrapper = object : FrameLayout(this@AssistantService) {
|
||||
@Volatile var contentTop = fixedHeightPx
|
||||
@Volatile var contentBottom = Int.MAX_VALUE
|
||||
private var gestureInContent = false
|
||||
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
|
||||
if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
|
||||
gestureInContent = ev.y >= contentTop
|
||||
gestureInContent = ev.y >= contentTop && ev.y <= contentBottom
|
||||
}
|
||||
if (!gestureInContent) return false
|
||||
return super.dispatchTouchEvent(ev)
|
||||
|
|
@ -276,17 +286,20 @@ class AssistantService : LifecycleService(), SavedStateRegistryOwner {
|
|||
val state by assistantManager.state.collectAsState()
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.BottomCenter
|
||||
contentAlignment = if (overlayAtTop) Alignment.TopCenter else Alignment.BottomCenter
|
||||
) {
|
||||
AssistantPillBar(
|
||||
state = state,
|
||||
isAtTop = overlayAtTop,
|
||||
onClose = { shutdown() },
|
||||
onInterrupt = { assistantManager.interrupt() },
|
||||
onPositionChange = { top -> moveOverlayTo(top) },
|
||||
onCameraToggle = { handleCameraToggle() },
|
||||
onScreenCaptureToggle = { handleScreenCaptureToggle() },
|
||||
cameraCaptureManager = cameraCaptureManager,
|
||||
modifier = Modifier.onGloballyPositioned { coordinates ->
|
||||
wrapper.contentTop = coordinates.positionInWindow().y.toInt()
|
||||
wrapper.contentBottom = (coordinates.positionInWindow().y + coordinates.size.height).toInt()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,8 +73,10 @@ private val ErrorColor = Color(0xFFEF4444)
|
|||
@Composable
|
||||
fun AssistantPillBar(
|
||||
state: VoiceModeState,
|
||||
isAtTop: Boolean,
|
||||
onClose: () -> Unit,
|
||||
onInterrupt: () -> Unit,
|
||||
onPositionChange: (Boolean) -> Unit,
|
||||
onCameraToggle: () -> Unit,
|
||||
onScreenCaptureToggle: () -> Unit,
|
||||
cameraCaptureManager: CameraCaptureManager,
|
||||
|
|
@ -97,8 +99,8 @@ fun AssistantPillBar(
|
|||
// Camera preview above the pill bar
|
||||
AnimatedVisibility(
|
||||
visible = state.isCameraActive,
|
||||
enter = expandVertically(expandFrom = Alignment.Bottom),
|
||||
exit = shrinkVertically(shrinkTowards = Alignment.Bottom)
|
||||
enter = expandVertically(expandFrom = if (isAtTop) Alignment.Top else Alignment.Bottom),
|
||||
exit = shrinkVertically(shrinkTowards = if (isAtTop) Alignment.Top else Alignment.Bottom)
|
||||
) {
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
|
||||
|
|
@ -286,6 +288,22 @@ fun AssistantPillBar(
|
|||
)
|
||||
}
|
||||
|
||||
// Move position toggle
|
||||
IconButton(
|
||||
onClick = { onPositionChange(!isAtTop) },
|
||||
modifier = Modifier.size(36.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(
|
||||
if (isAtTop) LucideR.drawable.lucide_ic_chevron_down
|
||||
else LucideR.drawable.lucide_ic_chevron_up
|
||||
),
|
||||
contentDescription = if (isAtTop) "Move to bottom" else "Move to top",
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = TextSecondary
|
||||
)
|
||||
}
|
||||
|
||||
// Close button
|
||||
IconButton(
|
||||
onClick = onClose,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue