Fix menu shifting issue and code cleanup. Fix menu shifting by adding fillViewport and padding to ScrollView. Added missing getLastClearedLines method. Improved code quality with proper logging constants and removed unused imports.

This commit is contained in:
cmclark00 2025-03-28 12:33:42 -04:00
parent 8661fd8a80
commit 9ab9b53407
5 changed files with 119 additions and 73 deletions

View file

@ -1,26 +1,18 @@
package com.mintris package com.mintris
import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.VibrationEffect import android.os.VibrationEffect
import android.os.Vibrator import android.os.Vibrator
import android.os.VibratorManager
import android.view.View import android.view.View
import android.widget.Button import android.view.HapticFeedbackConstants
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.mintris.databinding.ActivityMainBinding import com.mintris.databinding.ActivityMainBinding
import com.mintris.game.GameHaptics import com.mintris.game.GameHaptics
import com.mintris.game.GameView import com.mintris.game.GameView
import com.mintris.game.NextPieceView
import com.mintris.game.TitleScreen import com.mintris.game.TitleScreen
import android.view.HapticFeedbackConstants
import com.mintris.model.GameBoard import com.mintris.model.GameBoard
import com.mintris.audio.GameMusic import com.mintris.audio.GameMusic
import com.mintris.model.HighScoreManager import com.mintris.model.HighScoreManager
@ -33,10 +25,15 @@ import android.graphics.Color
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import android.graphics.Rect import android.graphics.Rect
import android.util.Log
import android.view.KeyEvent import android.view.KeyEvent
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
companion object {
private const val TAG = "MainActivity"
}
// UI components // UI components
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
private lateinit var gameView: GameView private lateinit var gameView: GameView
@ -165,18 +162,18 @@ class MainActivity : AppCompatActivity() {
} }
gameView.onLineClear = { lineCount -> gameView.onLineClear = { lineCount ->
android.util.Log.d("MainActivity", "Received line clear callback: $lineCount lines") Log.d(TAG, "Received line clear callback: $lineCount lines")
// Use enhanced haptic feedback for line clears // Use enhanced haptic feedback for line clears
if (isSoundEnabled) { if (isSoundEnabled) {
android.util.Log.d("MainActivity", "Sound is enabled, triggering haptic feedback") Log.d(TAG, "Sound is enabled, triggering haptic feedback")
try { try {
gameHaptics.vibrateForLineClear(lineCount) gameHaptics.vibrateForLineClear(lineCount)
android.util.Log.d("MainActivity", "Haptic feedback triggered successfully") Log.d(TAG, "Haptic feedback triggered successfully")
} catch (e: Exception) { } catch (e: Exception) {
android.util.Log.e("MainActivity", "Error triggering haptic feedback", e) Log.e(TAG, "Error triggering haptic feedback", e)
} }
} else { } else {
android.util.Log.d("MainActivity", "Sound is disabled, skipping haptic feedback") Log.d(TAG, "Sound is disabled, skipping haptic feedback")
} }
// Record line clear in stats // Record line clear in stats
statsManager.recordLineClear(lineCount) statsManager.recordLineClear(lineCount)

View file

@ -7,15 +7,66 @@ import android.os.Vibrator
import android.os.VibratorManager import android.os.VibratorManager
import android.view.HapticFeedbackConstants import android.view.HapticFeedbackConstants
import android.view.View import android.view.View
import android.util.Log
/**
* Handles haptic feedback for game events
*/
class GameHaptics(private val context: Context) { class GameHaptics(private val context: Context) {
private val vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
companion object {
private const val TAG = "GameHaptics"
}
// Vibrator service
private val vibrator: Vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val vibratorManager = context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager val vibratorManager = context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
vibratorManager.defaultVibrator vibratorManager.defaultVibrator
} else { } else {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
} }
// Vibrate for line clear (more intense for more lines)
fun vibrateForLineClear(lineCount: Int) {
Log.d(TAG, "Attempting to vibrate for $lineCount lines")
// Only proceed if the device has a vibrator and it's available
if (!vibrator.hasVibrator()) return
// Scale duration and amplitude based on line count
// More lines = longer and stronger vibration
val duration = when(lineCount) {
1 -> 50L // Single line: short vibration
2 -> 80L // Double line: slightly longer
3 -> 120L // Triple line: even longer
4 -> 200L // Tetris: longest vibration
else -> 50L
}
val amplitude = when(lineCount) {
1 -> 80 // Single line: mild vibration (80/255)
2 -> 120 // Double line: medium vibration (120/255)
3 -> 180 // Triple line: strong vibration (180/255)
4 -> 255 // Tetris: maximum vibration (255/255)
else -> 80
}
Log.d(TAG, "Vibration parameters - Duration: ${duration}ms, Amplitude: $amplitude")
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
vibrator.vibrate(VibrationEffect.createOneShot(duration, amplitude))
Log.d(TAG, "Vibration triggered successfully")
} else {
@Suppress("DEPRECATION")
vibrator.vibrate(duration)
Log.w(TAG, "Device does not support vibration effects (Android < 8.0)")
}
} catch (e: Exception) {
Log.e(TAG, "Error triggering vibration", e)
}
}
fun performHapticFeedback(view: View, feedbackType: Int) { fun performHapticFeedback(view: View, feedbackType: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
@ -26,40 +77,6 @@ class GameHaptics(private val context: Context) {
} }
} }
fun vibrateForLineClear(lineCount: Int) {
android.util.Log.d("GameHaptics", "Attempting to vibrate for $lineCount lines")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val duration = when (lineCount) {
4 -> 200L // Tetris - doubled from 100L
3 -> 160L // Triples - doubled from 80L
2 -> 120L // Doubles - doubled from 60L
1 -> 80L // Singles - doubled from 40L
else -> 0L
}
val amplitude = when (lineCount) {
4 -> 255 // Full amplitude for Tetris
3 -> 230 // 90% amplitude for triples
2 -> 180 // 70% amplitude for doubles
1 -> 128 // 50% amplitude for singles
else -> 0
}
android.util.Log.d("GameHaptics", "Vibration parameters - Duration: ${duration}ms, Amplitude: $amplitude")
if (duration > 0 && amplitude > 0) {
try {
val vibrationEffect = VibrationEffect.createOneShot(duration, amplitude)
vibrator.vibrate(vibrationEffect)
android.util.Log.d("GameHaptics", "Vibration triggered successfully")
} catch (e: Exception) {
android.util.Log.e("GameHaptics", "Error triggering vibration", e)
}
}
} else {
android.util.Log.w("GameHaptics", "Device does not support vibration effects (Android < 8.0)")
}
}
fun vibrateForPieceLock() { fun vibrateForPieceLock() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val vibrationEffect = VibrationEffect.createOneShot(50L, VibrationEffect.DEFAULT_AMPLITUDE) val vibrationEffect = VibrationEffect.createOneShot(50L, VibrationEffect.DEFAULT_AMPLITUDE)

View file

@ -12,12 +12,12 @@ import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.AttributeSet import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.animation.LinearInterpolator import android.view.animation.LinearInterpolator
import android.view.WindowManager
import android.view.Display
import android.hardware.display.DisplayManager import android.hardware.display.DisplayManager
import android.view.Display
import com.mintris.model.GameBoard import com.mintris.model.GameBoard
import com.mintris.model.Tetromino import com.mintris.model.Tetromino
import com.mintris.model.TetrominoType import com.mintris.model.TetrominoType
@ -33,6 +33,10 @@ class GameView @JvmOverloads constructor(
defStyleAttr: Int = 0 defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) { ) : View(context, attrs, defStyleAttr) {
companion object {
private const val TAG = "GameView"
}
// Game board model // Game board model
private var gameBoard = GameBoard() private var gameBoard = GameBoard()
private var gameHaptics: GameHaptics? = null private var gameHaptics: GameHaptics? = null
@ -165,17 +169,17 @@ class GameView @JvmOverloads constructor(
gameBoard.onPieceMove = { onPieceMove?.invoke() } gameBoard.onPieceMove = { onPieceMove?.invoke() }
gameBoard.onPieceLock = { onPieceLock?.invoke() } gameBoard.onPieceLock = { onPieceLock?.invoke() }
gameBoard.onLineClear = { lineCount, clearedLines -> gameBoard.onLineClear = { lineCount, clearedLines ->
android.util.Log.d("GameView", "Received line clear from GameBoard: $lineCount lines") Log.d(TAG, "Received line clear from GameBoard: $lineCount lines")
try { try {
onLineClear?.invoke(lineCount) onLineClear?.invoke(lineCount)
// Use the lines that were cleared directly // Use the lines that were cleared directly
linesToPulse.clear() linesToPulse.clear()
linesToPulse.addAll(clearedLines) linesToPulse.addAll(clearedLines)
android.util.Log.d("GameView", "Found ${linesToPulse.size} lines to pulse") Log.d(TAG, "Found ${linesToPulse.size} lines to pulse")
startPulseAnimation(lineCount) startPulseAnimation(lineCount)
android.util.Log.d("GameView", "Forwarded line clear callback") Log.d(TAG, "Forwarded line clear callback")
} catch (e: Exception) { } catch (e: Exception) {
android.util.Log.e("GameView", "Error forwarding line clear callback", e) Log.e(TAG, "Error forwarding line clear callback", e)
} }
} }
@ -184,7 +188,11 @@ class GameView @JvmOverloads constructor(
// Set better frame rate using modern APIs // Set better frame rate using modern APIs
val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY) val display = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
displayManager.getDisplay(Display.DEFAULT_DISPLAY)
} else {
displayManager.displays.firstOrNull()
}
display?.let { disp -> display?.let { disp ->
val refreshRate = disp.refreshRate val refreshRate = disp.refreshRate
// Set game loop interval based on refresh rate, but don't go faster than the base interval // Set game loop interval based on refresh rate, but don't go faster than the base interval
@ -289,7 +297,7 @@ class GameView @JvmOverloads constructor(
val totalHeight = blockSize * verticalBlocks val totalHeight = blockSize * verticalBlocks
// Log dimensions for debugging // Log dimensions for debugging
android.util.Log.d("GameView", "Board dimensions: width=$width, height=$height, blockSize=$blockSize, boardLeft=$boardLeft, boardTop=$boardTop, totalHeight=$totalHeight") Log.d(TAG, "Board dimensions: width=$width, height=$height, blockSize=$blockSize, boardLeft=$boardLeft, boardTop=$boardTop, totalHeight=$totalHeight")
} }
override fun onDraw(canvas: Canvas) { override fun onDraw(canvas: Canvas) {
@ -739,17 +747,17 @@ class GameView @JvmOverloads constructor(
gameBoard.onPieceMove = { onPieceMove?.invoke() } gameBoard.onPieceMove = { onPieceMove?.invoke() }
gameBoard.onPieceLock = { onPieceLock?.invoke() } gameBoard.onPieceLock = { onPieceLock?.invoke() }
gameBoard.onLineClear = { lineCount, clearedLines -> gameBoard.onLineClear = { lineCount, clearedLines ->
android.util.Log.d("GameView", "Received line clear from GameBoard: $lineCount lines") Log.d(TAG, "Received line clear from GameBoard: $lineCount lines")
try { try {
onLineClear?.invoke(lineCount) onLineClear?.invoke(lineCount)
// Use the lines that were cleared directly // Use the lines that were cleared directly
linesToPulse.clear() linesToPulse.clear()
linesToPulse.addAll(clearedLines) linesToPulse.addAll(clearedLines)
android.util.Log.d("GameView", "Found ${linesToPulse.size} lines to pulse") Log.d(TAG, "Found ${linesToPulse.size} lines to pulse")
startPulseAnimation(lineCount) startPulseAnimation(lineCount)
android.util.Log.d("GameView", "Forwarded line clear callback") Log.d(TAG, "Forwarded line clear callback")
} catch (e: Exception) { } catch (e: Exception) {
android.util.Log.e("GameView", "Error forwarding line clear callback", e) Log.e(TAG, "Error forwarding line clear callback", e)
} }
} }
@ -785,7 +793,7 @@ class GameView @JvmOverloads constructor(
* Start the pulse animation for line clear * Start the pulse animation for line clear
*/ */
private fun startPulseAnimation(lineCount: Int) { private fun startPulseAnimation(lineCount: Int) {
android.util.Log.d("GameView", "Starting pulse animation for $lineCount lines") Log.d(TAG, "Starting pulse animation for $lineCount lines")
// Cancel any existing animation // Cancel any existing animation
pulseAnimator?.cancel() pulseAnimator?.cancel()
@ -804,7 +812,7 @@ class GameView @JvmOverloads constructor(
pulseAlpha = animation.animatedValue as Float pulseAlpha = animation.animatedValue as Float
isPulsing = true isPulsing = true
invalidate() invalidate()
android.util.Log.d("GameView", "Pulse animation update: alpha = $pulseAlpha") Log.d(TAG, "Pulse animation update: alpha = $pulseAlpha")
} }
addListener(object : android.animation.AnimatorListenerAdapter() { addListener(object : android.animation.AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: android.animation.Animator) { override fun onAnimationEnd(animation: android.animation.Animator) {
@ -812,7 +820,7 @@ class GameView @JvmOverloads constructor(
pulseAlpha = 0f pulseAlpha = 0f
linesToPulse.clear() linesToPulse.clear()
invalidate() invalidate()
android.util.Log.d("GameView", "Pulse animation ended") Log.d(TAG, "Pulse animation ended")
} }
}) })
} }

View file

@ -1,6 +1,6 @@
package com.mintris.model package com.mintris.model
import kotlin.random.Random import android.util.Log
/** /**
* Represents the game board (grid) and manages game state * Represents the game board (grid) and manages game state
@ -9,6 +9,10 @@ class GameBoard(
val width: Int = 10, val width: Int = 10,
val height: Int = 20 val height: Int = 20
) { ) {
companion object {
private const val TAG = "GameBoard"
}
// Board grid to track locked pieces // Board grid to track locked pieces
// True = occupied, False = empty // True = occupied, False = empty
private val grid = Array(height) { BooleanArray(width) { false } } private val grid = Array(height) { BooleanArray(width) { false } }
@ -55,6 +59,9 @@ class GameBoard(
var onNextPieceChanged: (() -> Unit)? = null var onNextPieceChanged: (() -> Unit)? = null
var onLineClear: ((Int, List<Int>) -> Unit)? = null var onLineClear: ((Int, List<Int>) -> Unit)? = null
// Store the last cleared lines
private val lastClearedLines = mutableListOf<Int>()
init { init {
spawnNextPiece() spawnNextPiece()
spawnPiece() spawnPiece()
@ -66,7 +73,7 @@ class GameBoard(
private fun spawnNextPiece() { private fun spawnNextPiece() {
// If bag is empty, refill it with all piece types // If bag is empty, refill it with all piece types
if (bag.isEmpty()) { if (bag.isEmpty()) {
bag.addAll(TetrominoType.values()) bag.addAll(TetrominoType.entries.toTypedArray())
bag.shuffle() bag.shuffle()
} }
@ -326,18 +333,26 @@ class GameBoard(
y-- y--
} }
// Store the last cleared lines
lastClearedLines.clear()
lastClearedLines.addAll(linesToClear)
// If lines were cleared, calculate score in background and trigger callback // If lines were cleared, calculate score in background and trigger callback
if (shiftAmount > 0) { if (shiftAmount > 0) {
android.util.Log.d("GameBoard", "Lines cleared: $shiftAmount") // Log line clear
Log.d(TAG, "Lines cleared: $shiftAmount")
// Trigger line clear callback on main thread with the lines that were cleared // Trigger line clear callback on main thread with the lines that were cleared
val mainHandler = android.os.Handler(android.os.Looper.getMainLooper()) val mainHandler = android.os.Handler(android.os.Looper.getMainLooper())
mainHandler.post { mainHandler.post {
android.util.Log.d("GameBoard", "Triggering onLineClear callback with $shiftAmount lines") // Call the line clear callback with the cleared line count
try { try {
onLineClear?.invoke(shiftAmount, linesToClear) // Pass the lines that were cleared Log.d(TAG, "Triggering onLineClear callback with $shiftAmount lines")
android.util.Log.d("GameBoard", "onLineClear callback completed successfully") val clearedLines = getLastClearedLines()
onLineClear?.invoke(shiftAmount, clearedLines)
Log.d(TAG, "onLineClear callback completed successfully")
} catch (e: Exception) { } catch (e: Exception) {
android.util.Log.e("GameBoard", "Error in onLineClear callback", e) Log.e(TAG, "Error in onLineClear callback", e)
} }
} }
@ -582,4 +597,11 @@ class GameBoard(
* Get the current combo count * Get the current combo count
*/ */
fun getCombo(): Int = combo fun getCombo(): Int = combo
/**
* Get the list of lines that were most recently cleared
*/
private fun getLastClearedLines(): List<Int> {
return lastClearedLines.toList()
}
} }

View file

@ -282,13 +282,15 @@
<ScrollView <ScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1"> android:layout_weight="1"
android:fillViewport="true">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:gravity="center"> android:gravity="center"
android:paddingTop="16dp">
<Button <Button
android:id="@+id/pauseStartButton" android:id="@+id/pauseStartButton"