mirror of
https://github.com/cmclark00/mintris.git
synced 2025-05-18 02:45:20 +01:00
more gamepad support
This commit is contained in:
parent
7e4423efce
commit
7d7090d7ea
5 changed files with 818 additions and 254 deletions
|
@ -43,6 +43,8 @@ import androidx.core.content.ContextCompat
|
|||
import android.widget.Button
|
||||
import android.widget.ScrollView
|
||||
import android.widget.ImageButton
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.widget.TextView
|
||||
|
||||
class MainActivity : AppCompatActivity(),
|
||||
GamepadController.GamepadConnectionListener,
|
||||
|
@ -552,18 +554,123 @@ class MainActivity : AppCompatActivity(),
|
|||
*/
|
||||
private fun showPauseMenu() {
|
||||
binding.pauseContainer.visibility = View.VISIBLE
|
||||
binding.pauseStartButton.visibility = View.VISIBLE
|
||||
binding.resumeButton.visibility = View.GONE
|
||||
|
||||
// Set button visibility based on game state
|
||||
if (gameView.isPaused) {
|
||||
binding.resumeButton.visibility = View.VISIBLE
|
||||
binding.pauseStartButton.visibility = View.GONE
|
||||
} else {
|
||||
// This case might happen if pause is triggered before game start (e.g., from title)
|
||||
binding.resumeButton.visibility = View.GONE
|
||||
binding.pauseStartButton.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
// Check if we're in landscape mode (used for finding specific views, not for order)
|
||||
val isLandscape = resources.configuration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE
|
||||
|
||||
// Update level badge
|
||||
binding.pauseLevelBadge?.setLevel(progressionManager.getPlayerLevel())
|
||||
binding.pauseLevelBadge?.setThemeColor(getThemeColor(currentTheme))
|
||||
|
||||
// Get theme color
|
||||
val textColor = getThemeColor(currentTheme)
|
||||
// Get theme colors
|
||||
val themeColor = getThemeColor(currentTheme)
|
||||
val backgroundColor = getThemeBackgroundColor(currentTheme)
|
||||
|
||||
// Apply theme color to pause container background
|
||||
val backgroundColor = when (currentTheme) {
|
||||
// Apply background color to pause container
|
||||
binding.pauseContainer.setBackgroundColor(backgroundColor)
|
||||
|
||||
// Apply theme colors to buttons and toggles
|
||||
binding.pauseStartButton.setTextColor(themeColor)
|
||||
binding.pauseRestartButton.setTextColor(themeColor)
|
||||
binding.resumeButton.setTextColor(themeColor)
|
||||
binding.highScoresButton.setTextColor(themeColor)
|
||||
binding.statsButton.setTextColor(themeColor)
|
||||
binding.pauseLevelUpButton?.setTextColor(themeColor) // Safe call
|
||||
binding.pauseLevelDownButton?.setTextColor(themeColor) // Safe call
|
||||
binding.settingsButton?.setTextColor(themeColor) // Safe call for sound toggle button text
|
||||
binding.musicToggle?.setColorFilter(themeColor) // Safe call
|
||||
|
||||
// Apply theme colors to text elements (using safe calls)
|
||||
binding.settingsTitle?.setTextColor(themeColor)
|
||||
binding.selectLevelText?.setTextColor(themeColor)
|
||||
binding.musicText?.setTextColor(themeColor)
|
||||
binding.pauseLevelText?.setTextColor(themeColor)
|
||||
|
||||
// Apply theme to specific labels in portrait/landscape - RELY ON HELPER BELOW
|
||||
if (isLandscape) {
|
||||
// findViewById<TextView>(R.id.selectThemeTextLandscape)?.setTextColor(themeColor) // REMOVED
|
||||
// findViewById<TextView>(R.id.selectBlockSkinTextLandscape)?.setTextColor(themeColor) // REMOVED
|
||||
// Also color general text views in the landscape container, avoiding selectors
|
||||
applyThemeColorToTextViews(findViewById(R.id.pauseContainer), themeColor)
|
||||
} else {
|
||||
// binding.selectThemeText?.setTextColor(themeColor) // REMOVED
|
||||
// binding.selectBlockSkinText?.setTextColor(themeColor) // REMOVED
|
||||
// Apply to portrait container as well if needed (assuming root or specific container)
|
||||
applyThemeColorToTextViews(binding.pauseContainer, themeColor) // Apply to main container
|
||||
}
|
||||
|
||||
// Update theme selector instances
|
||||
val currentUnlockedThemes = progressionManager.getUnlockedThemes()
|
||||
binding.themeSelector.updateThemes(currentUnlockedThemes, currentTheme)
|
||||
findViewById<ThemeSelector>(R.id.inPauseThemeSelector)?.updateThemes(currentUnlockedThemes, currentTheme)
|
||||
|
||||
// Update block skin selector instances
|
||||
val currentBlockSkin = gameView.getCurrentBlockSkin()
|
||||
val currentUnlockedBlocks = progressionManager.getUnlockedBlocks()
|
||||
val currentLevel = progressionManager.getPlayerLevel()
|
||||
binding.blockSkinSelector.updateBlockSkins(currentUnlockedBlocks, currentBlockSkin, currentLevel)
|
||||
findViewById<BlockSkinSelector>(R.id.inPauseBlockSkinSelector)?.updateBlockSkins(currentUnlockedBlocks, currentBlockSkin, currentLevel)
|
||||
|
||||
// Reset scroll position
|
||||
binding.pauseMenuScrollView?.scrollTo(0, 0)
|
||||
findLandscapeScrollView()?.scrollTo(0, 0)
|
||||
|
||||
// Initialize pause menu navigation (builds the list of navigable items)
|
||||
initPauseMenuNavigation()
|
||||
|
||||
// Reset selection index (will be set and highlighted in showPauseMenu)
|
||||
currentMenuSelection = 0
|
||||
|
||||
// Highlight the initially selected menu item
|
||||
if (pauseMenuItems.isNotEmpty()) {
|
||||
highlightMenuItem(currentMenuSelection)
|
||||
}
|
||||
}
|
||||
|
||||
/** Helper to apply theme color to TextViews within a container, avoiding selectors */
|
||||
private fun applyThemeColorToTextViews(container: View?, themeColor: Int) {
|
||||
if (container == null) return
|
||||
if (container is android.view.ViewGroup) {
|
||||
for (i in 0 until container.childCount) {
|
||||
val child = container.getChildAt(i)
|
||||
if (child is TextView) {
|
||||
// Check if parent is a selector
|
||||
var parent = child.parent
|
||||
var isInSelector = false
|
||||
while (parent != null) {
|
||||
if (parent is ThemeSelector || parent is BlockSkinSelector) {
|
||||
isInSelector = true
|
||||
break
|
||||
}
|
||||
if (parent === container) break // Stop at the container boundary
|
||||
parent = parent.parent
|
||||
}
|
||||
if (!isInSelector) {
|
||||
child.setTextColor(themeColor)
|
||||
}
|
||||
} else if (child is android.view.ViewGroup) {
|
||||
// Recurse only if not a selector
|
||||
if (child !is ThemeSelector && child !is BlockSkinSelector) {
|
||||
applyThemeColorToTextViews(child, themeColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Helper to get background color based on theme */
|
||||
private fun getThemeBackgroundColor(themeId: String): Int {
|
||||
return when (themeId) {
|
||||
PlayerProgressionManager.THEME_CLASSIC -> Color.BLACK
|
||||
PlayerProgressionManager.THEME_NEON -> Color.parseColor("#0D0221")
|
||||
PlayerProgressionManager.THEME_MONOCHROME -> Color.parseColor("#1A1A1A")
|
||||
|
@ -572,58 +679,6 @@ class MainActivity : AppCompatActivity(),
|
|||
PlayerProgressionManager.THEME_GALAXY -> Color.parseColor("#0B0C10")
|
||||
else -> Color.BLACK
|
||||
}
|
||||
binding.pauseContainer.setBackgroundColor(backgroundColor)
|
||||
|
||||
// Apply theme colors to buttons
|
||||
binding.pauseStartButton.setTextColor(textColor)
|
||||
binding.pauseRestartButton.setTextColor(textColor)
|
||||
binding.resumeButton.setTextColor(textColor)
|
||||
binding.highScoresButton.setTextColor(textColor)
|
||||
binding.statsButton.setTextColor(textColor)
|
||||
binding.pauseLevelText?.setTextColor(textColor)
|
||||
binding.pauseLevelUpButton.setTextColor(textColor)
|
||||
binding.pauseLevelDownButton.setTextColor(textColor)
|
||||
binding.settingsButton.setTextColor(textColor)
|
||||
binding.musicToggle.setColorFilter(textColor)
|
||||
|
||||
// Apply theme colors to text elements
|
||||
binding.settingsTitle?.setTextColor(textColor)
|
||||
binding.selectLevelText.setTextColor(textColor)
|
||||
binding.musicText.setTextColor(textColor)
|
||||
|
||||
// Update theme selector - handle both standard and landscape versions
|
||||
updateThemeSelector()
|
||||
|
||||
// Handle landscape mode theme selectors (using null-safe calls)
|
||||
val inPauseThemeSelector = findViewById<ThemeSelector>(R.id.inPauseThemeSelector)
|
||||
inPauseThemeSelector?.updateThemes(
|
||||
unlockedThemes = progressionManager.getUnlockedThemes(),
|
||||
currentTheme = currentTheme
|
||||
)
|
||||
|
||||
// Update block skin selector - handle both standard and landscape versions
|
||||
blockSkinSelector.updateBlockSkins(
|
||||
progressionManager.getUnlockedBlocks(),
|
||||
gameView.getCurrentBlockSkin(),
|
||||
progressionManager.getPlayerLevel()
|
||||
)
|
||||
|
||||
// Handle landscape mode block skin selectors (using null-safe calls)
|
||||
val inPauseBlockSkinSelector = findViewById<BlockSkinSelector>(R.id.inPauseBlockSkinSelector)
|
||||
inPauseBlockSkinSelector?.updateBlockSkins(
|
||||
progressionManager.getUnlockedBlocks(),
|
||||
gameView.getCurrentBlockSkin(),
|
||||
progressionManager.getPlayerLevel()
|
||||
)
|
||||
|
||||
// Initialize pause menu navigation
|
||||
initPauseMenuNavigation()
|
||||
|
||||
// Reset and highlight first selectable menu item
|
||||
if (pauseMenuItems.isNotEmpty()) {
|
||||
currentMenuSelection = if (binding.resumeButton.visibility == View.VISIBLE) 0 else 1
|
||||
highlightMenuItem(currentMenuSelection)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1181,17 +1236,39 @@ class MainActivity : AppCompatActivity(),
|
|||
}
|
||||
}
|
||||
|
||||
override fun onMenuLeft() {
|
||||
runOnUiThread {
|
||||
if (binding.pauseContainer.visibility == View.VISIBLE && pauseMenuItems.isNotEmpty()) {
|
||||
val selectedItem = pauseMenuItems.getOrNull(currentMenuSelection)
|
||||
when (selectedItem) {
|
||||
is ThemeSelector -> selectedItem.focusPreviousItem()
|
||||
is BlockSkinSelector -> selectedItem.focusPreviousItem()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMenuRight() {
|
||||
runOnUiThread {
|
||||
if (binding.pauseContainer.visibility == View.VISIBLE && pauseMenuItems.isNotEmpty()) {
|
||||
val selectedItem = pauseMenuItems.getOrNull(currentMenuSelection)
|
||||
when (selectedItem) {
|
||||
is ThemeSelector -> selectedItem.focusNextItem()
|
||||
is BlockSkinSelector -> selectedItem.focusNextItem()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMenuSelect() {
|
||||
runOnUiThread {
|
||||
if (binding.pauseContainer.visibility == View.VISIBLE) {
|
||||
activateSelectedMenuItem()
|
||||
if (binding.pauseContainer.visibility == View.VISIBLE && pauseMenuItems.isNotEmpty()) {
|
||||
activateSelectedMenuItem() // Handles selectors and other items
|
||||
} else if (titleScreen.visibility == View.VISIBLE) {
|
||||
titleScreen.performClick()
|
||||
} else if (progressionScreen.visibility == View.VISIBLE) {
|
||||
// Handle continue in progression screen
|
||||
progressionScreen.performContinue()
|
||||
} else if (binding.gameOverContainer.visibility == View.VISIBLE) {
|
||||
// Handle play again in game over screen
|
||||
binding.playAgainButton.performClick()
|
||||
}
|
||||
}
|
||||
|
@ -1202,47 +1279,158 @@ class MainActivity : AppCompatActivity(),
|
|||
*/
|
||||
private fun initPauseMenuNavigation() {
|
||||
pauseMenuItems.clear()
|
||||
pauseMenuItems.add(binding.resumeButton)
|
||||
pauseMenuItems.add(binding.pauseStartButton)
|
||||
pauseMenuItems.add(binding.pauseRestartButton)
|
||||
pauseMenuItems.add(binding.highScoresButton)
|
||||
pauseMenuItems.add(binding.statsButton)
|
||||
pauseMenuItems.add(binding.pauseLevelUpButton)
|
||||
pauseMenuItems.add(binding.pauseLevelDownButton)
|
||||
pauseMenuItems.add(binding.settingsButton)
|
||||
pauseMenuItems.add(binding.musicToggle)
|
||||
|
||||
// Add theme selector if present
|
||||
val themeSelector = binding.themeSelector
|
||||
if (themeSelector.visibility == View.VISIBLE) {
|
||||
pauseMenuItems.add(themeSelector)
|
||||
}
|
||||
// Check landscape mode only to select the *correct instance* of the view
|
||||
val isLandscape = resources.configuration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE
|
||||
|
||||
// Add block skin selector if present
|
||||
val blockSkinSelector = binding.blockSkinSelector
|
||||
if (blockSkinSelector.visibility == View.VISIBLE) {
|
||||
pauseMenuItems.add(blockSkinSelector)
|
||||
// Define the unified order (Portrait Order)
|
||||
val orderedViews = mutableListOf<View?>()
|
||||
|
||||
// Group 1: Game control buttons (Resume/Start New Game, Restart)
|
||||
if (binding.resumeButton.visibility == View.VISIBLE) {
|
||||
orderedViews.add(binding.resumeButton)
|
||||
}
|
||||
if (binding.pauseStartButton.visibility == View.VISIBLE) {
|
||||
// In pause menu, the 'Start New Game' button might be visible if paused from title?
|
||||
// Let's assume pauseRestartButton is the primary one during active pause.
|
||||
// Keep the logic simple: add if visible.
|
||||
orderedViews.add(binding.pauseStartButton)
|
||||
}
|
||||
orderedViews.add(binding.pauseRestartButton) // Always add restart
|
||||
|
||||
// Group 2: Stats and Scoring
|
||||
orderedViews.add(binding.highScoresButton)
|
||||
orderedViews.add(binding.statsButton)
|
||||
|
||||
// Group 3: Level selection (use safe calls)
|
||||
orderedViews.add(binding.pauseLevelUpButton)
|
||||
orderedViews.add(binding.pauseLevelDownButton)
|
||||
|
||||
// Group 4: Sound and Music controls (Moved UP) - Use safe calls
|
||||
orderedViews.add(binding.settingsButton) // Sound toggle (Button)
|
||||
orderedViews.add(binding.musicToggle) // Music toggle (ImageButton)
|
||||
|
||||
// Group 5: Visual customization (Theme and Block Skins)
|
||||
// Choose the correct instance based on orientation and visibility
|
||||
val themeSelectorInstance = if (isLandscape) findViewById<ThemeSelector>(R.id.inPauseThemeSelector) else binding.themeSelector
|
||||
val blockSkinSelectorInstance = if (isLandscape) findViewById<BlockSkinSelector>(R.id.inPauseBlockSkinSelector) else binding.blockSkinSelector
|
||||
|
||||
// Only add the selectors if they are actually visible in the current layout
|
||||
if (themeSelectorInstance?.visibility == View.VISIBLE) {
|
||||
orderedViews.add(themeSelectorInstance)
|
||||
}
|
||||
if (blockSkinSelectorInstance?.visibility == View.VISIBLE) {
|
||||
orderedViews.add(blockSkinSelectorInstance)
|
||||
}
|
||||
|
||||
// Add all non-null, visible items from the defined order to the final navigation list
|
||||
pauseMenuItems.addAll(orderedViews.filterNotNull().filter { it.visibility == View.VISIBLE })
|
||||
|
||||
// Reset selection index (will be set and highlighted in showPauseMenu)
|
||||
// currentMenuSelection = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight the currently selected menu item
|
||||
*/
|
||||
private fun highlightMenuItem(index: Int) {
|
||||
// Reset all items to normal state
|
||||
// Ensure index is valid
|
||||
if (pauseMenuItems.isEmpty() || index < 0 || index >= pauseMenuItems.size) {
|
||||
Log.w(TAG, "highlightMenuItem called with invalid index: $index or empty items")
|
||||
return
|
||||
}
|
||||
|
||||
val themeColor = getThemeColor(currentTheme)
|
||||
val highlightBorderColor = themeColor // Use theme color for the main highlight border
|
||||
|
||||
pauseMenuItems.forEachIndexed { i, item ->
|
||||
item.alpha = if (i == index) 1.0f else 0.7f
|
||||
item.scaleX = if (i == index) 1.2f else 1.0f
|
||||
item.scaleY = if (i == index) 1.2f else 1.0f
|
||||
// Add or remove border based on selection
|
||||
if (item is Button) {
|
||||
item.background = if (i == index) {
|
||||
ContextCompat.getDrawable(this, R.drawable.menu_item_selected)
|
||||
} else {
|
||||
ColorDrawable(Color.TRANSPARENT)
|
||||
val isSelected = (i == index)
|
||||
|
||||
// Reset common properties
|
||||
item.alpha = if (isSelected) 1.0f else 0.7f
|
||||
if (item is Button || item is ImageButton) {
|
||||
item.scaleX = 1.0f
|
||||
item.scaleY = 1.0f
|
||||
}
|
||||
// Reset background/focus state for all items before applying highlight
|
||||
when (item) {
|
||||
is Button -> item.background = ColorDrawable(Color.TRANSPARENT)
|
||||
is ImageButton -> item.background = null // Let default background handle non-selected
|
||||
is ThemeSelector -> {
|
||||
item.background = ColorDrawable(Color.TRANSPARENT)
|
||||
item.setHasFocus(false) // Explicitly remove component focus
|
||||
}
|
||||
is BlockSkinSelector -> {
|
||||
item.background = ColorDrawable(Color.TRANSPARENT)
|
||||
item.setHasFocus(false) // Explicitly remove component focus
|
||||
}
|
||||
}
|
||||
|
||||
// Apply highlight to the selected item
|
||||
if (isSelected) {
|
||||
// Apply scaling to buttons
|
||||
if (item is Button || item is ImageButton) {
|
||||
item.scaleX = 1.1f
|
||||
item.scaleY = 1.1f
|
||||
}
|
||||
|
||||
// Add border/background based on type
|
||||
val borderWidth = 4 // Unified border width for highlight
|
||||
val cornerRadius = 12f
|
||||
|
||||
when (item) {
|
||||
is Button -> {
|
||||
// Attempt to use a themed drawable, fallback to simple border
|
||||
ContextCompat.getDrawable(this, R.drawable.menu_item_selected)?.mutate()?.let {
|
||||
if (it is GradientDrawable) {
|
||||
it.setStroke(borderWidth, highlightBorderColor)
|
||||
item.background = it
|
||||
} else {
|
||||
// Fallback for non-gradient drawable (e.g., tinting)
|
||||
it.setTint(highlightBorderColor)
|
||||
item.background = it
|
||||
}
|
||||
} ?: run {
|
||||
// Absolute fallback: create a border if drawable fails
|
||||
item.background = GradientDrawable().apply {
|
||||
setColor(Color.TRANSPARENT)
|
||||
setStroke(borderWidth, highlightBorderColor)
|
||||
setCornerRadius(cornerRadius)
|
||||
}
|
||||
}
|
||||
}
|
||||
is ImageButton -> {
|
||||
item.background = GradientDrawable().apply {
|
||||
setColor(Color.TRANSPARENT) // Keep original button look, just add border
|
||||
setStroke(borderWidth, highlightBorderColor)
|
||||
setCornerRadius(cornerRadius)
|
||||
}
|
||||
}
|
||||
is ThemeSelector -> {
|
||||
item.background = GradientDrawable().apply {
|
||||
setColor(Color.TRANSPARENT)
|
||||
setStroke(borderWidth, highlightBorderColor)
|
||||
setCornerRadius(cornerRadius)
|
||||
}
|
||||
item.setHasFocus(true) // Tell selector it HAS component focus
|
||||
}
|
||||
is BlockSkinSelector -> {
|
||||
item.background = GradientDrawable().apply {
|
||||
setColor(Color.TRANSPARENT)
|
||||
setStroke(borderWidth, highlightBorderColor)
|
||||
setCornerRadius(cornerRadius)
|
||||
}
|
||||
item.setHasFocus(true) // Tell selector it HAS component focus
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Provide haptic feedback for selection change
|
||||
gameHaptics.vibrateForPieceMove()
|
||||
|
||||
// Ensure the selected item is visible
|
||||
scrollToSelectedItem()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1250,10 +1438,21 @@ class MainActivity : AppCompatActivity(),
|
|||
*/
|
||||
private fun moveMenuSelectionUp() {
|
||||
if (pauseMenuItems.isEmpty()) return
|
||||
currentMenuSelection = (currentMenuSelection - 1 + pauseMenuItems.size) % pauseMenuItems.size
|
||||
highlightMenuItem(currentMenuSelection)
|
||||
scrollToSelectedItem()
|
||||
gameHaptics.vibrateForPieceMove()
|
||||
|
||||
// Calculate new selection index with wrapping
|
||||
val previousSelection = currentMenuSelection
|
||||
currentMenuSelection = (currentMenuSelection - 1)
|
||||
|
||||
// Prevent wrapping from bottom to top for more intuitive navigation
|
||||
if (currentMenuSelection < 0) {
|
||||
currentMenuSelection = 0
|
||||
return // Already at top, don't provide feedback
|
||||
}
|
||||
|
||||
// Only provide feedback if selection actually changed
|
||||
if (previousSelection != currentMenuSelection) {
|
||||
highlightMenuItem(currentMenuSelection)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1261,30 +1460,82 @@ class MainActivity : AppCompatActivity(),
|
|||
*/
|
||||
private fun moveMenuSelectionDown() {
|
||||
if (pauseMenuItems.isEmpty()) return
|
||||
currentMenuSelection = (currentMenuSelection + 1) % pauseMenuItems.size
|
||||
highlightMenuItem(currentMenuSelection)
|
||||
scrollToSelectedItem()
|
||||
gameHaptics.vibrateForPieceMove()
|
||||
|
||||
// Calculate new selection index with wrapping
|
||||
val previousSelection = currentMenuSelection
|
||||
currentMenuSelection = (currentMenuSelection + 1)
|
||||
|
||||
// Prevent wrapping from bottom to top for more intuitive navigation
|
||||
if (currentMenuSelection >= pauseMenuItems.size) {
|
||||
currentMenuSelection = pauseMenuItems.size - 1
|
||||
return // Already at bottom, don't provide feedback
|
||||
}
|
||||
|
||||
// Only provide feedback if selection actually changed
|
||||
if (previousSelection != currentMenuSelection) {
|
||||
highlightMenuItem(currentMenuSelection)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll the selected menu item into view
|
||||
*/
|
||||
private fun scrollToSelectedItem() {
|
||||
if (pauseMenuItems.isEmpty()) return
|
||||
if (pauseMenuItems.isEmpty() || currentMenuSelection < 0 || currentMenuSelection >= pauseMenuItems.size) return
|
||||
|
||||
val selectedItem = pauseMenuItems[currentMenuSelection]
|
||||
val scrollView = pauseMenuScrollView ?: return
|
||||
|
||||
// Calculate the item's position relative to the scroll view
|
||||
val itemTop = selectedItem.top
|
||||
val itemBottom = selectedItem.bottom
|
||||
val scrollViewHeight = scrollView.height
|
||||
// Determine which scroll view to use based on orientation
|
||||
val isLandscape = resources.configuration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE
|
||||
val scrollView = if (isLandscape) {
|
||||
findLandscapeScrollView()
|
||||
} else {
|
||||
binding.pauseMenuScrollView
|
||||
} ?: return
|
||||
|
||||
// If the item is partially or fully below the visible area, scroll down
|
||||
if (itemBottom > scrollViewHeight) {
|
||||
scrollView.smoothScrollTo(0, itemBottom - scrollViewHeight)
|
||||
}
|
||||
// If the item is partially or fully above the visible area, scroll up
|
||||
else if (itemTop < 0) {
|
||||
scrollView.smoothScrollTo(0, itemTop)
|
||||
// Delay the scrolling slightly to ensure measurements are correct
|
||||
scrollView.post {
|
||||
try {
|
||||
// Calculate item's position in the ScrollView's coordinate system
|
||||
val scrollBounds = Rect()
|
||||
scrollView.getHitRect(scrollBounds)
|
||||
|
||||
// Get item's location on screen
|
||||
val itemLocation = IntArray(2)
|
||||
selectedItem.getLocationOnScreen(itemLocation)
|
||||
|
||||
// Get ScrollView's location on screen
|
||||
val scrollLocation = IntArray(2)
|
||||
scrollView.getLocationOnScreen(scrollLocation)
|
||||
|
||||
// Calculate relative position
|
||||
val itemY = itemLocation[1] - scrollLocation[1]
|
||||
|
||||
// Get the top and bottom of the item relative to the ScrollView
|
||||
val itemTop = itemY
|
||||
val itemBottom = itemY + selectedItem.height
|
||||
|
||||
// Get the visible height of the ScrollView
|
||||
val visibleHeight = scrollView.height
|
||||
|
||||
// Add padding for better visibility
|
||||
val padding = 50
|
||||
|
||||
// Calculate the scroll target position
|
||||
val scrollY = scrollView.scrollY
|
||||
|
||||
// If item is above visible area
|
||||
if (itemTop < padding) {
|
||||
scrollView.smoothScrollTo(0, scrollY + (itemTop - padding))
|
||||
}
|
||||
// If item is below visible area
|
||||
else if (itemBottom > visibleHeight - padding) {
|
||||
scrollView.smoothScrollTo(0, scrollY + (itemBottom - visibleHeight + padding))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Handle any exceptions that might occur during scrolling
|
||||
Log.e(TAG, "Error scrolling to selected item", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1293,6 +1544,65 @@ class MainActivity : AppCompatActivity(),
|
|||
*/
|
||||
private fun activateSelectedMenuItem() {
|
||||
if (pauseMenuItems.isEmpty() || currentMenuSelection < 0 || currentMenuSelection >= pauseMenuItems.size) return
|
||||
pauseMenuItems[currentMenuSelection].performClick()
|
||||
|
||||
val selectedItem = pauseMenuItems[currentMenuSelection]
|
||||
|
||||
// Vibrate regardless of item type for consistent feedback
|
||||
gameHaptics.vibrateForPieceLock()
|
||||
|
||||
when (selectedItem) {
|
||||
is ThemeSelector -> {
|
||||
// Confirm the internally focused theme
|
||||
selectedItem.confirmSelection()
|
||||
// Applying the theme might change colors, so refresh the pause menu display
|
||||
// Need to delay slightly to allow theme application to potentially finish
|
||||
Handler(Looper.getMainLooper()).postDelayed({ showPauseMenu() }, 100)
|
||||
}
|
||||
is BlockSkinSelector -> {
|
||||
// Confirm the internally focused block skin
|
||||
selectedItem.confirmSelection()
|
||||
// Skin change doesn't affect menu colors, no refresh needed here
|
||||
}
|
||||
else -> {
|
||||
// Handle other menu items with a standard click
|
||||
selectedItem.performClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find any ScrollView in the pause container for landscape mode
|
||||
*/
|
||||
private fun findLandscapeScrollView(): ScrollView? {
|
||||
// First try to find a ScrollView directly in the landscape layout
|
||||
val pauseContainer = findViewById<View>(R.id.pauseContainer) ?: return null
|
||||
|
||||
// Try to find any ScrollView in the pause container
|
||||
if (pauseContainer is android.view.ViewGroup) {
|
||||
// Search for ScrollView in the container
|
||||
fun findScrollView(view: android.view.View): ScrollView? {
|
||||
if (view is ScrollView && view.visibility == View.VISIBLE) {
|
||||
return view
|
||||
} else if (view is android.view.ViewGroup) {
|
||||
for (i in 0 until view.childCount) {
|
||||
val child = view.getChildAt(i)
|
||||
val scrollView = findScrollView(child)
|
||||
if (scrollView != null) {
|
||||
return scrollView
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Try to find ScrollView in the container
|
||||
val scrollView = findScrollView(pauseContainer)
|
||||
if (scrollView != null && scrollView != binding.pauseMenuScrollView) {
|
||||
return scrollView
|
||||
}
|
||||
}
|
||||
|
||||
// If no landscape ScrollView is found, fall back to the default one
|
||||
return binding.pauseMenuScrollView
|
||||
}
|
||||
}
|
|
@ -153,6 +153,8 @@ class GamepadController(
|
|||
fun onMenuUp()
|
||||
fun onMenuDown()
|
||||
fun onMenuSelect()
|
||||
fun onMenuLeft()
|
||||
fun onMenuRight()
|
||||
}
|
||||
|
||||
// Listeners
|
||||
|
@ -279,6 +281,14 @@ class GamepadController(
|
|||
navigationListener?.onMenuDown()
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_DPAD_LEFT -> {
|
||||
navigationListener?.onMenuLeft()
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_DPAD_RIGHT -> {
|
||||
navigationListener?.onMenuRight()
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_BUTTON_A,
|
||||
KeyEvent.KEYCODE_DPAD_CENTER -> {
|
||||
navigationListener?.onMenuSelect()
|
||||
|
|
|
@ -11,6 +11,8 @@ import android.widget.TextView
|
|||
import androidx.cardview.widget.CardView
|
||||
import com.mintris.R
|
||||
import com.mintris.model.PlayerProgressionManager
|
||||
import android.animation.ValueAnimator
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
|
||||
/**
|
||||
* UI component for selecting block skins
|
||||
|
@ -27,11 +29,19 @@ class BlockSkinSelector @JvmOverloads constructor(
|
|||
// Callback when a block skin is selected
|
||||
var onBlockSkinSelected: ((String) -> Unit)? = null
|
||||
|
||||
// Currently selected block skin
|
||||
// Currently selected block skin (persisted)
|
||||
private var selectedSkin: String = "block_skin_1"
|
||||
|
||||
// Block skin cards
|
||||
// Block skin cards map (skinId -> CardView)
|
||||
private val skinCards = mutableMapOf<String, CardView>()
|
||||
// Ordered list of skin IDs for navigation
|
||||
private val skinIdList = mutableListOf<String>()
|
||||
// Currently focused skin ID (for gamepad navigation within the selector)
|
||||
private var focusedSkinId: String? = null
|
||||
// Index of the currently focused skin in skinIdList
|
||||
private var focusedIndex: Int = -1
|
||||
// Flag indicating if the entire selector component has focus from the main menu
|
||||
private var hasComponentFocus: Boolean = false
|
||||
|
||||
// Player level for determining what should be unlocked
|
||||
private var playerLevel: Int = 1
|
||||
|
@ -52,69 +62,47 @@ class BlockSkinSelector @JvmOverloads constructor(
|
|||
// Store player level
|
||||
this.playerLevel = playerLevel
|
||||
|
||||
// Clear existing skin cards
|
||||
// Clear existing skin cards and ID list
|
||||
skinsGrid.removeAllViews()
|
||||
skinCards.clear()
|
||||
skinIdList.clear()
|
||||
|
||||
// Update selected skin
|
||||
// Update selected skin and initial focus
|
||||
selectedSkin = currentSkin
|
||||
focusedSkinId = currentSkin
|
||||
focusedIndex = -1 // Reset index
|
||||
|
||||
// Get all possible skins and their details
|
||||
val allSkins = getBlockSkins()
|
||||
// Get all possible skins and their details, sorted for consistent order
|
||||
val allSkins = getBlockSkins().entries.sortedWith(compareBy({ it.value.unlockLevel }, { it.value.displayName })).associate { it.key to it.value }
|
||||
|
||||
// Add skin cards to the grid
|
||||
// Add skin cards to the grid and build ID list
|
||||
allSkins.forEach { (skinId, skinInfo) ->
|
||||
val isUnlocked = unlockedSkins.contains(skinId) || playerLevel >= skinInfo.unlockLevel
|
||||
val isEffectivelyUnlocked = unlockedSkins.contains(skinId) || playerLevel >= skinInfo.unlockLevel
|
||||
val isSelected = skinId == selectedSkin
|
||||
|
||||
val skinCard = createBlockSkinCard(skinId, skinInfo, isUnlocked, isSelected)
|
||||
// Only add unlocked skins to the navigable list
|
||||
if (isEffectivelyUnlocked) {
|
||||
skinIdList.add(skinId)
|
||||
}
|
||||
|
||||
val skinCard = createBlockSkinCard(skinId, skinInfo, isEffectivelyUnlocked, isSelected)
|
||||
skinCards[skinId] = skinCard
|
||||
skinsGrid.addView(skinCard)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the selected skin with a visual effect
|
||||
*/
|
||||
fun setSelectedSkin(skinId: String) {
|
||||
if (skinId == selectedSkin) return
|
||||
|
||||
// Update previously selected card
|
||||
skinCards[selectedSkin]?.let { prevCard ->
|
||||
prevCard.cardElevation = 2f
|
||||
// Reset any special styling
|
||||
prevCard.background = null
|
||||
prevCard.setCardBackgroundColor(getBlockSkins()[selectedSkin]?.backgroundColor ?: Color.BLACK)
|
||||
}
|
||||
|
||||
// Update visual state of newly selected card
|
||||
skinCards[skinId]?.let { card ->
|
||||
card.cardElevation = 12f
|
||||
|
||||
// Flash animation for selection feedback
|
||||
val flashColor = Color.WHITE
|
||||
val originalColor = getBlockSkins()[skinId]?.backgroundColor ?: Color.BLACK
|
||||
|
||||
// Create animator for flash effect
|
||||
val flashAnimator = android.animation.ValueAnimator.ofArgb(flashColor, originalColor)
|
||||
flashAnimator.duration = 300 // 300ms
|
||||
flashAnimator.addUpdateListener { animator ->
|
||||
val color = animator.animatedValue as Int
|
||||
card.setCardBackgroundColor(color)
|
||||
|
||||
// Update focused index if this is the currently selected/focused skin
|
||||
if (isEffectivelyUnlocked && skinId == focusedSkinId) {
|
||||
focusedIndex = skinIdList.indexOf(skinId)
|
||||
}
|
||||
flashAnimator.start()
|
||||
|
||||
// Add special border to selected card
|
||||
val gradientDrawable = android.graphics.drawable.GradientDrawable().apply {
|
||||
setColor(originalColor)
|
||||
setStroke(6, Color.WHITE) // Thicker border
|
||||
cornerRadius = 12f
|
||||
}
|
||||
card.background = gradientDrawable
|
||||
}
|
||||
|
||||
// Update selected skin
|
||||
selectedSkin = skinId
|
||||
// Ensure focus index is valid if the previously focused skin is no longer available/unlocked
|
||||
if (focusedIndex == -1 && skinIdList.isNotEmpty()) {
|
||||
focusedIndex = 0
|
||||
focusedSkinId = skinIdList[0]
|
||||
}
|
||||
|
||||
// Apply initial focus highlight if the component has focus
|
||||
highlightFocusedCard()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -136,20 +124,6 @@ class BlockSkinSelector @JvmOverloads constructor(
|
|||
// Set card background color based on skin
|
||||
setCardBackgroundColor(skinInfo.backgroundColor)
|
||||
|
||||
// Add more noticeable visual indicator for selected skin
|
||||
if (isSelected) {
|
||||
setContentPadding(4, 4, 4, 4)
|
||||
// Create a gradient drawable for the border
|
||||
val gradientDrawable = android.graphics.drawable.GradientDrawable().apply {
|
||||
setColor(skinInfo.backgroundColor)
|
||||
setStroke(6, Color.WHITE) // Thicker border
|
||||
cornerRadius = 12f
|
||||
}
|
||||
background = gradientDrawable
|
||||
// Add glow effect via elevation
|
||||
cardElevation = 12f
|
||||
}
|
||||
|
||||
// Set card dimensions
|
||||
val cardSize = resources.getDimensionPixelSize(R.dimen.theme_card_size)
|
||||
layoutParams = GridLayout.LayoutParams().apply {
|
||||
|
@ -160,7 +134,7 @@ class BlockSkinSelector @JvmOverloads constructor(
|
|||
setMargins(8, 8, 8, 8)
|
||||
}
|
||||
|
||||
// Apply locked/selected state visuals
|
||||
// Apply locked/selected state visuals (only opacity here)
|
||||
alpha = if (isUnlocked) 1.0f else 0.5f
|
||||
}
|
||||
|
||||
|
@ -244,14 +218,10 @@ class BlockSkinSelector @JvmOverloads constructor(
|
|||
// Set up click listener only for unlocked skins
|
||||
if (isUnlocked) {
|
||||
card.setOnClickListener {
|
||||
// Only trigger callback if this isn't already the selected skin
|
||||
if (skinId != selectedSkin) {
|
||||
// Update UI for selection
|
||||
setSelectedSkin(skinId)
|
||||
|
||||
// Notify listener
|
||||
onBlockSkinSelected?.invoke(skinId)
|
||||
}
|
||||
// Clicking directly selects the skin
|
||||
focusedSkinId = skinId
|
||||
focusedIndex = skinIdList.indexOf(skinId)
|
||||
confirmSelection() // Directly confirm click selection
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -305,4 +275,159 @@ class BlockSkinSelector @JvmOverloads constructor(
|
|||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the entire component has focus (from the parent menu).
|
||||
*/
|
||||
fun setHasFocus(hasFocus: Boolean) {
|
||||
hasComponentFocus = hasFocus
|
||||
highlightFocusedCard() // Re-apply highlights
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the internal focus to the next available skin.
|
||||
*/
|
||||
fun focusNextItem() {
|
||||
if (!hasComponentFocus || skinIdList.isEmpty()) return
|
||||
|
||||
focusedIndex = (focusedIndex + 1) % skinIdList.size
|
||||
focusedSkinId = skinIdList[focusedIndex]
|
||||
highlightFocusedCard()
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the internal focus to the previous available skin.
|
||||
*/
|
||||
fun focusPreviousItem() {
|
||||
if (!hasComponentFocus || skinIdList.isEmpty()) return
|
||||
|
||||
focusedIndex = (focusedIndex - 1 + skinIdList.size) % skinIdList.size
|
||||
focusedSkinId = skinIdList[focusedIndex]
|
||||
highlightFocusedCard()
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms the currently focused skin as the selected skin.
|
||||
* Triggers the onBlockSkinSelected callback.
|
||||
*/
|
||||
fun confirmSelection() {
|
||||
if (!hasComponentFocus || focusedSkinId == null || focusedSkinId == selectedSkin) {
|
||||
return // No change needed
|
||||
}
|
||||
|
||||
// Update the selected skin
|
||||
val newlySelectedSkin = focusedSkinId!!
|
||||
selectedSkin = newlySelectedSkin
|
||||
|
||||
// Update visual states
|
||||
highlightFocusedCard()
|
||||
|
||||
// Trigger the callback
|
||||
onBlockSkinSelected?.invoke(selectedSkin)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the visual highlight state of the skin cards based on
|
||||
* selection and internal focus.
|
||||
*/
|
||||
private fun highlightFocusedCard() {
|
||||
if (skinCards.isEmpty()) return
|
||||
|
||||
val focusColor = Color.YELLOW // Color for focused-but-not-selected
|
||||
val selectedColor = Color.WHITE // Color for selected (might be focused or not)
|
||||
|
||||
skinCards.forEach { (skinId, card) ->
|
||||
val skinInfo = getBlockSkins()[skinId] ?: return@forEach
|
||||
// Check unlock status based on the navigable list derived from level/unlocks
|
||||
val isUnlocked = skinIdList.contains(skinId)
|
||||
|
||||
if (!isUnlocked) {
|
||||
// Keep locked skins visually distinct
|
||||
card.alpha = 0.5f
|
||||
card.cardElevation = 2f
|
||||
card.background = null
|
||||
card.setCardBackgroundColor(skinInfo.backgroundColor)
|
||||
card.scaleX = 1.0f // Reset scale
|
||||
card.scaleY = 1.0f
|
||||
return@forEach
|
||||
}
|
||||
|
||||
// Reset unlocked cards first
|
||||
card.alpha = 1.0f
|
||||
card.cardElevation = 4f
|
||||
card.background = null
|
||||
card.setCardBackgroundColor(skinInfo.backgroundColor)
|
||||
card.scaleX = 1.0f
|
||||
card.scaleY = 1.0f
|
||||
|
||||
val isSelected = (skinId == selectedSkin)
|
||||
val isFocused = (hasComponentFocus && skinId == focusedSkinId)
|
||||
|
||||
var borderColor = Color.TRANSPARENT
|
||||
var borderWidth = 0
|
||||
var elevation = 4f
|
||||
var scale = 1.0f
|
||||
|
||||
if (isSelected) {
|
||||
borderColor = selectedColor
|
||||
borderWidth = 6 // Thick border for selected
|
||||
elevation = 12f // Higher elevation for selected
|
||||
}
|
||||
|
||||
if (isFocused) {
|
||||
// Focused item gets a distinct border (unless it's also selected)
|
||||
if (!isSelected) {
|
||||
borderColor = focusColor
|
||||
borderWidth = 4 // Slightly thinner border for focused
|
||||
}
|
||||
elevation = 12f // Use high elevation for focus too
|
||||
scale = 1.1f // Scale up the focused item
|
||||
}
|
||||
|
||||
// Apply scale
|
||||
card.scaleX = scale
|
||||
card.scaleY = scale
|
||||
|
||||
// Apply border and elevation
|
||||
if (borderWidth > 0) {
|
||||
val gradientDrawable = GradientDrawable().apply {
|
||||
setColor(skinInfo.backgroundColor) // Use skin's background for the fill
|
||||
setStroke(borderWidth, borderColor)
|
||||
cornerRadius = 12f
|
||||
}
|
||||
card.background = gradientDrawable
|
||||
} else {
|
||||
card.background = null // Ensure no border if not selected/focused
|
||||
}
|
||||
card.cardElevation = elevation
|
||||
}
|
||||
}
|
||||
|
||||
// Keep selectNextBlockSkin temporarily for compatibility, but it shouldn't be called by MainActivity anymore
|
||||
fun selectNextBlockSkin() {
|
||||
val allSkins = getBlockSkins().keys.toList()
|
||||
val currentIndex = allSkins.indexOf(selectedSkin)
|
||||
if (currentIndex == -1) return
|
||||
|
||||
var nextIndex = (currentIndex + 1) % allSkins.size
|
||||
while (nextIndex != currentIndex) {
|
||||
val nextSkin = allSkins[nextIndex]
|
||||
val skinInfo = getBlockSkins()[nextSkin] ?: continue
|
||||
val isEffectivelyUnlocked = skinCards[nextSkin]?.alpha == 1.0f // Basic check based on alpha
|
||||
|| playerLevel >= skinInfo.unlockLevel
|
||||
|
||||
if (isEffectivelyUnlocked) {
|
||||
// This method now just sets the internal focus and confirms
|
||||
focusedSkinId = nextSkin
|
||||
focusedIndex = skinIdList.indexOf(nextSkin) // Update index based on navigable list
|
||||
if (focusedIndex == -1) { // If not found in navigable list, reset focus
|
||||
focusedIndex = 0
|
||||
focusedSkinId = if (skinIdList.isNotEmpty()) skinIdList[0] else null
|
||||
}
|
||||
confirmSelection() // Confirm the selection
|
||||
return
|
||||
}
|
||||
nextIndex = (nextIndex + 1) % allSkins.size
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,6 +11,8 @@ import android.widget.TextView
|
|||
import androidx.cardview.widget.CardView
|
||||
import com.mintris.R
|
||||
import com.mintris.model.PlayerProgressionManager
|
||||
import android.animation.ValueAnimator
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
|
||||
/**
|
||||
* UI component for selecting game themes
|
||||
|
@ -27,11 +29,19 @@ class ThemeSelector @JvmOverloads constructor(
|
|||
// Callback when a theme is selected
|
||||
var onThemeSelected: ((String) -> Unit)? = null
|
||||
|
||||
// Currently selected theme
|
||||
// Currently selected theme (persisted)
|
||||
private var selectedTheme: String = PlayerProgressionManager.THEME_CLASSIC
|
||||
|
||||
// Theme cards
|
||||
// Theme cards map (themeId -> CardView)
|
||||
private val themeCards = mutableMapOf<String, CardView>()
|
||||
// Ordered list of theme IDs for navigation
|
||||
private val themeIdList = mutableListOf<String>()
|
||||
// Currently focused theme ID (for gamepad navigation within the selector)
|
||||
private var focusedThemeId: String? = null
|
||||
// Index of the currently focused theme in themeIdList
|
||||
private var focusedIndex: Int = -1
|
||||
// Flag indicating if the entire selector component has focus from the main menu
|
||||
private var hasComponentFocus: Boolean = false
|
||||
|
||||
init {
|
||||
// Inflate the layout
|
||||
|
@ -46,25 +56,47 @@ class ThemeSelector @JvmOverloads constructor(
|
|||
* Update the theme selector with unlocked themes
|
||||
*/
|
||||
fun updateThemes(unlockedThemes: Set<String>, currentTheme: String) {
|
||||
// Clear existing theme cards
|
||||
// Clear existing theme cards and ID list
|
||||
themesGrid.removeAllViews()
|
||||
themeCards.clear()
|
||||
themeIdList.clear()
|
||||
|
||||
// Update selected theme
|
||||
selectedTheme = currentTheme
|
||||
focusedThemeId = currentTheme // Initially focus the selected theme
|
||||
focusedIndex = -1 // Reset index
|
||||
|
||||
// Get all possible themes and their details
|
||||
val allThemes = getThemes()
|
||||
// Get all possible themes and their details, sorted for consistent order
|
||||
val allThemes = getThemes().entries.sortedWith(compareBy({ it.value.unlockLevel }, { it.value.displayName })).associate { it.key to it.value }
|
||||
|
||||
// Add theme cards to the grid
|
||||
// Add theme cards to the grid and build the ID list
|
||||
allThemes.forEach { (themeId, themeInfo) ->
|
||||
val isUnlocked = unlockedThemes.contains(themeId)
|
||||
val isSelected = themeId == selectedTheme
|
||||
|
||||
// Only add unlocked themes to the navigable list
|
||||
if (isUnlocked) {
|
||||
themeIdList.add(themeId)
|
||||
}
|
||||
|
||||
val themeCard = createThemeCard(themeId, themeInfo, isUnlocked, isSelected)
|
||||
themeCards[themeId] = themeCard
|
||||
themesGrid.addView(themeCard)
|
||||
|
||||
// Update focused index if this is the currently selected/focused theme
|
||||
if (isUnlocked && themeId == focusedThemeId) {
|
||||
focusedIndex = themeIdList.indexOf(themeId)
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure focus index is valid if the previously focused theme is no longer available/unlocked
|
||||
if (focusedIndex == -1 && themeIdList.isNotEmpty()) {
|
||||
focusedIndex = 0
|
||||
focusedThemeId = themeIdList[0]
|
||||
}
|
||||
|
||||
// Apply initial focus highlight if the component has focus
|
||||
highlightFocusedCard()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -86,20 +118,6 @@ class ThemeSelector @JvmOverloads constructor(
|
|||
// Set card background color based on theme
|
||||
setCardBackgroundColor(themeInfo.primaryColor)
|
||||
|
||||
// Add more noticeable visual indicator for selected theme
|
||||
if (isSelected) {
|
||||
setContentPadding(4, 4, 4, 4)
|
||||
// Create a gradient drawable for the border
|
||||
val gradientDrawable = android.graphics.drawable.GradientDrawable().apply {
|
||||
setColor(themeInfo.primaryColor)
|
||||
setStroke(6, Color.WHITE) // Thicker border
|
||||
cornerRadius = 12f
|
||||
}
|
||||
background = gradientDrawable
|
||||
// Add glow effect via elevation
|
||||
cardElevation = 12f
|
||||
}
|
||||
|
||||
// Set card dimensions
|
||||
val cardSize = resources.getDimensionPixelSize(R.dimen.theme_card_size)
|
||||
layoutParams = GridLayout.LayoutParams().apply {
|
||||
|
@ -194,38 +212,10 @@ class ThemeSelector @JvmOverloads constructor(
|
|||
// Set up click listener only for unlocked themes
|
||||
if (isUnlocked) {
|
||||
card.setOnClickListener {
|
||||
// Only trigger callback if this isn't already the selected theme
|
||||
if (themeId != selectedTheme) {
|
||||
// Update previously selected card
|
||||
themeCards[selectedTheme]?.let { prevCard ->
|
||||
prevCard.cardElevation = 2f
|
||||
// Reset any special styling
|
||||
prevCard.background = null
|
||||
prevCard.setCardBackgroundColor(getThemes()[selectedTheme]?.primaryColor ?: Color.BLACK)
|
||||
}
|
||||
|
||||
// Update visual state of newly selected card
|
||||
card.cardElevation = 12f
|
||||
|
||||
// Flash animation for selection feedback
|
||||
val flashColor = Color.WHITE
|
||||
val originalColor = themeInfo.primaryColor
|
||||
|
||||
// Create animator for flash effect
|
||||
val flashAnimator = android.animation.ValueAnimator.ofArgb(flashColor, originalColor)
|
||||
flashAnimator.duration = 300 // 300ms
|
||||
flashAnimator.addUpdateListener { animator ->
|
||||
val color = animator.animatedValue as Int
|
||||
card.setCardBackgroundColor(color)
|
||||
}
|
||||
flashAnimator.start()
|
||||
|
||||
// Update selected theme
|
||||
selectedTheme = themeId
|
||||
|
||||
// Notify listener
|
||||
onThemeSelected?.invoke(themeId)
|
||||
}
|
||||
// Clicking directly selects the theme
|
||||
focusedThemeId = themeId
|
||||
focusedIndex = themeIdList.indexOf(themeId)
|
||||
confirmSelection() // Directly confirm click selection
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -292,4 +282,131 @@ class ThemeSelector @JvmOverloads constructor(
|
|||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the entire component has focus (from the parent menu).
|
||||
* Controls the outer border visibility.
|
||||
*/
|
||||
fun setHasFocus(hasFocus: Boolean) {
|
||||
hasComponentFocus = hasFocus
|
||||
// Update visual state based on component focus
|
||||
highlightFocusedCard() // Re-apply highlights
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the internal focus to the next available theme.
|
||||
*/
|
||||
fun focusNextItem() {
|
||||
if (!hasComponentFocus || themeIdList.isEmpty()) return // Only navigate if component has focus
|
||||
|
||||
focusedIndex = (focusedIndex + 1) % themeIdList.size
|
||||
focusedThemeId = themeIdList[focusedIndex]
|
||||
highlightFocusedCard()
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the internal focus to the previous available theme.
|
||||
*/
|
||||
fun focusPreviousItem() {
|
||||
if (!hasComponentFocus || themeIdList.isEmpty()) return // Only navigate if component has focus
|
||||
|
||||
focusedIndex = (focusedIndex - 1 + themeIdList.size) % themeIdList.size
|
||||
focusedThemeId = themeIdList[focusedIndex]
|
||||
highlightFocusedCard()
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms the currently focused theme as the selected theme.
|
||||
* Triggers the onThemeSelected callback.
|
||||
*/
|
||||
fun confirmSelection() {
|
||||
if (!hasComponentFocus || focusedThemeId == null || focusedThemeId == selectedTheme) {
|
||||
// No change needed if component doesn't have focus, nothing is focused,
|
||||
// or the focused item is already selected
|
||||
return
|
||||
}
|
||||
|
||||
// Update the selected theme
|
||||
selectedTheme = focusedThemeId!!
|
||||
|
||||
// Update visual states for all cards
|
||||
highlightFocusedCard() // This will now mark the new theme as selected
|
||||
|
||||
// Trigger the callback
|
||||
onThemeSelected?.invoke(selectedTheme)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the visual highlight state of the theme cards based on
|
||||
* selection and internal focus.
|
||||
*/
|
||||
private fun highlightFocusedCard() {
|
||||
if (themeCards.isEmpty()) return
|
||||
|
||||
val focusColor = Color.YELLOW // Color for the focused-but-not-selected item
|
||||
val selectedColor = Color.WHITE // Color for the selected item (might be focused or not)
|
||||
|
||||
themeCards.forEach { (themeId, card) ->
|
||||
val themeInfo = getThemes()[themeId] ?: return@forEach
|
||||
val isUnlocked = themeIdList.contains(themeId) // Check if it's in the navigable list
|
||||
|
||||
if (!isUnlocked) {
|
||||
// Keep locked themes visually distinct
|
||||
card.alpha = 0.5f
|
||||
card.cardElevation = 2f
|
||||
card.background = null // Remove any border/background
|
||||
card.setCardBackgroundColor(themeInfo.primaryColor)
|
||||
return@forEach // Skip further styling for locked themes
|
||||
}
|
||||
|
||||
// Reset unlocked cards first
|
||||
card.alpha = 1.0f
|
||||
card.cardElevation = 4f // Default elevation for unlocked cards
|
||||
card.background = null
|
||||
card.setCardBackgroundColor(themeInfo.primaryColor)
|
||||
card.scaleX = 1.0f
|
||||
card.scaleY = 1.0f
|
||||
|
||||
val isSelected = (themeId == selectedTheme)
|
||||
val isFocused = (hasComponentFocus && themeId == focusedThemeId)
|
||||
|
||||
var borderColor = Color.TRANSPARENT
|
||||
var borderWidth = 0
|
||||
var elevation = 4f
|
||||
var scale = 1.0f
|
||||
|
||||
if (isSelected) {
|
||||
borderColor = selectedColor
|
||||
borderWidth = 6 // Thick border for selected
|
||||
elevation = 12f // Higher elevation for selected
|
||||
}
|
||||
|
||||
if (isFocused) {
|
||||
// Focused item gets a distinct border (unless it's also selected)
|
||||
if (!isSelected) {
|
||||
borderColor = focusColor
|
||||
borderWidth = 4 // Slightly thinner border for focused
|
||||
}
|
||||
elevation = 12f // Use high elevation for focus too
|
||||
scale = 1.1f // Scale up the focused item
|
||||
}
|
||||
|
||||
// Apply scale
|
||||
card.scaleX = scale
|
||||
card.scaleY = scale
|
||||
|
||||
// Apply border and elevation
|
||||
if (borderWidth > 0) {
|
||||
val gradientDrawable = GradientDrawable().apply {
|
||||
setColor(themeInfo.primaryColor)
|
||||
setStroke(borderWidth, borderColor)
|
||||
cornerRadius = 12f // Keep consistent corner radius
|
||||
}
|
||||
card.background = gradientDrawable
|
||||
} else {
|
||||
card.background = null // Ensure no border if not selected/focused
|
||||
}
|
||||
card.cardElevation = elevation
|
||||
}
|
||||
}
|
||||
}
|
|
@ -472,22 +472,7 @@
|
|||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Theme Selector -->
|
||||
<com.mintris.ui.ThemeSelector
|
||||
android:id="@+id/themeSelector"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<!-- Block Skin Selector -->
|
||||
<com.mintris.ui.BlockSkinSelector
|
||||
android:id="@+id/blockSkinSelector"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<!-- Sound Toggle -->
|
||||
<Button
|
||||
android:id="@+id/settingsButton"
|
||||
android:layout_width="200dp"
|
||||
|
@ -501,6 +486,7 @@
|
|||
android:fontFamily="sans-serif"
|
||||
android:textAllCaps="false" />
|
||||
|
||||
<!-- Music Toggle -->
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -529,6 +515,22 @@
|
|||
android:padding="12dp"
|
||||
android:src="@drawable/ic_volume_up" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Theme Selector -->
|
||||
<com.mintris.ui.ThemeSelector
|
||||
android:id="@+id/themeSelector"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<!-- Block Skin Selector -->
|
||||
<com.mintris.ui.BlockSkinSelector
|
||||
android:id="@+id/blockSkinSelector"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginBottom="16dp" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</LinearLayout>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue