From 7895a30071e2d39a9a6e10dbb9c4e3197d738129 Mon Sep 17 00:00:00 2001 From: Quildra Date: Fri, 1 Aug 2025 08:02:11 +0100 Subject: [PATCH] feat: add draggable FAB and fix system UI visibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Make FAB truly draggable with detectDragGestures - Add screen bounds checking to keep FAB visible - Hide navigation bar and status bar for immersive overlay experience - Support both Android R+ (WindowInsetsController) and legacy system UI flags - FAB now floats anywhere on screen and can be repositioned by dragging Fixes: Navigation bar and status bar no longer show during capture mode Fixes: FAB is now truly floating and user can position it anywhere 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../pokegoalshelper/ui/FloatingUIActivity.kt | 88 ++++++++++++++++--- 1 file changed, 78 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/quillstudios/pokegoalshelper/ui/FloatingUIActivity.kt b/app/src/main/java/com/quillstudios/pokegoalshelper/ui/FloatingUIActivity.kt index 6ff05e1..f68aedf 100644 --- a/app/src/main/java/com/quillstudios/pokegoalshelper/ui/FloatingUIActivity.kt +++ b/app/src/main/java/com/quillstudios/pokegoalshelper/ui/FloatingUIActivity.kt @@ -20,6 +20,12 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.hapticfeedback.HapticFeedbackType @@ -95,8 +101,28 @@ class FloatingUIActivity : ComponentActivity() { } private fun setupTransparentOverlay() { - // Make the activity transparent + // Make the activity transparent and fullscreen window.apply { + // Hide system UI (navigation bar, status bar) + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { + setDecorFitsSystemWindows(false) + insetsController?.let { controller -> + controller.hide(android.view.WindowInsets.Type.systemBars()) + controller.systemBarsBehavior = android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } + } else { + @Suppress("DEPRECATION") + decorView.systemUiVisibility = ( + android.view.View.SYSTEM_UI_FLAG_LAYOUT_STABLE + or android.view.View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + or android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + or android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + or android.view.View.SYSTEM_UI_FLAG_FULLSCREEN + or android.view.View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + ) + } + + // Make it show over other apps setFlags( WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL, WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL @@ -106,7 +132,6 @@ class FloatingUIActivity : ComponentActivity() { WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH ) - // Make it show over other apps if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY) } else { @@ -150,20 +175,36 @@ fun FloatingFABInterface( var isProcessing by remember { mutableStateOf(false) } val hapticFeedback = LocalHapticFeedback.current + // Draggable position state + var fabOffset by remember { mutableStateOf(Offset.Zero) } + val configuration = LocalConfiguration.current + val density = LocalDensity.current + + // Calculate screen bounds for FAB positioning + val screenWidth = with(density) { configuration.screenWidthDp.dp.toPx() } + val screenHeight = with(density) { configuration.screenHeightDp.dp.toPx() } + val fabSize = with(density) { 56.dp.toPx() } + Box( modifier = Modifier .fillMaxSize() - .background(Color.Transparent), - contentAlignment = Alignment.BottomEnd + .background(Color.Transparent) ) { // Main content area - transparent to allow touches through Spacer(modifier = Modifier.fillMaxSize()) - // FAB and menu area + // FAB and menu area - positioned with offset Column( horizontalAlignment = Alignment.End, verticalArrangement = Arrangement.Bottom, - modifier = Modifier.padding(16.dp) + modifier = Modifier + .offset { + IntOffset( + fabOffset.x.toInt(), + fabOffset.y.toInt() + ) + } + .padding(16.dp) ) { // Expanded Menu Items @@ -213,7 +254,7 @@ fun FloatingFABInterface( ) MenuFABItem( - icon = Icons.Default.RadioButtonUnchecked, + icon = Icons.Default.AccountCircle, label = "POKEBALL", containerColor = MaterialTheme.colorScheme.error, onClick = { @@ -263,6 +304,19 @@ fun FloatingFABInterface( onLongClick = { hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) onClose() + }, + onDrag = { dragAmount -> + val newOffset = Offset( + (fabOffset.x + dragAmount.x).coerceIn( + -screenWidth + fabSize, + 0f + ), + (fabOffset.y + dragAmount.y).coerceIn( + -screenHeight + fabSize, + 0f + ) + ) + fabOffset = newOffset } ) } @@ -274,7 +328,8 @@ fun MainFloatingActionButton( isProcessing: Boolean, isMenuExpanded: Boolean, onClick: () -> Unit, - onLongClick: () -> Unit + onLongClick: () -> Unit, + onDrag: (Offset) -> Unit ) { val rotation by animateFloatAsState( targetValue = if (isMenuExpanded) 45f else 0f, @@ -295,7 +350,20 @@ fun MainFloatingActionButton( onClick = onClick, modifier = Modifier .size(56.dp) - .rotate(if (isProcessing) processingRotation else rotation), + .rotate(if (isProcessing) processingRotation else rotation) + .pointerInput(Unit) { + detectDragGestures( + onDragStart = { + // Provide haptic feedback when drag starts + }, + onDragEnd = { + // Optional: snap to screen edges + }, + onDrag = { _, dragAmount -> + onDrag(dragAmount) + } + ) + }, containerColor = when { isProcessing -> MaterialTheme.colorScheme.tertiary isMenuExpanded -> MaterialTheme.colorScheme.error @@ -310,7 +378,7 @@ fun MainFloatingActionButton( imageVector = when { isProcessing -> Icons.Default.Refresh isMenuExpanded -> Icons.Default.Close - else -> Icons.Default.Camera + else -> Icons.Default.Home }, contentDescription = when { isProcessing -> "Processing..."