Browse Source

refactor: remove unused UI implementations and clean up codebase

- Delete FloatingActionButtonUI.kt (580 lines) - unused Material 3 FAB
- Delete FloatingOrbUI.kt (270 lines) - unused basic orb implementation
- Delete FloatingComposeOverlay.kt (352 lines) - unused Compose overlay
- Delete FloatingUIActivity.kt (444 lines) - unused activity with dead code
- Remove FloatingUIActivity from AndroidManifest.xml
- Clean up unused imports and functions in MainActivity.kt
- Update comments in ScreenCaptureService.kt

Total cleanup: 1,646 lines of unused code removed
Active UI: EnhancedFloatingFAB.kt remains as the working implementation

🤖 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
47490749cf
  1. 9
      app/src/main/AndroidManifest.xml
  2. 10
      app/src/main/java/com/quillstudios/pokegoalshelper/MainActivity.kt
  3. 4
      app/src/main/java/com/quillstudios/pokegoalshelper/ScreenCaptureService.kt
  4. 580
      app/src/main/java/com/quillstudios/pokegoalshelper/ui/FloatingActionButtonUI.kt
  5. 352
      app/src/main/java/com/quillstudios/pokegoalshelper/ui/FloatingComposeOverlay.kt
  6. 270
      app/src/main/java/com/quillstudios/pokegoalshelper/ui/FloatingOrbUI.kt
  7. 444
      app/src/main/java/com/quillstudios/pokegoalshelper/ui/FloatingUIActivity.kt

9
app/src/main/AndroidManifest.xml

@ -29,15 +29,6 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".ui.FloatingUIActivity"
android:exported="false"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:launchMode="singleTop"
android:excludeFromRecents="true"
android:taskAffinity=""
android:showOnLockScreen="true" />
<service <service
android:name=".ScreenCaptureService" android:name=".ScreenCaptureService"
android:foregroundServiceType="mediaProjection" android:foregroundServiceType="mediaProjection"

10
app/src/main/java/com/quillstudios/pokegoalshelper/MainActivity.kt

@ -25,7 +25,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.quillstudios.pokegoalshelper.ui.theme.PokeGoalsHelperTheme import com.quillstudios.pokegoalshelper.ui.theme.PokeGoalsHelperTheme
import com.quillstudios.pokegoalshelper.ui.FloatingUIActivity
import org.opencv.android.OpenCVLoader import org.opencv.android.OpenCVLoader
import org.opencv.core.Mat import org.opencv.core.Mat
import org.opencv.core.CvType import org.opencv.core.CvType
@ -166,15 +165,6 @@ class MainActivity : ComponentActivity() {
isCapturing = false isCapturing = false
} }
private fun startFloatingUI() {
val floatingUIIntent = Intent(this, FloatingUIActivity::class.java).apply {
action = FloatingUIActivity.ACTION_SHOW_FAB
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
startActivity(floatingUIIntent)
Log.d(TAG, "FloatingUIActivity started")
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()

4
app/src/main/java/com/quillstudios/pokegoalshelper/ScreenCaptureService.kt

@ -92,7 +92,7 @@ class ScreenCaptureService : Service() {
} }
/** /**
* Binder for communication with FloatingUIActivity * Binder for external communication (if needed in future)
*/ */
inner class LocalBinder : Binder() { inner class LocalBinder : Binder() {
fun getService(): ScreenCaptureService = this@ScreenCaptureService fun getService(): ScreenCaptureService = this@ScreenCaptureService
@ -181,7 +181,7 @@ class ScreenCaptureService : Service() {
override fun onBind(intent: Intent?): IBinder = binder override fun onBind(intent: Intent?): IBinder = binder
// === Public API for FloatingUIActivity === // === Public API for external communication ===
/** /**
* Trigger manual detection from UI * Trigger manual detection from UI

580
app/src/main/java/com/quillstudios/pokegoalshelper/ui/FloatingActionButtonUI.kt

@ -1,580 +0,0 @@
package com.quillstudios.pokegoalshelper.ui
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.*
import android.graphics.drawable.GradientDrawable
import android.os.Build
import android.os.VibrationEffect
import android.os.Vibrator
import android.util.Log
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.view.animation.AccelerateDecelerateInterpolator
import android.view.animation.OvershootInterpolator
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
// import com.quillstudios.pokegoalshelper.R // Not needed for now
import com.quillstudios.pokegoalshelper.ui.interfaces.DetectionUIEvents
import com.quillstudios.pokegoalshelper.ui.interfaces.DetectionUICallbacks
/**
* Modern Material 3 Floating Action Button UI for capture mode.
* Replaces the basic floating orb with a proper FAB implementation.
*
* Features:
* - Material 3 design with proper elevation and shadows
* - Smooth animations for state changes
* - Haptic feedback for interactions
* - Glass-morphism effects
* - Contextual menu expansion
*/
class FloatingActionButtonUI(
private val context: Context,
private val detectionEvents: DetectionUIEvents
) : DetectionUICallbacks {
companion object {
private const val TAG = "FloatingActionButtonUI"
// Material 3 FAB dimensions (dp converted to px)
private const val FAB_SIZE_LARGE = 56 // Main FAB
private const val FAB_SIZE_SMALL = 40 // Menu item FABs
private const val FAB_MARGIN = 16
// Animation durations
private const val ANIMATION_DURATION_FAST = 150L
private const val ANIMATION_DURATION_MEDIUM = 300L
// Material 3 elevation levels
private const val ELEVATION_RESTING = 6f
private const val ELEVATION_PRESSED = 12f
private const val ELEVATION_MENU = 8f
}
private var windowManager: WindowManager? = null
private var fabContainer: FrameLayout? = null
private var mainFab: View? = null
private var expandedMenu: LinearLayout? = null
private var scrimView: View? = null
private var isMenuExpanded = false
private var isProcessing = false
private var currentAnimator: AnimatorSet? = null
private val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
/**
* Initialize and show the floating action button
*/
fun show() {
try {
if (fabContainer != null) return // Already shown
windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
createFloatingActionButton()
Log.d(TAG, "✅ Modern FAB UI shown")
} catch (e: Exception) {
Log.e(TAG, "❌ Error showing FAB", e)
}
}
/**
* Hide and cleanup the floating action button
*/
fun hide() {
try {
currentAnimator?.cancel()
if (isMenuExpanded) {
collapseMenu(animate = false)
}
fabContainer?.let {
windowManager?.removeView(it)
fabContainer = null
}
mainFab = null
expandedMenu = null
windowManager = null
Log.d(TAG, "🗑️ FAB UI hidden")
} catch (e: Exception) {
Log.e(TAG, "❌ Error hiding FAB", e)
}
}
// === DetectionUICallbacks Implementation ===
override fun onDetectionStarted() {
isProcessing = true
updateFabState()
// Auto-collapse menu during processing
if (isMenuExpanded) {
collapseMenu()
}
}
override fun onDetectionCompleted(detectionCount: Int) {
isProcessing = false
updateFabState()
// Provide haptic feedback for completion
provideFeedback(VibrationEffect.EFFECT_TICK)
Log.d(TAG, "🎯 Detection completed: $detectionCount objects")
}
override fun onDetectionFailed(error: String) {
isProcessing = false
updateFabState()
// Provide error feedback
provideFeedback(VibrationEffect.EFFECT_DOUBLE_CLICK)
Log.e(TAG, "❌ Detection failed: $error")
}
override fun onSettingsChanged(filterClass: String?, debugMode: Boolean, coordinateMode: String) {
Log.d(TAG, "⚙️ Settings updated - Filter: $filterClass, Debug: $debugMode, Mode: $coordinateMode")
// Update menu items based on current settings
updateMenuItems()
}
// === Private UI Methods ===
private fun createFloatingActionButton() {
// Create main container
fabContainer = FrameLayout(context).apply {
layoutParams = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
}
// Create main FAB
mainFab = createMainFab()
fabContainer?.addView(mainFab)
// Set up window parameters
val params = WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
} else {
@Suppress("DEPRECATION")
WindowManager.LayoutParams.TYPE_PHONE
},
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT
).apply {
gravity = Gravity.BOTTOM or Gravity.END
x = dpToPx(FAB_MARGIN)
y = dpToPx(FAB_MARGIN + 80) // Account for potential system UI
}
windowManager?.addView(fabContainer, params)
}
private fun createMainFab(): View {
return FrameLayout(context).apply {
val fabSize = dpToPx(FAB_SIZE_LARGE)
layoutParams = FrameLayout.LayoutParams(fabSize, fabSize)
// Create FAB background with Material 3 styling
background = createFabBackground(
color = getColor(android.R.color.holo_blue_bright),
elevation = ELEVATION_RESTING
)
// Add icon
val iconView = ImageView(context).apply {
layoutParams = FrameLayout.LayoutParams(
dpToPx(24), dpToPx(24), Gravity.CENTER
)
setImageResource(android.R.drawable.ic_menu_camera) // Using target icon
setColorFilter(Color.WHITE)
scaleType = ImageView.ScaleType.FIT_CENTER
}
addView(iconView)
// Set up click handling
setOnClickListener { handleFabClick() }
// Add touch feedback
isClickable = true
isFocusable = true
// Apply initial elevation
elevation = ELEVATION_RESTING
}
}
private fun createFabBackground(color: Int, elevation: Float): GradientDrawable {
return GradientDrawable().apply {
shape = GradientDrawable.OVAL
setColor(color)
// Add subtle gradient for depth
colors = intArrayOf(
lightenColor(color, 0.1f),
color,
darkenColor(color, 0.1f)
)
gradientType = GradientDrawable.RADIAL_GRADIENT
gradientRadius = dpToPx(FAB_SIZE_LARGE).toFloat() / 2
// Material 3 shadow simulation (since we can't use real elevation in overlays)
setStroke(dpToPx(1), Color.argb(30, 0, 0, 0))
}
}
private fun handleFabClick() {
provideFeedback(VibrationEffect.EFFECT_CLICK)
if (isProcessing) {
Log.d(TAG, "⚠️ Ignoring click - detection in progress")
return
}
if (isMenuExpanded) {
collapseMenu()
} else {
expandMenu()
}
}
private fun expandMenu() {
if (isMenuExpanded || isProcessing) return
// Create scrim for backdrop
createScrim()
// Create expanded menu
expandedMenu = createExpandedMenu()
fabContainer?.addView(expandedMenu)
// Animate expansion
animateMenuExpansion(true)
isMenuExpanded = true
updateFabState()
}
private fun collapseMenu(animate: Boolean = true) {
if (!isMenuExpanded) return
if (animate) {
animateMenuExpansion(false) {
removeMenuViews()
}
} else {
removeMenuViews()
}
isMenuExpanded = false
updateFabState()
}
private fun createScrim() {
scrimView = View(context).apply {
layoutParams = ViewGroup.LayoutParams(
dpToPx(200), dpToPx(300)
)
setBackgroundColor(Color.argb(80, 0, 0, 0)) // Semi-transparent scrim
alpha = 0f
setOnClickListener { collapseMenu() }
}
fabContainer?.addView(scrimView, 0) // Add as first child (background)
}
private fun createExpandedMenu(): LinearLayout {
return LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
layoutParams = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
Gravity.BOTTOM or Gravity.END
).apply {
bottomMargin = dpToPx(FAB_SIZE_LARGE + 16)
rightMargin = dpToPx(8)
}
// Create menu items
val menuItems = listOf(
FabMenuItem("🔍", "DETECT", getColor(android.R.color.holo_green_dark)) {
detectionEvents.onDetectionRequested()
},
FabMenuItem("", "SHINY", getColor(android.R.color.holo_orange_light)) {
detectionEvents.onClassFilterChanged("shiny_icon")
detectionEvents.onDetectionRequested()
},
FabMenuItem("", "BALL", getColor(android.R.color.holo_red_dark)) {
detectionEvents.onClassFilterChanged("ball_icon_cherishball")
detectionEvents.onDetectionRequested()
},
FabMenuItem("🎯", "ALL", getColor(android.R.color.darker_gray)) {
detectionEvents.onClassFilterChanged(null)
detectionEvents.onDetectionRequested()
},
FabMenuItem("🔧", "DEBUG", getColor(android.R.color.holo_purple)) {
detectionEvents.onDebugModeToggled()
detectionEvents.onDetectionRequested()
}
)
menuItems.forEachIndexed { index, item ->
val fabItem = createMenuFab(item)
addView(fabItem)
// Add spacing between items
if (index < menuItems.size - 1) {
val spacer = View(context).apply {
layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, dpToPx(8)
)
}
addView(spacer)
}
}
}
}
private fun createMenuFab(item: FabMenuItem): View {
return LinearLayout(context).apply {
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER_VERTICAL
layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
// Add label
val label = TextView(context).apply {
text = item.label
setTextColor(Color.WHITE)
textSize = 12f
setPadding(dpToPx(8), dpToPx(4), dpToPx(8), dpToPx(4))
setBackgroundResource(android.R.drawable.btn_default)
background.setTint(Color.argb(200, 0, 0, 0))
alpha = 0f // Start invisible for animation
}
addView(label)
// Add small FAB
val miniFab = FrameLayout(context).apply {
val size = dpToPx(FAB_SIZE_SMALL)
layoutParams = LinearLayout.LayoutParams(size, size).apply {
leftMargin = dpToPx(8)
}
background = createFabBackground(item.color, ELEVATION_MENU)
elevation = ELEVATION_MENU
alpha = 0f // Start invisible for animation
// Add icon
val iconView = TextView(context).apply {
text = item.icon
textSize = 16f
gravity = Gravity.CENTER
layoutParams = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
addView(iconView)
setOnClickListener {
provideFeedback(VibrationEffect.EFFECT_CLICK)
item.action()
collapseMenu()
}
isClickable = true
isFocusable = true
}
addView(miniFab)
}
}
private fun animateMenuExpansion(expand: Boolean, onComplete: (() -> Unit)? = null) {
currentAnimator?.cancel()
val animatorSet = AnimatorSet()
val animators = mutableListOf<ObjectAnimator>()
// Animate scrim
scrimView?.let { scrim ->
animators.add(ObjectAnimator.ofFloat(
scrim, "alpha",
if (expand) 0f else 1f,
if (expand) 1f else 0f
))
}
// Animate menu items
expandedMenu?.let { menu ->
for (i in 0 until menu.childCount) {
val child = menu.getChildAt(i)
if (child is LinearLayout) {
// Animate each FAB item
val delay = if (expand) i * 50L else (menu.childCount - i) * 30L
val scaleX = ObjectAnimator.ofFloat(
child, "scaleX",
if (expand) 0f else 1f,
if (expand) 1f else 0f
)
val scaleY = ObjectAnimator.ofFloat(
child, "scaleY",
if (expand) 0f else 1f,
if (expand) 1f else 0f
)
val alpha = ObjectAnimator.ofFloat(
child, "alpha",
if (expand) 0f else 1f,
if (expand) 1f else 0f
)
scaleX.startDelay = delay
scaleY.startDelay = delay
alpha.startDelay = delay
animators.addAll(listOf(scaleX, scaleY, alpha))
}
}
}
// Configure animation set
animatorSet.playTogether(animators as Collection<android.animation.Animator>)
animatorSet.duration = ANIMATION_DURATION_MEDIUM
animatorSet.interpolator = if (expand) OvershootInterpolator() else AccelerateDecelerateInterpolator()
animatorSet.addListener(object : android.animation.AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: android.animation.Animator) {
onComplete?.invoke()
currentAnimator = null
}
})
currentAnimator = animatorSet
animatorSet.start()
}
private fun removeMenuViews() {
scrimView?.let { fabContainer?.removeView(it) }
expandedMenu?.let { fabContainer?.removeView(it) }
scrimView = null
expandedMenu = null
}
private fun updateFabState() {
val iconView = (mainFab as? FrameLayout)?.getChildAt(0) as? ImageView
val fab = mainFab as? FrameLayout
when {
isProcessing -> {
iconView?.setImageResource(android.R.drawable.ic_popup_sync)
fab?.background = createFabBackground(
getColor(android.R.color.holo_orange_light),
ELEVATION_PRESSED
)
// Add rotation animation for processing state
animateProcessing(iconView)
}
isMenuExpanded -> {
iconView?.setImageResource(android.R.drawable.ic_menu_close_clear_cancel)
fab?.background = createFabBackground(
getColor(android.R.color.holo_red_light),
ELEVATION_PRESSED
)
}
else -> {
iconView?.setImageResource(android.R.drawable.ic_menu_camera)
fab?.background = createFabBackground(
getColor(android.R.color.holo_blue_bright),
ELEVATION_RESTING
)
iconView?.clearAnimation()
}
}
}
private fun updateMenuItems() {
// This could update menu item states based on current filter/debug settings
// For now, just log that we received the update
Log.d(TAG, "Menu items updated based on current settings")
}
private fun animateProcessing(view: View?) {
view?.let {
val rotation = ObjectAnimator.ofFloat(it, "rotation", 0f, 360f)
rotation.duration = 1000L
rotation.repeatCount = ValueAnimator.INFINITE
rotation.interpolator = AccelerateDecelerateInterpolator()
rotation.start()
}
}
private fun provideFeedback(effect: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
vibrator?.vibrate(VibrationEffect.createPredefined(effect))
} else {
@Suppress("DEPRECATION")
vibrator?.vibrate(50)
}
}
// === Utility Methods ===
private fun dpToPx(dp: Int): Int {
return (dp * context.resources.displayMetrics.density).toInt()
}
private fun getColor(colorRes: Int): Int {
return ContextCompat.getColor(context, colorRes)
}
private fun lightenColor(color: Int, factor: Float): Int {
val red = Color.red(color)
val green = Color.green(color)
val blue = Color.blue(color)
return Color.rgb(
(red + (255 - red) * factor).toInt().coerceAtMost(255),
(green + (255 - green) * factor).toInt().coerceAtMost(255),
(blue + (255 - blue) * factor).toInt().coerceAtMost(255)
)
}
private fun darkenColor(color: Int, factor: Float): Int {
val red = Color.red(color)
val green = Color.green(color)
val blue = Color.blue(color)
return Color.rgb(
(red * (1 - factor)).toInt().coerceAtLeast(0),
(green * (1 - factor)).toInt().coerceAtLeast(0),
(blue * (1 - factor)).toInt().coerceAtLeast(0)
)
}
/**
* Data class for FAB menu items
*/
private data class FabMenuItem(
val icon: String,
val label: String,
val color: Int,
val action: () -> Unit
)
}

352
app/src/main/java/com/quillstudios/pokegoalshelper/ui/FloatingComposeOverlay.kt

@ -1,352 +0,0 @@
package com.quillstudios.pokegoalshelper.ui
import android.content.Context
import android.graphics.PixelFormat
import android.os.Build
import android.util.Log
import android.view.Gravity
import android.view.View
import android.view.WindowManager
import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
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.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.quillstudios.pokegoalshelper.ui.theme.PokeGoalsHelperTheme
/**
* True floating overlay using WindowManager + ComposeView.
* This creates a proper system overlay that floats over all apps.
*/
class FloatingComposeOverlay(
private val context: Context,
private val onDetectionRequested: () -> Unit,
private val onClassFilterRequested: (String?) -> Unit,
private val onDebugToggled: () -> Unit,
private val onClose: () -> Unit
) {
companion object {
private const val TAG = "FloatingComposeOverlay"
}
private var windowManager: WindowManager? = null
private var overlayView: ComposeView? = null
private var isShowing = false
// Mutable state for Compose
private var fabPosition = mutableStateOf(Offset.Zero)
fun show() {
if (isShowing) return
try {
windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
createOverlayView()
isShowing = true
Log.d(TAG, "✅ Floating Compose overlay shown")
} catch (e: Exception) {
Log.e(TAG, "❌ Error showing floating overlay", e)
}
}
fun hide() {
if (!isShowing) return
try {
overlayView?.let { windowManager?.removeView(it) }
overlayView = null
windowManager = null
isShowing = false
Log.d(TAG, "🗑️ Floating Compose overlay hidden")
} catch (e: Exception) {
Log.e(TAG, "❌ Error hiding floating overlay", e)
}
}
private fun createOverlayView() {
overlayView = ComposeView(context).apply {
setContent {
PokeGoalsHelperTheme {
FloatingFABContent(
position = fabPosition.value,
onPositionChanged = { newPosition ->
fabPosition.value = newPosition
updateWindowPosition(newPosition)
},
onDetectionRequested = onDetectionRequested,
onClassFilterRequested = onClassFilterRequested,
onDebugToggled = onDebugToggled,
onClose = onClose
)
}
}
}
// Window parameters for true floating behavior
val params = WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
} else {
@Suppress("DEPRECATION")
WindowManager.LayoutParams.TYPE_PHONE
},
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
PixelFormat.TRANSLUCENT
).apply {
gravity = Gravity.TOP or Gravity.START
x = 50 // Initial position
y = 200
}
windowManager?.addView(overlayView, params)
}
private fun updateWindowPosition(position: Offset) {
overlayView?.let { view ->
val params = view.layoutParams as WindowManager.LayoutParams
params.x = position.x.toInt()
params.y = position.y.toInt()
try {
windowManager?.updateViewLayout(view, params)
} catch (e: Exception) {
Log.w(TAG, "Failed to update window position", e)
}
}
}
}
@Composable
fun FloatingFABContent(
position: Offset,
onPositionChanged: (Offset) -> Unit,
onDetectionRequested: () -> Unit,
onClassFilterRequested: (String?) -> Unit,
onDebugToggled: () -> Unit,
onClose: () -> Unit
) {
var isMenuExpanded by remember { mutableStateOf(false) }
var isProcessing by remember { mutableStateOf(false) }
val hapticFeedback = LocalHapticFeedback.current
// Animation states
val rotation by animateFloatAsState(
targetValue = if (isMenuExpanded) 45f else 0f,
animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy),
label = "FAB rotation"
)
val processingRotation by animateFloatAsState(
targetValue = if (isProcessing) 360f else 0f,
animationSpec = infiniteRepeatable(
animation = tween(1000, easing = LinearEasing),
repeatMode = RepeatMode.Restart
),
label = "Processing rotation"
)
Column(
horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.Bottom
) {
// Expanded Menu Items
AnimatedVisibility(
visible = isMenuExpanded,
enter = fadeIn(animationSpec = tween(300)) +
slideInVertically(
initialOffsetY = { it / 2 },
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
),
exit = fadeOut(animationSpec = tween(200)) +
slideOutVertically(
targetOffsetY = { it / 2 },
animationSpec = tween(200)
)
) {
Column(
horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.padding(bottom = 16.dp)
) {
// Menu FAB items
OverlayMenuFABItem(
icon = Icons.Default.Search,
label = "DETECT",
containerColor = MaterialTheme.colorScheme.primary,
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
onDetectionRequested()
isMenuExpanded = false
}
)
OverlayMenuFABItem(
icon = Icons.Default.Star,
label = "SHINY",
containerColor = MaterialTheme.colorScheme.tertiary,
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
onClassFilterRequested("shiny_icon")
onDetectionRequested()
isMenuExpanded = false
}
)
OverlayMenuFABItem(
icon = Icons.Default.AccountCircle,
label = "POKEBALL",
containerColor = MaterialTheme.colorScheme.error,
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
onClassFilterRequested("ball_icon_cherishball")
onDetectionRequested()
isMenuExpanded = false
}
)
OverlayMenuFABItem(
icon = Icons.Default.List,
label = "ALL",
containerColor = MaterialTheme.colorScheme.secondary,
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
onClassFilterRequested(null)
onDetectionRequested()
isMenuExpanded = false
}
)
OverlayMenuFABItem(
icon = Icons.Default.Build,
label = "DEBUG",
containerColor = MaterialTheme.colorScheme.outline,
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
onDebugToggled()
onDetectionRequested()
isMenuExpanded = false
}
)
}
}
// Main FAB with drag support
FloatingActionButton(
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
if (!isProcessing) {
isMenuExpanded = !isMenuExpanded
}
},
modifier = Modifier
.size(56.dp)
.rotate(if (isProcessing) processingRotation else rotation)
.pointerInput(Unit) {
detectDragGestures(
onDragStart = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.TextHandleMove)
},
onDrag = { _, dragAmount ->
val newPosition = Offset(
position.x + dragAmount.x,
position.y + dragAmount.y
)
onPositionChanged(newPosition)
}
)
},
containerColor = when {
isProcessing -> MaterialTheme.colorScheme.tertiary
isMenuExpanded -> MaterialTheme.colorScheme.error
else -> MaterialTheme.colorScheme.primary
},
elevation = FloatingActionButtonDefaults.elevation(
defaultElevation = 6.dp,
pressedElevation = 12.dp
)
) {
Icon(
imageVector = when {
isProcessing -> Icons.Default.Refresh
isMenuExpanded -> Icons.Default.Close
else -> Icons.Default.Home
},
contentDescription = when {
isProcessing -> "Processing..."
isMenuExpanded -> "Close menu"
else -> "Open detection menu"
},
tint = MaterialTheme.colorScheme.onPrimary
)
}
}
}
@Composable
fun OverlayMenuFABItem(
icon: ImageVector,
label: String,
containerColor: Color,
onClick: () -> Unit
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
// Label
Surface(
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.9f),
shape = MaterialTheme.shapes.small,
shadowElevation = 2.dp
) {
Text(
text = label,
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
color = MaterialTheme.colorScheme.onSurface,
fontSize = 12.sp
)
}
// Mini FAB
FloatingActionButton(
onClick = onClick,
modifier = Modifier.size(40.dp),
containerColor = containerColor,
elevation = FloatingActionButtonDefaults.elevation(
defaultElevation = 4.dp,
pressedElevation = 8.dp
)
) {
Icon(
imageVector = icon,
contentDescription = label,
tint = Color.White,
modifier = Modifier.size(20.dp)
)
}
}
}

270
app/src/main/java/com/quillstudios/pokegoalshelper/ui/FloatingOrbUI.kt

@ -1,270 +0,0 @@
package com.quillstudios.pokegoalshelper.ui
import android.content.Context
import android.graphics.PixelFormat
import android.os.Build
import android.util.Log
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.Button
import android.widget.LinearLayout
import com.quillstudios.pokegoalshelper.ui.interfaces.DetectionUIEvents
import com.quillstudios.pokegoalshelper.ui.interfaces.DetectionUICallbacks
/**
* Floating orb UI component that handles user interactions.
* Implements CalcIV-style expandable menu system.
*
* This is pure UI logic - no business logic or direct detector calls.
*/
class FloatingOrbUI(
private val context: Context,
private val detectionEvents: DetectionUIEvents
) : DetectionUICallbacks {
companion object {
private const val TAG = "FloatingOrbUI"
private const val ORB_SIZE = 120
private const val MENU_BUTTON_WIDTH = 160
private const val MENU_BUTTON_HEIGHT = 60
}
private var windowManager: WindowManager? = null
private var orbButton: View? = null
private var expandedMenu: View? = null
private var isMenuExpanded = false
private var isProcessing = false
/**
* Initialize and show the floating orb
*/
fun show() {
try {
if (orbButton != null) return // Already shown
windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
createFloatingOrb()
Log.d(TAG, "✅ Floating orb UI shown")
} catch (e: Exception) {
Log.e(TAG, "❌ Error showing floating orb", e)
}
}
/**
* Hide and cleanup the floating orb
*/
fun hide() {
try {
if (isMenuExpanded) {
collapseMenu()
}
orbButton?.let {
windowManager?.removeView(it)
orbButton = null
}
windowManager = null
Log.d(TAG, "🗑️ Floating orb UI hidden")
} catch (e: Exception) {
Log.e(TAG, "❌ Error hiding floating orb", e)
}
}
// === DetectionUICallbacks Implementation ===
override fun onDetectionStarted() {
isProcessing = true
updateOrbAppearance()
// Auto-collapse menu during processing
if (isMenuExpanded) {
collapseMenu()
}
}
override fun onDetectionCompleted(detectionCount: Int) {
isProcessing = false
updateOrbAppearance()
Log.d(TAG, "🎯 Detection completed: $detectionCount objects")
}
override fun onDetectionFailed(error: String) {
isProcessing = false
updateOrbAppearance()
Log.e(TAG, "❌ Detection failed: $error")
}
override fun onSettingsChanged(filterClass: String?, debugMode: Boolean, coordinateMode: String) {
Log.d(TAG, "⚙️ Settings updated - Filter: $filterClass, Debug: $debugMode, Mode: $coordinateMode")
// UI could update visual indicators here if needed
}
// === Private UI Methods ===
private fun createFloatingOrb() {
orbButton = Button(context).apply {
text = "🎯"
textSize = 20f
setBackgroundResource(android.R.drawable.btn_default)
background.setTint(0xFF4CAF50.toInt()) // Green
setTextColor(0xFFFFFFFF.toInt())
width = ORB_SIZE
height = ORB_SIZE
layoutParams = ViewGroup.LayoutParams(ORB_SIZE, ORB_SIZE)
setOnClickListener { handleOrbClick() }
}
val params = WindowManager.LayoutParams(
ORB_SIZE, ORB_SIZE,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
} else {
@Suppress("DEPRECATION")
WindowManager.LayoutParams.TYPE_PHONE
},
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT
).apply {
gravity = Gravity.TOP or Gravity.START
x = 50
y = 200
}
windowManager?.addView(orbButton, params)
}
private fun handleOrbClick() {
if (isProcessing) {
Log.d(TAG, "⚠️ Ignoring click - detection in progress")
return
}
if (isMenuExpanded) {
collapseMenu()
} else {
expandMenu()
}
}
private fun expandMenu() {
if (isMenuExpanded || isProcessing) return
val menuContainer = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
setBackgroundColor(0xE0000000.toInt()) // Semi-transparent black
setPadding(16, 16, 16, 16)
}
// Define menu options with their actions
val menuItems = listOf(
MenuOption("🔍 DETECT", 0xFF4CAF50.toInt()) {
detectionEvents.onDetectionRequested()
},
MenuOption("SHINY", 0xFFFFD700.toInt()) {
detectionEvents.onClassFilterChanged("shiny_icon")
detectionEvents.onDetectionRequested()
},
MenuOption("POKEBALL", 0xFFE91E63.toInt()) {
detectionEvents.onClassFilterChanged("ball_icon_cherishball")
detectionEvents.onDetectionRequested()
},
MenuOption("ALL", 0xFF607D8B.toInt()) {
detectionEvents.onClassFilterChanged(null)
detectionEvents.onDetectionRequested()
},
MenuOption("DEBUG", 0xFFFF5722.toInt()) {
detectionEvents.onDebugModeToggled()
detectionEvents.onDetectionRequested()
}
)
menuItems.forEach { option ->
val button = createMenuButton(option)
menuContainer.addView(button)
}
expandedMenu = menuContainer
val params = WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
} else {
@Suppress("DEPRECATION")
WindowManager.LayoutParams.TYPE_PHONE
},
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT
).apply {
gravity = Gravity.TOP or Gravity.START
x = 180 // Position next to the orb
y = 200
}
windowManager?.addView(expandedMenu, params)
isMenuExpanded = true
updateOrbAppearance()
}
private fun collapseMenu() {
if (!isMenuExpanded) return
expandedMenu?.let { windowManager?.removeView(it) }
expandedMenu = null
isMenuExpanded = false
updateOrbAppearance()
}
private fun createMenuButton(option: MenuOption): Button {
return Button(context).apply {
text = option.text
textSize = 14f // Increased text size
setBackgroundColor(option.color)
setTextColor(0xFFFFFFFF.toInt())
setPadding(8, 4, 8, 4) // Add padding for better text spacing
layoutParams = LinearLayout.LayoutParams(MENU_BUTTON_WIDTH, MENU_BUTTON_HEIGHT).apply {
setMargins(0, 0, 0, 8)
}
setOnClickListener {
option.action()
collapseMenu()
}
}
}
private fun updateOrbAppearance() {
(orbButton as? Button)?.apply {
when {
isProcessing -> {
text = ""
background.setTint(0xFFFF9800.toInt()) // Orange
}
isMenuExpanded -> {
text = ""
background.setTint(0xFFFF5722.toInt()) // Orange-red
}
else -> {
text = "🎯"
background.setTint(0xFF4CAF50.toInt()) // Green
}
}
}
}
/**
* Data class for menu options
*/
private data class MenuOption(
val text: String,
val color: Int,
val action: () -> Unit
)
}

444
app/src/main/java/com/quillstudios/pokegoalshelper/ui/FloatingUIActivity.kt

@ -1,444 +0,0 @@
package com.quillstudios.pokegoalshelper.ui
import android.app.Activity
import android.content.*
import android.os.Bundle
import android.os.IBinder
import android.util.Log
import android.view.WindowManager
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
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
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.quillstudios.pokegoalshelper.ScreenCaptureService
import com.quillstudios.pokegoalshelper.ui.theme.PokeGoalsHelperTheme
/**
* Transparent Activity that hosts the floating Material 3 FAB UI.
* Communicates with ScreenCaptureService via Binder for detection functionality.
*
* This approach allows us to use true Jetpack Compose with Material 3 components
* while keeping the detection logic in a background service.
*/
class FloatingUIActivity : ComponentActivity() {
companion object {
private const val TAG = "FloatingUIActivity"
const val ACTION_SHOW_FAB = "SHOW_FAB"
const val ACTION_HIDE_FAB = "HIDE_FAB"
}
private var screenCaptureService: ScreenCaptureService? = null
private var serviceBound = false
// Service connection to communicate with ScreenCaptureService
private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
Log.d(TAG, "Connected to ScreenCaptureService")
val binder = service as ScreenCaptureService.LocalBinder
screenCaptureService = binder.getService()
serviceBound = true
}
override fun onServiceDisconnected(arg0: ComponentName) {
Log.d(TAG, "Disconnected from ScreenCaptureService")
screenCaptureService = null
serviceBound = false
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Bind to the ScreenCaptureService
bindToScreenCaptureService()
setContent {
PokeGoalsHelperTheme {
FloatingFABInterface(
onDetectionRequested = { requestDetection() },
onClassFilterRequested = { className -> requestClassFilter(className) },
onDebugToggled = { toggleDebugMode() },
onClose = { finish() }
)
}
}
}
override fun onResume() {
super.onResume()
// Setup overlay after window is fully initialized
setupTransparentOverlay()
}
override fun onDestroy() {
super.onDestroy()
if (serviceBound) {
unbindService(serviceConnection)
serviceBound = false
}
}
private fun setupTransparentOverlay() {
try {
// 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
)
setFlags(
WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
)
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY)
} else {
@Suppress("DEPRECATION")
setType(WindowManager.LayoutParams.TYPE_PHONE)
}
}
} catch (e: Exception) {
Log.e(TAG, "Error setting up transparent overlay", e)
// Continue without overlay setup if it fails
}
}
private fun bindToScreenCaptureService() {
val intent = Intent(this, ScreenCaptureService::class.java)
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
}
// === Communication with Service ===
private fun requestDetection() {
screenCaptureService?.triggerDetection()
Log.d(TAG, "Requested detection from service")
}
private fun requestClassFilter(className: String?) {
screenCaptureService?.setClassFilter(className)
Log.d(TAG, "Set class filter to: $className")
}
private fun toggleDebugMode() {
screenCaptureService?.toggleDebugMode()
Log.d(TAG, "Toggled debug mode")
}
}
@Composable
fun FloatingFABInterface(
onDetectionRequested: () -> Unit,
onClassFilterRequested: (String?) -> Unit,
onDebugToggled: () -> Unit,
onClose: () -> Unit
) {
var isMenuExpanded by remember { mutableStateOf(false) }
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)
) {
// Main content area - transparent to allow touches through
Spacer(modifier = Modifier.fillMaxSize())
// FAB and menu area - positioned with offset
Column(
horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.Bottom,
modifier = Modifier
.offset {
IntOffset(
fabOffset.x.toInt(),
fabOffset.y.toInt()
)
}
.padding(16.dp)
) {
// Expanded Menu Items
AnimatedVisibility(
visible = isMenuExpanded,
enter = fadeIn(animationSpec = tween(300)) +
slideInVertically(
initialOffsetY = { it / 2 },
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
),
exit = fadeOut(animationSpec = tween(200)) +
slideOutVertically(
targetOffsetY = { it / 2 },
animationSpec = tween(200)
)
) {
Column(
horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.padding(bottom = 16.dp)
) {
// Menu FAB items
MenuFABItem(
icon = Icons.Default.Search,
label = "DETECT",
containerColor = MaterialTheme.colorScheme.primary,
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
onDetectionRequested()
isMenuExpanded = false
}
)
MenuFABItem(
icon = Icons.Default.Star,
label = "SHINY",
containerColor = MaterialTheme.colorScheme.tertiary,
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
onClassFilterRequested("shiny_icon")
onDetectionRequested()
isMenuExpanded = false
}
)
MenuFABItem(
icon = Icons.Default.AccountCircle,
label = "POKEBALL",
containerColor = MaterialTheme.colorScheme.error,
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
onClassFilterRequested("ball_icon_cherishball")
onDetectionRequested()
isMenuExpanded = false
}
)
MenuFABItem(
icon = Icons.Default.List,
label = "ALL",
containerColor = MaterialTheme.colorScheme.secondary,
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
onClassFilterRequested(null)
onDetectionRequested()
isMenuExpanded = false
}
)
MenuFABItem(
icon = Icons.Default.Build,
label = "DEBUG",
containerColor = MaterialTheme.colorScheme.outline,
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
onDebugToggled()
onDetectionRequested()
isMenuExpanded = false
}
)
}
}
// Main FAB
MainFloatingActionButton(
isProcessing = isProcessing,
isMenuExpanded = isMenuExpanded,
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
if (!isProcessing) {
isMenuExpanded = !isMenuExpanded
}
},
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
}
)
}
}
}
@Composable
fun MainFloatingActionButton(
isProcessing: Boolean,
isMenuExpanded: Boolean,
onClick: () -> Unit,
onLongClick: () -> Unit,
onDrag: (Offset) -> Unit
) {
val rotation by animateFloatAsState(
targetValue = if (isMenuExpanded) 45f else 0f,
animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy),
label = "FAB rotation"
)
val processingRotation by animateFloatAsState(
targetValue = if (isProcessing) 360f else 0f,
animationSpec = infiniteRepeatable(
animation = tween(1000, easing = LinearEasing),
repeatMode = RepeatMode.Restart
),
label = "Processing rotation"
)
FloatingActionButton(
onClick = onClick,
modifier = Modifier
.size(56.dp)
.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
else -> MaterialTheme.colorScheme.primary
},
elevation = FloatingActionButtonDefaults.elevation(
defaultElevation = 6.dp,
pressedElevation = 12.dp
)
) {
Icon(
imageVector = when {
isProcessing -> Icons.Default.Refresh
isMenuExpanded -> Icons.Default.Close
else -> Icons.Default.Home
},
contentDescription = when {
isProcessing -> "Processing..."
isMenuExpanded -> "Close menu"
else -> "Open detection menu"
},
tint = MaterialTheme.colorScheme.onPrimary
)
}
}
@Composable
fun MenuFABItem(
icon: ImageVector,
label: String,
containerColor: Color,
onClick: () -> Unit
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
// Label
Surface(
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.9f),
shape = MaterialTheme.shapes.small,
shadowElevation = 2.dp
) {
Text(
text = label,
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
color = MaterialTheme.colorScheme.onSurface,
fontSize = 12.sp
)
}
// Mini FAB
FloatingActionButton(
onClick = onClick,
modifier = Modifier.size(40.dp),
containerColor = containerColor,
elevation = FloatingActionButtonDefaults.elevation(
defaultElevation = 4.dp,
pressedElevation = 8.dp
)
) {
Icon(
imageVector = icon,
contentDescription = label,
tint = Color.White,
modifier = Modifier.size(20.dp)
)
}
}
}
Loading…
Cancel
Save