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.Button
|
||||||
import android.widget.ScrollView
|
import android.widget.ScrollView
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
|
import android.graphics.drawable.GradientDrawable
|
||||||
|
import android.widget.TextView
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity(),
|
class MainActivity : AppCompatActivity(),
|
||||||
GamepadController.GamepadConnectionListener,
|
GamepadController.GamepadConnectionListener,
|
||||||
|
@ -552,18 +554,123 @@ class MainActivity : AppCompatActivity(),
|
||||||
*/
|
*/
|
||||||
private fun showPauseMenu() {
|
private fun showPauseMenu() {
|
||||||
binding.pauseContainer.visibility = View.VISIBLE
|
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
|
// Update level badge
|
||||||
binding.pauseLevelBadge?.setLevel(progressionManager.getPlayerLevel())
|
binding.pauseLevelBadge?.setLevel(progressionManager.getPlayerLevel())
|
||||||
binding.pauseLevelBadge?.setThemeColor(getThemeColor(currentTheme))
|
binding.pauseLevelBadge?.setThemeColor(getThemeColor(currentTheme))
|
||||||
|
|
||||||
// Get theme color
|
// Get theme colors
|
||||||
val textColor = getThemeColor(currentTheme)
|
val themeColor = getThemeColor(currentTheme)
|
||||||
|
val backgroundColor = getThemeBackgroundColor(currentTheme)
|
||||||
|
|
||||||
// Apply theme color to pause container background
|
// Apply background color to pause container
|
||||||
val backgroundColor = when (currentTheme) {
|
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_CLASSIC -> Color.BLACK
|
||||||
PlayerProgressionManager.THEME_NEON -> Color.parseColor("#0D0221")
|
PlayerProgressionManager.THEME_NEON -> Color.parseColor("#0D0221")
|
||||||
PlayerProgressionManager.THEME_MONOCHROME -> Color.parseColor("#1A1A1A")
|
PlayerProgressionManager.THEME_MONOCHROME -> Color.parseColor("#1A1A1A")
|
||||||
|
@ -572,58 +679,6 @@ class MainActivity : AppCompatActivity(),
|
||||||
PlayerProgressionManager.THEME_GALAXY -> Color.parseColor("#0B0C10")
|
PlayerProgressionManager.THEME_GALAXY -> Color.parseColor("#0B0C10")
|
||||||
else -> Color.BLACK
|
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() {
|
override fun onMenuSelect() {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
if (binding.pauseContainer.visibility == View.VISIBLE) {
|
if (binding.pauseContainer.visibility == View.VISIBLE && pauseMenuItems.isNotEmpty()) {
|
||||||
activateSelectedMenuItem()
|
activateSelectedMenuItem() // Handles selectors and other items
|
||||||
} else if (titleScreen.visibility == View.VISIBLE) {
|
} else if (titleScreen.visibility == View.VISIBLE) {
|
||||||
titleScreen.performClick()
|
titleScreen.performClick()
|
||||||
} else if (progressionScreen.visibility == View.VISIBLE) {
|
} else if (progressionScreen.visibility == View.VISIBLE) {
|
||||||
// Handle continue in progression screen
|
|
||||||
progressionScreen.performContinue()
|
progressionScreen.performContinue()
|
||||||
} else if (binding.gameOverContainer.visibility == View.VISIBLE) {
|
} else if (binding.gameOverContainer.visibility == View.VISIBLE) {
|
||||||
// Handle play again in game over screen
|
|
||||||
binding.playAgainButton.performClick()
|
binding.playAgainButton.performClick()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1202,47 +1279,158 @@ class MainActivity : AppCompatActivity(),
|
||||||
*/
|
*/
|
||||||
private fun initPauseMenuNavigation() {
|
private fun initPauseMenuNavigation() {
|
||||||
pauseMenuItems.clear()
|
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
|
// Check landscape mode only to select the *correct instance* of the view
|
||||||
val themeSelector = binding.themeSelector
|
val isLandscape = resources.configuration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE
|
||||||
if (themeSelector.visibility == View.VISIBLE) {
|
|
||||||
pauseMenuItems.add(themeSelector)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add block skin selector if present
|
// Define the unified order (Portrait Order)
|
||||||
val blockSkinSelector = binding.blockSkinSelector
|
val orderedViews = mutableListOf<View?>()
|
||||||
if (blockSkinSelector.visibility == View.VISIBLE) {
|
|
||||||
pauseMenuItems.add(blockSkinSelector)
|
// 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
|
* Highlight the currently selected menu item
|
||||||
*/
|
*/
|
||||||
private fun highlightMenuItem(index: Int) {
|
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 ->
|
pauseMenuItems.forEachIndexed { i, item ->
|
||||||
item.alpha = if (i == index) 1.0f else 0.7f
|
val isSelected = (i == index)
|
||||||
item.scaleX = if (i == index) 1.2f else 1.0f
|
|
||||||
item.scaleY = if (i == index) 1.2f else 1.0f
|
// Reset common properties
|
||||||
// Add or remove border based on selection
|
item.alpha = if (isSelected) 1.0f else 0.7f
|
||||||
if (item is Button) {
|
if (item is Button || item is ImageButton) {
|
||||||
item.background = if (i == index) {
|
item.scaleX = 1.0f
|
||||||
ContextCompat.getDrawable(this, R.drawable.menu_item_selected)
|
item.scaleY = 1.0f
|
||||||
} else {
|
}
|
||||||
ColorDrawable(Color.TRANSPARENT)
|
// 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() {
|
private fun moveMenuSelectionUp() {
|
||||||
if (pauseMenuItems.isEmpty()) return
|
if (pauseMenuItems.isEmpty()) return
|
||||||
currentMenuSelection = (currentMenuSelection - 1 + pauseMenuItems.size) % pauseMenuItems.size
|
|
||||||
highlightMenuItem(currentMenuSelection)
|
// Calculate new selection index with wrapping
|
||||||
scrollToSelectedItem()
|
val previousSelection = currentMenuSelection
|
||||||
gameHaptics.vibrateForPieceMove()
|
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() {
|
private fun moveMenuSelectionDown() {
|
||||||
if (pauseMenuItems.isEmpty()) return
|
if (pauseMenuItems.isEmpty()) return
|
||||||
currentMenuSelection = (currentMenuSelection + 1) % pauseMenuItems.size
|
|
||||||
highlightMenuItem(currentMenuSelection)
|
// Calculate new selection index with wrapping
|
||||||
scrollToSelectedItem()
|
val previousSelection = currentMenuSelection
|
||||||
gameHaptics.vibrateForPieceMove()
|
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() {
|
private fun scrollToSelectedItem() {
|
||||||
if (pauseMenuItems.isEmpty()) return
|
if (pauseMenuItems.isEmpty() || currentMenuSelection < 0 || currentMenuSelection >= pauseMenuItems.size) return
|
||||||
|
|
||||||
val selectedItem = pauseMenuItems[currentMenuSelection]
|
val selectedItem = pauseMenuItems[currentMenuSelection]
|
||||||
val scrollView = pauseMenuScrollView ?: return
|
|
||||||
|
|
||||||
// Calculate the item's position relative to the scroll view
|
// Determine which scroll view to use based on orientation
|
||||||
val itemTop = selectedItem.top
|
val isLandscape = resources.configuration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE
|
||||||
val itemBottom = selectedItem.bottom
|
val scrollView = if (isLandscape) {
|
||||||
val scrollViewHeight = scrollView.height
|
findLandscapeScrollView()
|
||||||
|
} else {
|
||||||
|
binding.pauseMenuScrollView
|
||||||
|
} ?: return
|
||||||
|
|
||||||
// If the item is partially or fully below the visible area, scroll down
|
// Delay the scrolling slightly to ensure measurements are correct
|
||||||
if (itemBottom > scrollViewHeight) {
|
scrollView.post {
|
||||||
scrollView.smoothScrollTo(0, itemBottom - scrollViewHeight)
|
try {
|
||||||
}
|
// Calculate item's position in the ScrollView's coordinate system
|
||||||
// If the item is partially or fully above the visible area, scroll up
|
val scrollBounds = Rect()
|
||||||
else if (itemTop < 0) {
|
scrollView.getHitRect(scrollBounds)
|
||||||
scrollView.smoothScrollTo(0, itemTop)
|
|
||||||
|
// 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() {
|
private fun activateSelectedMenuItem() {
|
||||||
if (pauseMenuItems.isEmpty() || currentMenuSelection < 0 || currentMenuSelection >= pauseMenuItems.size) return
|
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 onMenuUp()
|
||||||
fun onMenuDown()
|
fun onMenuDown()
|
||||||
fun onMenuSelect()
|
fun onMenuSelect()
|
||||||
|
fun onMenuLeft()
|
||||||
|
fun onMenuRight()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listeners
|
// Listeners
|
||||||
|
@ -279,6 +281,14 @@ class GamepadController(
|
||||||
navigationListener?.onMenuDown()
|
navigationListener?.onMenuDown()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
KeyEvent.KEYCODE_DPAD_LEFT -> {
|
||||||
|
navigationListener?.onMenuLeft()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_DPAD_RIGHT -> {
|
||||||
|
navigationListener?.onMenuRight()
|
||||||
|
return true
|
||||||
|
}
|
||||||
KeyEvent.KEYCODE_BUTTON_A,
|
KeyEvent.KEYCODE_BUTTON_A,
|
||||||
KeyEvent.KEYCODE_DPAD_CENTER -> {
|
KeyEvent.KEYCODE_DPAD_CENTER -> {
|
||||||
navigationListener?.onMenuSelect()
|
navigationListener?.onMenuSelect()
|
||||||
|
|
|
@ -11,6 +11,8 @@ import android.widget.TextView
|
||||||
import androidx.cardview.widget.CardView
|
import androidx.cardview.widget.CardView
|
||||||
import com.mintris.R
|
import com.mintris.R
|
||||||
import com.mintris.model.PlayerProgressionManager
|
import com.mintris.model.PlayerProgressionManager
|
||||||
|
import android.animation.ValueAnimator
|
||||||
|
import android.graphics.drawable.GradientDrawable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UI component for selecting block skins
|
* UI component for selecting block skins
|
||||||
|
@ -27,11 +29,19 @@ class BlockSkinSelector @JvmOverloads constructor(
|
||||||
// Callback when a block skin is selected
|
// Callback when a block skin is selected
|
||||||
var onBlockSkinSelected: ((String) -> Unit)? = null
|
var onBlockSkinSelected: ((String) -> Unit)? = null
|
||||||
|
|
||||||
// Currently selected block skin
|
// Currently selected block skin (persisted)
|
||||||
private var selectedSkin: String = "block_skin_1"
|
private var selectedSkin: String = "block_skin_1"
|
||||||
|
|
||||||
// Block skin cards
|
// Block skin cards map (skinId -> CardView)
|
||||||
private val skinCards = mutableMapOf<String, 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
|
// Player level for determining what should be unlocked
|
||||||
private var playerLevel: Int = 1
|
private var playerLevel: Int = 1
|
||||||
|
@ -52,69 +62,47 @@ class BlockSkinSelector @JvmOverloads constructor(
|
||||||
// Store player level
|
// Store player level
|
||||||
this.playerLevel = playerLevel
|
this.playerLevel = playerLevel
|
||||||
|
|
||||||
// Clear existing skin cards
|
// Clear existing skin cards and ID list
|
||||||
skinsGrid.removeAllViews()
|
skinsGrid.removeAllViews()
|
||||||
skinCards.clear()
|
skinCards.clear()
|
||||||
|
skinIdList.clear()
|
||||||
|
|
||||||
// Update selected skin
|
// Update selected skin and initial focus
|
||||||
selectedSkin = currentSkin
|
selectedSkin = currentSkin
|
||||||
|
focusedSkinId = currentSkin
|
||||||
|
focusedIndex = -1 // Reset index
|
||||||
|
|
||||||
// Get all possible skins and their details
|
// Get all possible skins and their details, sorted for consistent order
|
||||||
val allSkins = getBlockSkins()
|
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) ->
|
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 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
|
skinCards[skinId] = skinCard
|
||||||
skinsGrid.addView(skinCard)
|
skinsGrid.addView(skinCard)
|
||||||
}
|
|
||||||
}
|
// Update focused index if this is the currently selected/focused skin
|
||||||
|
if (isEffectivelyUnlocked && skinId == focusedSkinId) {
|
||||||
/**
|
focusedIndex = skinIdList.indexOf(skinId)
|
||||||
* 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)
|
|
||||||
}
|
}
|
||||||
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
|
// Ensure focus index is valid if the previously focused skin is no longer available/unlocked
|
||||||
selectedSkin = skinId
|
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
|
// Set card background color based on skin
|
||||||
setCardBackgroundColor(skinInfo.backgroundColor)
|
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
|
// Set card dimensions
|
||||||
val cardSize = resources.getDimensionPixelSize(R.dimen.theme_card_size)
|
val cardSize = resources.getDimensionPixelSize(R.dimen.theme_card_size)
|
||||||
layoutParams = GridLayout.LayoutParams().apply {
|
layoutParams = GridLayout.LayoutParams().apply {
|
||||||
|
@ -160,7 +134,7 @@ class BlockSkinSelector @JvmOverloads constructor(
|
||||||
setMargins(8, 8, 8, 8)
|
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
|
alpha = if (isUnlocked) 1.0f else 0.5f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -244,14 +218,10 @@ class BlockSkinSelector @JvmOverloads constructor(
|
||||||
// Set up click listener only for unlocked skins
|
// Set up click listener only for unlocked skins
|
||||||
if (isUnlocked) {
|
if (isUnlocked) {
|
||||||
card.setOnClickListener {
|
card.setOnClickListener {
|
||||||
// Only trigger callback if this isn't already the selected skin
|
// Clicking directly selects the skin
|
||||||
if (skinId != selectedSkin) {
|
focusedSkinId = skinId
|
||||||
// Update UI for selection
|
focusedIndex = skinIdList.indexOf(skinId)
|
||||||
setSelectedSkin(skinId)
|
confirmSelection() // Directly confirm click selection
|
||||||
|
|
||||||
// Notify listener
|
|
||||||
onBlockSkinSelected?.invoke(skinId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 androidx.cardview.widget.CardView
|
||||||
import com.mintris.R
|
import com.mintris.R
|
||||||
import com.mintris.model.PlayerProgressionManager
|
import com.mintris.model.PlayerProgressionManager
|
||||||
|
import android.animation.ValueAnimator
|
||||||
|
import android.graphics.drawable.GradientDrawable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UI component for selecting game themes
|
* UI component for selecting game themes
|
||||||
|
@ -27,11 +29,19 @@ class ThemeSelector @JvmOverloads constructor(
|
||||||
// Callback when a theme is selected
|
// Callback when a theme is selected
|
||||||
var onThemeSelected: ((String) -> Unit)? = null
|
var onThemeSelected: ((String) -> Unit)? = null
|
||||||
|
|
||||||
// Currently selected theme
|
// Currently selected theme (persisted)
|
||||||
private var selectedTheme: String = PlayerProgressionManager.THEME_CLASSIC
|
private var selectedTheme: String = PlayerProgressionManager.THEME_CLASSIC
|
||||||
|
|
||||||
// Theme cards
|
// Theme cards map (themeId -> CardView)
|
||||||
private val themeCards = mutableMapOf<String, 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 {
|
init {
|
||||||
// Inflate the layout
|
// Inflate the layout
|
||||||
|
@ -46,25 +56,47 @@ class ThemeSelector @JvmOverloads constructor(
|
||||||
* Update the theme selector with unlocked themes
|
* Update the theme selector with unlocked themes
|
||||||
*/
|
*/
|
||||||
fun updateThemes(unlockedThemes: Set<String>, currentTheme: String) {
|
fun updateThemes(unlockedThemes: Set<String>, currentTheme: String) {
|
||||||
// Clear existing theme cards
|
// Clear existing theme cards and ID list
|
||||||
themesGrid.removeAllViews()
|
themesGrid.removeAllViews()
|
||||||
themeCards.clear()
|
themeCards.clear()
|
||||||
|
themeIdList.clear()
|
||||||
|
|
||||||
// Update selected theme
|
// Update selected theme
|
||||||
selectedTheme = currentTheme
|
selectedTheme = currentTheme
|
||||||
|
focusedThemeId = currentTheme // Initially focus the selected theme
|
||||||
|
focusedIndex = -1 // Reset index
|
||||||
|
|
||||||
// Get all possible themes and their details
|
// Get all possible themes and their details, sorted for consistent order
|
||||||
val allThemes = getThemes()
|
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) ->
|
allThemes.forEach { (themeId, themeInfo) ->
|
||||||
val isUnlocked = unlockedThemes.contains(themeId)
|
val isUnlocked = unlockedThemes.contains(themeId)
|
||||||
val isSelected = themeId == selectedTheme
|
val isSelected = themeId == selectedTheme
|
||||||
|
|
||||||
|
// Only add unlocked themes to the navigable list
|
||||||
|
if (isUnlocked) {
|
||||||
|
themeIdList.add(themeId)
|
||||||
|
}
|
||||||
|
|
||||||
val themeCard = createThemeCard(themeId, themeInfo, isUnlocked, isSelected)
|
val themeCard = createThemeCard(themeId, themeInfo, isUnlocked, isSelected)
|
||||||
themeCards[themeId] = themeCard
|
themeCards[themeId] = themeCard
|
||||||
themesGrid.addView(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
|
// Set card background color based on theme
|
||||||
setCardBackgroundColor(themeInfo.primaryColor)
|
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
|
// Set card dimensions
|
||||||
val cardSize = resources.getDimensionPixelSize(R.dimen.theme_card_size)
|
val cardSize = resources.getDimensionPixelSize(R.dimen.theme_card_size)
|
||||||
layoutParams = GridLayout.LayoutParams().apply {
|
layoutParams = GridLayout.LayoutParams().apply {
|
||||||
|
@ -194,38 +212,10 @@ class ThemeSelector @JvmOverloads constructor(
|
||||||
// Set up click listener only for unlocked themes
|
// Set up click listener only for unlocked themes
|
||||||
if (isUnlocked) {
|
if (isUnlocked) {
|
||||||
card.setOnClickListener {
|
card.setOnClickListener {
|
||||||
// Only trigger callback if this isn't already the selected theme
|
// Clicking directly selects the theme
|
||||||
if (themeId != selectedTheme) {
|
focusedThemeId = themeId
|
||||||
// Update previously selected card
|
focusedIndex = themeIdList.indexOf(themeId)
|
||||||
themeCards[selectedTheme]?.let { prevCard ->
|
confirmSelection() // Directly confirm click selection
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<!-- Theme Selector -->
|
<!-- Sound Toggle -->
|
||||||
<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" />
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/settingsButton"
|
android:id="@+id/settingsButton"
|
||||||
android:layout_width="200dp"
|
android:layout_width="200dp"
|
||||||
|
@ -501,6 +486,7 @@
|
||||||
android:fontFamily="sans-serif"
|
android:fontFamily="sans-serif"
|
||||||
android:textAllCaps="false" />
|
android:textAllCaps="false" />
|
||||||
|
|
||||||
|
<!-- Music Toggle -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
@ -529,6 +515,22 @@
|
||||||
android:padding="12dp"
|
android:padding="12dp"
|
||||||
android:src="@drawable/ic_volume_up" />
|
android:src="@drawable/ic_volume_up" />
|
||||||
</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" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue