Browse Source
- 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
7 changed files with 2 additions and 1667 deletions
@ -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 |
|
||||
) |
|
||||
} |
|
||||
@ -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) |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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 |
|
||||
) |
|
||||
} |
|
||||
@ -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…
Reference in new issue