From c9c7ac066de916348a74f7d1e7a65daa5630d14e Mon Sep 17 00:00:00 2001 From: Quildra Date: Fri, 1 Aug 2025 13:43:10 +0100 Subject: [PATCH] feat: implement separate window approach for floating FAB menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../pokegoalshelper/ui/EnhancedFloatingFAB.kt | 243 +++++++++++------- 1 file changed, 151 insertions(+), 92 deletions(-) diff --git a/app/src/main/java/com/quillstudios/pokegoalshelper/ui/EnhancedFloatingFAB.kt b/app/src/main/java/com/quillstudios/pokegoalshelper/ui/EnhancedFloatingFAB.kt index bc7dec8..ac49cd0 100644 --- a/app/src/main/java/com/quillstudios/pokegoalshelper/ui/EnhancedFloatingFAB.kt +++ b/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) } }