Browse Source

feat: implement separate window approach for floating FAB menu

- Replace single window with separate WindowManager windows for FAB and menu
- Fix main FAB movement issue by using independent window positioning
- Calculate actual menu width instead of estimating for precise positioning
- Menu now appears to the left of FAB with proper 8dp spacing
- Remove obsolete updateRootLayout() function and auto-hide behavior
- Add debug logging for position calculations

The separate windows approach eliminates layout interference between
the main FAB and expandable menu, ensuring the FAB stays fixed while
the menu expands leftward as requested.

🤖 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
c9c7ac066d
  1. 243
      app/src/main/java/com/quillstudios/pokegoalshelper/ui/EnhancedFloatingFAB.kt

243
app/src/main/java/com/quillstudios/pokegoalshelper/ui/EnhancedFloatingFAB.kt

@ -51,9 +51,12 @@ class EnhancedFloatingFAB(
}
private var windowManager: WindowManager? = null
private var rootView: ViewGroup? = null
private var mainFAB: ImageButton? = null
private var menuContainer: LinearLayout? = null
// Separate window parameters
private var fabParams: WindowManager.LayoutParams? = null
private var menuParams: WindowManager.LayoutParams? = null
private var isShowing = false
private var isMenuExpanded = false
private var isDragging = false
@ -87,10 +90,18 @@ class EnhancedFloatingFAB(
if (!isShowing) return
try {
rootView?.let { windowManager?.removeView(it) }
rootView = null
// Remove menu window if showing
if (isMenuExpanded) {
menuContainer?.let { windowManager?.removeView(it) }
}
// Remove main FAB window
mainFAB?.let { windowManager?.removeView(it) }
mainFAB = null
menuContainer = null
fabParams = null
menuParams = null
windowManager = null
isShowing = false
handler.removeCallbacksAndMessages(null)
@ -101,39 +112,28 @@ class EnhancedFloatingFAB(
}
private fun createFloatingView() {
// Create root container - sized exactly to content for precise touch handling
rootView = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
gravity = Gravity.END or Gravity.BOTTOM
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
}
// Create main FAB as separate window
mainFAB = createMainFAB()
// Create expandable menu container
// Create menu container as separate window
menuContainer = createMenuContainer()
rootView?.addView(menuContainer)
// Create main FAB
mainFAB = createMainFAB()
rootView?.addView(mainFAB)
updateMenuLayout()
// Set up window parameters for true floating
// Set up initial position
val screenSize = getScreenSize()
currentX = screenSize.first - dpToPx(FAB_SIZE_DP + 16) // Start on right edge
currentY = screenSize.second / 2 // Center vertically
val params = WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
// Create window parameters for main FAB
fabParams = WindowManager.LayoutParams(
dpToPx(FAB_SIZE_DP),
dpToPx(FAB_SIZE_DP),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
} else {
@Suppress("DEPRECATION")
WindowManager.LayoutParams.TYPE_PHONE
},
// Critical flags for touch-through behavior
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
@ -144,10 +144,33 @@ class EnhancedFloatingFAB(
y = this@EnhancedFloatingFAB.currentY
}
windowManager?.addView(rootView, params)
// Create window parameters for menu (initially hidden)
menuParams = 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_NO_LIMITS,
PixelFormat.TRANSLUCENT
).apply {
gravity = Gravity.TOP or Gravity.START
// Position menu to the left of FAB with proper spacing
val menuWidth = 200
val padding = dpToPx(8) // 8dp padding between menu and FAB
// Menu's right edge should align with FAB's left edge minus padding
x = maxOf(0, this@EnhancedFloatingFAB.currentX - menuWidth - padding)
y = this@EnhancedFloatingFAB.currentY // Same vertical position as FAB
}
// Start auto-hide timer
scheduleAutoHide()
// Add main FAB window
windowManager?.addView(mainFAB, fabParams)
}
private fun createMainFAB(): ImageButton {
@ -176,11 +199,10 @@ class EnhancedFloatingFAB(
private fun createMenuContainer(): LinearLayout {
return LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
gravity = Gravity.END
visibility = View.GONE
gravity = Gravity.TOP or Gravity.END // Align menu items to top-right of container
visibility = View.VISIBLE // Changed from GONE to VISIBLE
// Add menu items
addMenuItems(this)
// Initial menu items will be added by updateMenuLayout()
layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
@ -189,32 +211,43 @@ class EnhancedFloatingFAB(
}
}
private fun addMenuItems(container: LinearLayout) {
val menuItems = listOf(
MenuItemData("DEBUG", android.R.drawable.ic_menu_info_details, android.R.color.holo_orange_dark) {
onDebugToggled()
onDetectionRequested()
},
MenuItemData("ALL", android.R.drawable.ic_menu_view, android.R.color.holo_green_dark) {
onClassFilterRequested(null)
onDetectionRequested()
},
MenuItemData("POKEBALL", android.R.drawable.ic_menu_mylocation, android.R.color.holo_red_dark) {
onClassFilterRequested("ball_icon_cherishball")
onDetectionRequested()
},
MenuItemData("SHINY", android.R.drawable.btn_star_big_on, android.R.color.holo_purple) {
onClassFilterRequested("shiny_icon")
onDetectionRequested()
},
MenuItemData("DETECT", android.R.drawable.ic_menu_search, android.R.color.holo_blue_dark) {
onDetectionRequested()
private fun updateMenuLayout() {
menuContainer?.let { container ->
// Clear existing items
container.removeAllViews()
// Menu items should align to the right edge of the menu container
// so they appear closest to the main FAB
(container as LinearLayout).gravity = Gravity.TOP or Gravity.END
// Add menu items with appropriate layout
val menuItems = listOf(
MenuItemData("DEBUG", android.R.drawable.ic_menu_info_details, android.R.color.holo_orange_dark) {
onDebugToggled()
onDetectionRequested()
},
MenuItemData("ALL", android.R.drawable.ic_menu_view, android.R.color.holo_green_dark) {
onClassFilterRequested(null)
onDetectionRequested()
},
MenuItemData("POKEBALL", android.R.drawable.ic_menu_mylocation, android.R.color.holo_red_dark) {
onClassFilterRequested("ball_icon_cherishball")
onDetectionRequested()
},
MenuItemData("SHINY", android.R.drawable.btn_star_big_on, android.R.color.holo_purple) {
onClassFilterRequested("shiny_icon")
onDetectionRequested()
},
MenuItemData("DETECT", android.R.drawable.ic_menu_search, android.R.color.holo_blue_dark) {
onDetectionRequested()
}
)
menuItems.forEach { item ->
val menuRow = createMenuRow(item)
container.addView(menuRow)
}
)
menuItems.forEach { item ->
val menuRow = createMenuRow(item)
container.addView(menuRow)
}
}
@ -234,7 +267,7 @@ class EnhancedFloatingFAB(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
).apply {
setMargins(0, 0, dpToPx(8), 0)
setMargins(0, 0, dpToPx(8), 0) // 8dp space between label and mini FAB
}
}
@ -249,7 +282,6 @@ class EnhancedFloatingFAB(
performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK)
item.action()
hideMenu()
scheduleAutoHide()
}
layoutParams = LinearLayout.LayoutParams(
@ -258,6 +290,7 @@ class EnhancedFloatingFAB(
)
}
// Always: LABEL → FAB (label on left, mini FAB on right, closest to main FAB)
addView(label)
addView(miniFAB)
@ -265,7 +298,7 @@ class EnhancedFloatingFAB(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
).apply {
setMargins(0, 0, 0, dpToPx(8))
setMargins(0, 0, 0, dpToPx(8)) // 8dp space between menu rows
}
}
}
@ -322,7 +355,7 @@ class EnhancedFloatingFAB(
MotionEvent.ACTION_UP -> {
if (isDragging) {
snapToEdgeIfNeeded()
scheduleAutoHide()
// scheduleAutoHide() // Disabled - no auto-hide after drag
} else {
// Handle click
performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK)
@ -331,7 +364,7 @@ class EnhancedFloatingFAB(
} else {
showMenu()
}
scheduleAutoHide()
// scheduleAutoHide() // Disabled - menu stays open until manually closed
}
isDragging = false
true
@ -343,14 +376,46 @@ class EnhancedFloatingFAB(
}
private fun updateWindowPosition() {
rootView?.let { view ->
val params = view.layoutParams as WindowManager.LayoutParams
params.x = currentX
params.y = currentY
try {
windowManager?.updateViewLayout(view, params)
} catch (e: Exception) {
Log.w(TAG, "Failed to update window position", e)
// Update main FAB position
mainFAB?.let { fab ->
fabParams?.let { params ->
params.x = currentX
params.y = currentY
try {
windowManager?.updateViewLayout(fab, params)
} catch (e: Exception) {
Log.w(TAG, "Failed to update FAB position", e)
}
}
}
// Update menu position if visible
if (isMenuExpanded) {
updateMenuPosition()
}
}
private fun updateMenuPosition() {
menuContainer?.let { menu ->
menuParams?.let { params ->
// Calculate actual menu width by measuring the container
menu.measure(
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
)
val menuWidth = menu.measuredWidth
val padding = dpToPx(8) // 8dp padding between menu and FAB
// Menu's right edge should align with FAB's left edge minus padding
params.x = maxOf(0, currentX - menuWidth - padding)
params.y = currentY // Same vertical position as FAB (top alignment)
Log.d(TAG, "Menu position: x=${params.x}, y=${params.y}, measured width=${menuWidth}, FAB at: x=$currentX, y=$currentY")
try {
windowManager?.updateViewLayout(menu, params)
} catch (e: Exception) {
Log.w(TAG, "Failed to update menu position", e)
}
}
}
}
@ -393,9 +458,21 @@ class EnhancedFloatingFAB(
private fun showMenu() {
if (isMenuExpanded) return
Log.d(TAG, "showMenu() called")
// Update menu layout for current position
updateMenuLayout()
// Update menu position
updateMenuPosition()
// Add menu window
menuContainer?.let { container ->
Log.d(TAG, "Adding menu container to WindowManager")
container.visibility = View.VISIBLE
windowManager?.addView(container, menuParams)
isMenuExpanded = true
Log.d(TAG, "Menu window added, isMenuExpanded = $isMenuExpanded")
// Animate menu items in
for (i in 0 until container.childCount) {
@ -426,30 +503,12 @@ class EnhancedFloatingFAB(
if (!isMenuExpanded) return
menuContainer?.let { container ->
// Animate menu items out
for (i in 0 until container.childCount) {
val child = container.getChildAt(i)
ObjectAnimator.ofFloat(child, "alpha", 1f, 0f).apply {
duration = MENU_ANIMATION_DURATION / 2
start()
}
ObjectAnimator.ofFloat(child, "translationY", 0f, dpToPx(20).toFloat()).apply {
duration = MENU_ANIMATION_DURATION / 2
interpolator = AccelerateDecelerateInterpolator()
if (i == container.childCount - 1) {
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
container.visibility = View.GONE
isMenuExpanded = false
}
})
}
start()
}
// Remove menu window
try {
windowManager?.removeView(container)
isMenuExpanded = false
} catch (e: Exception) {
Log.w(TAG, "Failed to remove menu window", e)
}
}

Loading…
Cancel
Save