mintris/app/src/main/java/com/pixelmintdrop/ui/ThemeSelector.kt

415 lines
15 KiB
Kotlin
Raw Normal View History

package com.mintris.ui
import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.FrameLayout
import android.widget.GridLayout
import android.widget.TextView
import androidx.cardview.widget.CardView
import com.mintris.R
import com.mintris.model.PlayerProgressionManager
2025-03-31 13:38:38 -04:00
import android.animation.ValueAnimator
import android.graphics.drawable.GradientDrawable
import android.util.Log
/**
* UI component for selecting game themes
*/
class ThemeSelector @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
private val themesGrid: GridLayout
private val availableThemesLabel: TextView
// Callback when a theme is selected
var onThemeSelected: ((String) -> Unit)? = null
2025-03-31 13:38:38 -04:00
// Currently selected theme (persisted)
private var selectedTheme: String = PlayerProgressionManager.THEME_CLASSIC
2025-03-31 13:38:38 -04:00
// Theme cards map (themeId -> CardView)
private val themeCards = mutableMapOf<String, CardView>()
2025-03-31 13:38:38 -04:00
// 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
LayoutInflater.from(context).inflate(R.layout.theme_selector, this, true)
// Get references to views
themesGrid = findViewById(R.id.themes_grid)
availableThemesLabel = findViewById(R.id.available_themes_label)
}
/**
* Update the theme selector with unlocked themes
*/
fun updateThemes(unlockedThemes: Set<String>, currentTheme: String) {
2025-03-31 13:38:38 -04:00
// Clear existing theme cards and ID list
themesGrid.removeAllViews()
themeCards.clear()
2025-03-31 13:38:38 -04:00
themeIdList.clear()
// Update selected theme
selectedTheme = currentTheme
2025-03-31 13:38:38 -04:00
focusedThemeId = currentTheme // Initially focus the selected theme
focusedIndex = -1 // Reset index
2025-03-31 13:38:38 -04:00
// 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 }
2025-03-31 13:38:38 -04:00
// Add theme cards to the grid and build the ID list
allThemes.forEach { (themeId, themeInfo) ->
val isUnlocked = unlockedThemes.contains(themeId)
val isSelected = themeId == selectedTheme
2025-03-31 13:38:38 -04:00
// 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)
2025-03-31 13:38:38 -04:00
// Update focused index if this is the currently selected/focused theme
if (isUnlocked && themeId == focusedThemeId) {
focusedIndex = themeIdList.indexOf(themeId)
}
}
2025-03-31 13:38:38 -04:00
// 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()
}
/**
* Create a card for a theme
*/
private fun createThemeCard(
themeId: String,
themeInfo: ThemeInfo,
isUnlocked: Boolean,
isSelected: Boolean
): CardView {
// Create the card
val card = CardView(context).apply {
id = View.generateViewId()
radius = 12f
cardElevation = if (isSelected) 8f else 2f
useCompatPadding = true
// Set card background color based on theme
setCardBackgroundColor(themeInfo.primaryColor)
// Set card dimensions
val cardSize = resources.getDimensionPixelSize(R.dimen.theme_card_size)
layoutParams = GridLayout.LayoutParams().apply {
width = cardSize
height = cardSize
columnSpec = GridLayout.spec(GridLayout.UNDEFINED, 1f)
rowSpec = GridLayout.spec(GridLayout.UNDEFINED, 1f)
setMargins(8, 8, 8, 8)
}
// Apply locked/selected state visuals
alpha = if (isUnlocked) 1.0f else 0.5f
}
// Create theme content container
val container = FrameLayout(context).apply {
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
}
// Create the theme preview
val themePreview = View(context).apply {
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
// Set the background color
setBackgroundColor(themeInfo.primaryColor)
}
// Add a label with the theme name
val themeLabel = TextView(context).apply {
text = themeInfo.displayName
setTextColor(themeInfo.textColor)
textSize = 14f
textAlignment = View.TEXT_ALIGNMENT_CENTER
// Position at the bottom of the card
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT
).apply {
gravity = android.view.Gravity.BOTTOM or android.view.Gravity.CENTER_HORIZONTAL
setMargins(4, 4, 4, 8)
}
}
// Add level requirement for locked themes
val levelRequirement = TextView(context).apply {
text = "Level ${themeInfo.unlockLevel}"
setTextColor(Color.WHITE)
textSize = 12f
textAlignment = View.TEXT_ALIGNMENT_CENTER
visibility = if (isUnlocked) View.GONE else View.VISIBLE
// Position at the center of the card
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT
).apply {
gravity = android.view.Gravity.CENTER
}
2025-03-27 22:28:47 -04:00
// Make text bold and more visible for better readability
typeface = android.graphics.Typeface.DEFAULT_BOLD
setShadowLayer(3f, 1f, 1f, Color.BLACK)
}
// Add a lock icon if the theme is locked
val lockOverlay = View(context).apply {
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
// Add lock icon or visual indicator
setBackgroundResource(R.drawable.lock_overlay)
visibility = if (isUnlocked) View.GONE else View.VISIBLE
}
// Add all elements to container
container.addView(themePreview)
container.addView(themeLabel)
container.addView(lockOverlay)
container.addView(levelRequirement)
// Add container to card
card.addView(container)
// Set up click listener only for unlocked themes
if (isUnlocked) {
card.setOnClickListener {
2025-03-31 13:38:38 -04:00
// Clicking directly selects the theme
Log.d("ThemeSelector", "Theme card clicked: $themeId (isUnlocked=$isUnlocked)")
2025-03-31 13:38:38 -04:00
focusedThemeId = themeId
focusedIndex = themeIdList.indexOf(themeId)
confirmSelection() // Directly confirm click selection
}
}
return card
}
/**
* Data class for theme information
*/
data class ThemeInfo(
val displayName: String,
val primaryColor: Int,
val secondaryColor: Int,
val textColor: Int,
val unlockLevel: Int
)
/**
* Get all available themes with their details
*/
private fun getThemes(): Map<String, ThemeInfo> {
return mapOf(
PlayerProgressionManager.THEME_CLASSIC to ThemeInfo(
displayName = "Classic",
primaryColor = Color.parseColor("#000000"),
secondaryColor = Color.parseColor("#1F1F1F"),
textColor = Color.WHITE,
unlockLevel = 1
),
PlayerProgressionManager.THEME_NEON to ThemeInfo(
displayName = "Neon",
primaryColor = Color.parseColor("#0D0221"),
secondaryColor = Color.parseColor("#650D89"),
textColor = Color.parseColor("#FF00FF"),
unlockLevel = 5
),
PlayerProgressionManager.THEME_MONOCHROME to ThemeInfo(
displayName = "Monochrome",
primaryColor = Color.parseColor("#1A1A1A"),
secondaryColor = Color.parseColor("#333333"),
textColor = Color.LTGRAY,
unlockLevel = 10
),
PlayerProgressionManager.THEME_RETRO to ThemeInfo(
displayName = "Retro",
primaryColor = Color.parseColor("#3F2832"),
secondaryColor = Color.parseColor("#087E8B"),
textColor = Color.parseColor("#FF5A5F"),
unlockLevel = 15
),
PlayerProgressionManager.THEME_MINIMALIST to ThemeInfo(
displayName = "Minimalist",
primaryColor = Color.parseColor("#FFFFFF"),
secondaryColor = Color.parseColor("#F0F0F0"),
textColor = Color.BLACK,
unlockLevel = 20
),
PlayerProgressionManager.THEME_GALAXY to ThemeInfo(
displayName = "Galaxy",
primaryColor = Color.parseColor("#0B0C10"),
secondaryColor = Color.parseColor("#1F2833"),
textColor = Color.parseColor("#66FCF1"),
unlockLevel = 25
)
)
}
2025-03-31 13:38:38 -04:00
/**
* 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() {
Log.d("ThemeSelector", "confirmSelection called. Focused theme: $focusedThemeId")
if (focusedThemeId == null || focusedThemeId == selectedTheme) {
// No change needed if nothing is focused,
2025-03-31 13:38:38 -04:00
// 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
}
}
}