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 windowManager: WindowManager? = null
private var rootView: ViewGroup? = null
private var mainFAB: ImageButton? = null private var mainFAB: ImageButton? = null
private var menuContainer: LinearLayout? = 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 isShowing = false
private var isMenuExpanded = false private var isMenuExpanded = false
private var isDragging = false private var isDragging = false
@ -87,10 +90,18 @@ class EnhancedFloatingFAB(
if (!isShowing) return if (!isShowing) return
try { try {
rootView?.let { windowManager?.removeView(it) } // Remove menu window if showing
rootView = null if (isMenuExpanded) {
menuContainer?.let { windowManager?.removeView(it) }
}
// Remove main FAB window
mainFAB?.let { windowManager?.removeView(it) }
mainFAB = null mainFAB = null
menuContainer = null menuContainer = null
fabParams = null
menuParams = null
windowManager = null windowManager = null
isShowing = false isShowing = false
handler.removeCallbacksAndMessages(null) handler.removeCallbacksAndMessages(null)
@ -101,39 +112,28 @@ class EnhancedFloatingFAB(
} }
private fun createFloatingView() { private fun createFloatingView() {
// Create root container - sized exactly to content for precise touch handling // Create main FAB as separate window
rootView = LinearLayout(context).apply { mainFAB = createMainFAB()
orientation = LinearLayout.VERTICAL
gravity = Gravity.END or Gravity.BOTTOM
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
}
// Create expandable menu container // Create menu container as separate window
menuContainer = createMenuContainer() menuContainer = createMenuContainer()
rootView?.addView(menuContainer) updateMenuLayout()
// Create main FAB
mainFAB = createMainFAB()
rootView?.addView(mainFAB)
// Set up window parameters for true floating // Set up initial position
val screenSize = getScreenSize() val screenSize = getScreenSize()
currentX = screenSize.first - dpToPx(FAB_SIZE_DP + 16) // Start on right edge currentX = screenSize.first - dpToPx(FAB_SIZE_DP + 16) // Start on right edge
currentY = screenSize.second / 2 // Center vertically currentY = screenSize.second / 2 // Center vertically
val params = WindowManager.LayoutParams( // Create window parameters for main FAB
WindowManager.LayoutParams.WRAP_CONTENT, fabParams = WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT, dpToPx(FAB_SIZE_DP),
dpToPx(FAB_SIZE_DP),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
} else { } else {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
WindowManager.LayoutParams.TYPE_PHONE WindowManager.LayoutParams.TYPE_PHONE
}, },
// Critical flags for touch-through behavior
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
@ -144,10 +144,33 @@ class EnhancedFloatingFAB(
y = this@EnhancedFloatingFAB.currentY 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 // Add main FAB window
scheduleAutoHide() windowManager?.addView(mainFAB, fabParams)
} }
private fun createMainFAB(): ImageButton { private fun createMainFAB(): ImageButton {
@ -176,11 +199,10 @@ class EnhancedFloatingFAB(
private fun createMenuContainer(): LinearLayout { private fun createMenuContainer(): LinearLayout {
return LinearLayout(context).apply { return LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL orientation = LinearLayout.VERTICAL
gravity = Gravity.END gravity = Gravity.TOP or Gravity.END // Align menu items to top-right of container
visibility = View.GONE visibility = View.VISIBLE // Changed from GONE to VISIBLE
// Add menu items // Initial menu items will be added by updateMenuLayout()
addMenuItems(this)
layoutParams = LinearLayout.LayoutParams( layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT,
@ -189,32 +211,43 @@ class EnhancedFloatingFAB(
} }
} }
private fun addMenuItems(container: LinearLayout) {
val menuItems = listOf( private fun updateMenuLayout() {
MenuItemData("DEBUG", android.R.drawable.ic_menu_info_details, android.R.color.holo_orange_dark) { menuContainer?.let { container ->
onDebugToggled() // Clear existing items
onDetectionRequested() container.removeAllViews()
},
MenuItemData("ALL", android.R.drawable.ic_menu_view, android.R.color.holo_green_dark) { // Menu items should align to the right edge of the menu container
onClassFilterRequested(null) // so they appear closest to the main FAB
onDetectionRequested() (container as LinearLayout).gravity = Gravity.TOP or Gravity.END
},
MenuItemData("POKEBALL", android.R.drawable.ic_menu_mylocation, android.R.color.holo_red_dark) { // Add menu items with appropriate layout
onClassFilterRequested("ball_icon_cherishball") val menuItems = listOf(
onDetectionRequested() MenuItemData("DEBUG", android.R.drawable.ic_menu_info_details, android.R.color.holo_orange_dark) {
}, onDebugToggled()
MenuItemData("SHINY", android.R.drawable.btn_star_big_on, android.R.color.holo_purple) { onDetectionRequested()
onClassFilterRequested("shiny_icon") },
onDetectionRequested() MenuItemData("ALL", android.R.drawable.ic_menu_view, android.R.color.holo_green_dark) {
}, onClassFilterRequested(null)
MenuItemData("DETECT", android.R.drawable.ic_menu_search, android.R.color.holo_blue_dark) { onDetectionRequested()
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,
ViewGroup.LayoutParams.WRAP_CONTENT ViewGroup.LayoutParams.WRAP_CONTENT
).apply { ).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) performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK)
item.action() item.action()
hideMenu() hideMenu()
scheduleAutoHide()
} }
layoutParams = LinearLayout.LayoutParams( 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(label)
addView(miniFAB) addView(miniFAB)
@ -265,7 +298,7 @@ class EnhancedFloatingFAB(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT ViewGroup.LayoutParams.WRAP_CONTENT
).apply { ).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 -> { MotionEvent.ACTION_UP -> {
if (isDragging) { if (isDragging) {
snapToEdgeIfNeeded() snapToEdgeIfNeeded()
scheduleAutoHide() // scheduleAutoHide() // Disabled - no auto-hide after drag
} else { } else {
// Handle click // Handle click
performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK) performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK)
@ -331,7 +364,7 @@ class EnhancedFloatingFAB(
} else { } else {
showMenu() showMenu()
} }
scheduleAutoHide() // scheduleAutoHide() // Disabled - menu stays open until manually closed
} }
isDragging = false isDragging = false
true true
@ -343,14 +376,46 @@ class EnhancedFloatingFAB(
} }
private fun updateWindowPosition() { private fun updateWindowPosition() {
rootView?.let { view -> // Update main FAB position
val params = view.layoutParams as WindowManager.LayoutParams mainFAB?.let { fab ->
params.x = currentX fabParams?.let { params ->
params.y = currentY params.x = currentX
try { params.y = currentY
windowManager?.updateViewLayout(view, params) try {
} catch (e: Exception) { windowManager?.updateViewLayout(fab, params)
Log.w(TAG, "Failed to update window position", e) } 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() { private fun showMenu() {
if (isMenuExpanded) return 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 -> menuContainer?.let { container ->
Log.d(TAG, "Adding menu container to WindowManager")
container.visibility = View.VISIBLE container.visibility = View.VISIBLE
windowManager?.addView(container, menuParams)
isMenuExpanded = true isMenuExpanded = true
Log.d(TAG, "Menu window added, isMenuExpanded = $isMenuExpanded")
// Animate menu items in // Animate menu items in
for (i in 0 until container.childCount) { for (i in 0 until container.childCount) {
@ -426,30 +503,12 @@ class EnhancedFloatingFAB(
if (!isMenuExpanded) return if (!isMenuExpanded) return
menuContainer?.let { container -> menuContainer?.let { container ->
// Animate menu items out // Remove menu window
for (i in 0 until container.childCount) { try {
val child = container.getChildAt(i) windowManager?.removeView(container)
isMenuExpanded = false
ObjectAnimator.ofFloat(child, "alpha", 1f, 0f).apply { } catch (e: Exception) {
duration = MENU_ANIMATION_DURATION / 2 Log.w(TAG, "Failed to remove menu window", e)
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()
}
} }
} }

Loading…
Cancel
Save