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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate 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.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
@ -95,8 +101,28 @@ class FloatingUIActivity : ComponentActivity() {
} }
private fun setupTransparentOverlay() { private fun setupTransparentOverlay() {
// Make the activity transparent // Make the activity transparent and fullscreen
window.apply { 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( setFlags(
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL, WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
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 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) { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY) setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY)
} else { } else {
@ -150,20 +175,36 @@ fun FloatingFABInterface(
var isProcessing by remember { mutableStateOf(false) } var isProcessing by remember { mutableStateOf(false) }
val hapticFeedback = LocalHapticFeedback.current 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( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(Color.Transparent), .background(Color.Transparent)
contentAlignment = Alignment.BottomEnd
) { ) {
// Main content area - transparent to allow touches through // Main content area - transparent to allow touches through
Spacer(modifier = Modifier.fillMaxSize()) Spacer(modifier = Modifier.fillMaxSize())
// FAB and menu area // FAB and menu area - positioned with offset
Column( Column(
horizontalAlignment = Alignment.End, horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.Bottom, verticalArrangement = Arrangement.Bottom,
modifier = Modifier.padding(16.dp) modifier = Modifier
.offset {
IntOffset(
fabOffset.x.toInt(),
fabOffset.y.toInt()
)
}
.padding(16.dp)
) { ) {
// Expanded Menu Items // Expanded Menu Items
@ -213,7 +254,7 @@ fun FloatingFABInterface(
) )
MenuFABItem( MenuFABItem(
icon = Icons.Default.RadioButtonUnchecked, icon = Icons.Default.AccountCircle,
label = "POKEBALL", label = "POKEBALL",
containerColor = MaterialTheme.colorScheme.error, containerColor = MaterialTheme.colorScheme.error,
onClick = { onClick = {
@ -263,6 +304,19 @@ fun FloatingFABInterface(
onLongClick = { onLongClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
onClose() 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, isProcessing: Boolean,
isMenuExpanded: Boolean, isMenuExpanded: Boolean,
onClick: () -> Unit, onClick: () -> Unit,
onLongClick: () -> Unit onLongClick: () -> Unit,
onDrag: (Offset) -> Unit
) { ) {
val rotation by animateFloatAsState( val rotation by animateFloatAsState(
targetValue = if (isMenuExpanded) 45f else 0f, targetValue = if (isMenuExpanded) 45f else 0f,
@ -295,7 +350,20 @@ fun MainFloatingActionButton(
onClick = onClick, onClick = onClick,
modifier = Modifier modifier = Modifier
.size(56.dp) .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 { containerColor = when {
isProcessing -> MaterialTheme.colorScheme.tertiary isProcessing -> MaterialTheme.colorScheme.tertiary
isMenuExpanded -> MaterialTheme.colorScheme.error isMenuExpanded -> MaterialTheme.colorScheme.error
@ -310,7 +378,7 @@ fun MainFloatingActionButton(
imageVector = when { imageVector = when {
isProcessing -> Icons.Default.Refresh isProcessing -> Icons.Default.Refresh
isMenuExpanded -> Icons.Default.Close isMenuExpanded -> Icons.Default.Close
else -> Icons.Default.Camera else -> Icons.Default.Home
}, },
contentDescription = when { contentDescription = when {
isProcessing -> "Processing..." isProcessing -> "Processing..."

Loading…
Cancel
Save