Browse Source

feat: add draggable FAB and fix system UI visibility

- 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 <noreply@anthropic.com>
feature/modern-capture-ui
Quildra 5 months ago
parent
commit
7895a30071
  1. 88
      app/src/main/java/com/quillstudios/pokegoalshelper/ui/FloatingUIActivity.kt

88
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..."

Loading…
Cancel
Save