adding gamepad support

This commit is contained in:
Corey 2025-03-31 04:52:11 -04:00
parent 86424eac32
commit 0ac25eb3a9
5 changed files with 796 additions and 5 deletions

View file

@ -31,8 +31,15 @@ import android.util.Log
import android.view.KeyEvent import android.view.KeyEvent
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.view.MotionEvent
import com.mintris.game.GamepadController
import android.view.InputDevice
import android.widget.Toast
import android.content.BroadcastReceiver
import android.content.IntentFilter
import android.app.AlertDialog
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity(), GamepadController.GamepadConnectionListener {
companion object { companion object {
private const val TAG = "MainActivity" private const val TAG = "MainActivity"
@ -66,6 +73,24 @@ class MainActivity : AppCompatActivity() {
// Activity result launcher for high score entry // Activity result launcher for high score entry
private lateinit var highScoreEntryLauncher: ActivityResultLauncher<Intent> private lateinit var highScoreEntryLauncher: ActivityResultLauncher<Intent>
// Gamepad controller
private lateinit var gamepadController: GamepadController
// Input device change receiver
private val inputDeviceReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
Intent.ACTION_CONFIGURATION_CHANGED -> {
// A gamepad might have connected or disconnected
checkGamepadConnections()
}
}
}
}
// Track connected gamepads to detect changes
private val connectedGamepads = mutableSetOf<String>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
// Register activity result launcher for high score entry // Register activity result launcher for high score entry
highScoreEntryLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> highScoreEntryLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
@ -93,6 +118,10 @@ class MainActivity : AppCompatActivity() {
themeSelector = binding.themeSelector themeSelector = binding.themeSelector
blockSkinSelector = binding.blockSkinSelector blockSkinSelector = binding.blockSkinSelector
// Initialize gamepad controller
gamepadController = GamepadController(gameView)
gamepadController.setGamepadConnectionListener(this)
// Set up touch event forwarding // Set up touch event forwarding
binding.touchInterceptor?.setOnTouchListener { _, event -> binding.touchInterceptor?.setOnTouchListener { _, event ->
if (gameView.visibility == View.VISIBLE && !gameView.isPaused && !gameView.isGameOver()) { if (gameView.visibility == View.VISIBLE && !gameView.isPaused && !gameView.isGameOver()) {
@ -231,8 +260,35 @@ class MainActivity : AppCompatActivity() {
updateUI(score, level, lines) updateUI(score, level, lines)
} }
gameView.onGameOver = { score -> gameView.onGameOver = { finalScore ->
showGameOver(score) // Pause music on game over
gameMusic.pause()
// Update high scores
val timePlayedMs = System.currentTimeMillis() - gameStartTime
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
val currentDate = dateFormat.format(Date())
// Track if this is a high score
val isHighScore = highScoreManager.isHighScore(finalScore)
// Show game over screen
showGameOver(finalScore)
// Save player stats to track game history
statsManager.updateSessionStats(finalScore, gameBoard.lines, piecesPlaced, timePlayedMs, currentLevel)
// Handle progression - XP earned, potential level up
val xpGained = progressionManager.calculateGameXP(finalScore, gameBoard.lines, currentLevel, timePlayedMs, statsManager.getSessionTetrises(), 0)
val newRewards = progressionManager.addXP(xpGained)
// Show progression screen if player earned XP
if (xpGained > 0) {
// Delay showing progression screen for a moment
Handler(Looper.getMainLooper()).postDelayed({
showProgressionScreen(xpGained, newRewards)
}, 2000)
}
} }
gameView.onLineClear = { lineCount -> gameView.onLineClear = { lineCount ->
@ -577,12 +633,83 @@ class MainActivity : AppCompatActivity() {
) )
} }
/**
* Start a new game
*/
private fun startGame() { private fun startGame() {
// Set initial game state
currentScore = 0
currentLevel = selectedLevel
piecesPlaced = 0
gameStartTime = System.currentTimeMillis()
// Update UI to show initial values
binding.scoreText.text = "$currentScore"
binding.currentLevelText.text = "$currentLevel"
binding.linesText.text = "0"
// Reset game view and game board
gameView.reset()
// Configure callbacks
gameView.onGameStateChanged = { score, level, lines ->
currentScore = score
currentLevel = level
binding.scoreText.text = "$score"
binding.currentLevelText.text = "$level"
binding.linesText.text = "$lines"
}
gameView.onGameOver = { finalScore ->
// Pause music on game over
gameMusic.pause()
// Update high scores
val timePlayedMs = System.currentTimeMillis() - gameStartTime
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
val currentDate = dateFormat.format(Date())
// Track if this is a high score
val isHighScore = highScoreManager.isHighScore(finalScore)
// Show game over screen
showGameOver(finalScore)
// Save player stats to track game history
statsManager.updateSessionStats(finalScore, gameBoard.lines, piecesPlaced, timePlayedMs, currentLevel)
// Handle progression - XP earned, potential level up
val xpGained = progressionManager.calculateGameXP(finalScore, gameBoard.lines, currentLevel, timePlayedMs, statsManager.getSessionTetrises(), 0)
val newRewards = progressionManager.addXP(xpGained)
// Show progression screen if player earned XP
if (xpGained > 0) {
// Delay showing progression screen for a moment
Handler(Looper.getMainLooper()).postDelayed({
showProgressionScreen(xpGained, newRewards)
}, 2000)
}
}
// Connect line clear callback to gamepad rumble
gameView.onLineClear = { lineCount ->
// Vibrate phone
gameHaptics.vibrateForLineClear(lineCount)
// Vibrate gamepad if connected
gamepadController.vibrateForLineClear(lineCount)
}
// Start the game
gameView.start() gameView.start()
gameMusic.setEnabled(isMusicEnabled) gameMusic.setEnabled(isMusicEnabled)
// Start background music if enabled
if (isMusicEnabled) { if (isMusicEnabled) {
gameMusic.start() gameMusic.start()
} }
gameStartTime = System.currentTimeMillis() gameStartTime = System.currentTimeMillis()
piecesPlaced = 0 piecesPlaced = 0
statsManager.startNewSession() statsManager.startNewSession()
@ -612,10 +739,25 @@ class MainActivity : AppCompatActivity() {
gameView.pause() gameView.pause()
gameMusic.pause() gameMusic.pause()
} }
// Unregister broadcast receiver
try {
unregisterReceiver(inputDeviceReceiver)
} catch (e: IllegalArgumentException) {
// Receiver wasn't registered
}
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
// Register for input device changes
val filter = IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED)
registerReceiver(inputDeviceReceiver, filter)
// Check for already connected gamepads
checkGamepadConnections()
// If we're on the title screen, don't auto-resume the game // If we're on the title screen, don't auto-resume the game
if (titleScreen.visibility == View.GONE && gameView.visibility == View.VISIBLE && binding.gameOverContainer.visibility == View.GONE && binding.pauseContainer.visibility == View.GONE) { if (titleScreen.visibility == View.GONE && gameView.visibility == View.VISIBLE && binding.gameOverContainer.visibility == View.GONE && binding.pauseContainer.visibility == View.GONE) {
resumeGame() resumeGame()
@ -783,10 +925,99 @@ class MainActivity : AppCompatActivity() {
} }
/** /**
* Completely block the hardware back button during gameplay * GamepadConnectionListener implementation
*/
override fun onGamepadConnected(gamepadName: String) {
runOnUiThread {
Toast.makeText(this, "Gamepad connected: $gamepadName", Toast.LENGTH_SHORT).show()
// Provide haptic feedback for gamepad connection
gameHaptics.vibrateForPieceLock()
// Show gamepad help if not shown before
if (!hasSeenGamepadHelp()) {
showGamepadHelpDialog()
}
}
}
override fun onGamepadDisconnected(gamepadName: String) {
runOnUiThread {
Toast.makeText(this, "Gamepad disconnected: $gamepadName", Toast.LENGTH_SHORT).show()
}
}
/**
* Show gamepad help dialog
*/
private fun showGamepadHelpDialog() {
val dialogView = layoutInflater.inflate(R.layout.gamepad_help_dialog, null)
val dialog = android.app.AlertDialog.Builder(this, android.R.style.Theme_DeviceDefault_Dialog_NoActionBar)
.setView(dialogView)
.setCancelable(true)
.create()
// Set up dismiss button
dialogView.findViewById<View>(R.id.gamepad_help_dismiss_button).setOnClickListener {
dialog.dismiss()
markGamepadHelpSeen()
}
dialog.show()
}
/**
* Check if user has seen the gamepad help
*/
private fun hasSeenGamepadHelp(): Boolean {
val prefs = getSharedPreferences("com.mintris.preferences", Context.MODE_PRIVATE)
return prefs.getBoolean("has_seen_gamepad_help", false)
}
/**
* Mark that user has seen the gamepad help
*/
private fun markGamepadHelpSeen() {
val prefs = getSharedPreferences("com.mintris.preferences", Context.MODE_PRIVATE)
prefs.edit().putBoolean("has_seen_gamepad_help", true).apply()
}
/**
* Check for connected/disconnected gamepads
*/
private fun checkGamepadConnections() {
val currentGamepads = GamepadController.getConnectedGamepadsInfo().toSet()
// Find newly connected gamepads
currentGamepads.filter { it !in connectedGamepads }.forEach { gamepadName ->
onGamepadConnected(gamepadName)
}
// Find disconnected gamepads
connectedGamepads.filter { it !in currentGamepads }.forEach { gamepadName ->
onGamepadDisconnected(gamepadName)
}
// Update the stored list of connected gamepads
connectedGamepads.clear()
connectedGamepads.addAll(currentGamepads)
}
/**
* Handle key events for both device controls and gamepads
*/ */
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
// If back button is pressed if (event == null) return super.onKeyDown(keyCode, event)
// First check if it's a gamepad input
if (GamepadController.isGamepad(event.device)) {
// If gamepad input was handled by the controller, consume the event
if (gamepadController.handleKeyEvent(keyCode, event)) {
return true
}
}
// If not handled as gamepad, handle as regular key event
// Handle back button
if (keyCode == KeyEvent.KEYCODE_BACK) { if (keyCode == KeyEvent.KEYCODE_BACK) {
// Handle back button press as a pause action during gameplay // Handle back button press as a pause action during gameplay
if (gameView.visibility == View.VISIBLE && !gameView.isPaused && !gameView.isGameOver()) { if (gameView.visibility == View.VISIBLE && !gameView.isPaused && !gameView.isGameOver()) {
@ -807,9 +1038,24 @@ class MainActivity : AppCompatActivity() {
return true // Consume the event return true // Consume the event
} }
} }
return super.onKeyDown(keyCode, event) return super.onKeyDown(keyCode, event)
} }
/**
* Handle motion events for gamepad analog sticks
*/
override fun onGenericMotionEvent(event: MotionEvent): Boolean {
// Check if it's a gamepad motion event (analog sticks)
if ((event.source and InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK) {
if (gamepadController.handleMotionEvent(event)) {
return true
}
}
return super.onGenericMotionEvent(event)
}
private fun showProgressionScreen(xpGained: Long, newRewards: List<String>) { private fun showProgressionScreen(xpGained: Long, newRewards: List<String>) {
// Apply theme before showing the screen // Apply theme before showing the screen
progressionScreen.applyTheme(currentTheme) progressionScreen.applyTheme(currentTheme)

View file

@ -1413,4 +1413,71 @@ class GameView @JvmOverloads constructor(
gameOverBlocksSpeed[i] *= 1.03f gameOverBlocksSpeed[i] *= 1.03f
} }
} }
/**
* Check if the game is active (running, not paused, not game over)
*/
fun isActive(): Boolean {
return isRunning && !isPaused && !gameBoard.isGameOver
}
/**
* Move the current piece left (for gamepad/keyboard support)
*/
fun moveLeft() {
if (!isActive()) return
gameBoard.moveLeft()
gameHaptics?.vibrateForPieceMove()
invalidate()
}
/**
* Move the current piece right (for gamepad/keyboard support)
*/
fun moveRight() {
if (!isActive()) return
gameBoard.moveRight()
gameHaptics?.vibrateForPieceMove()
invalidate()
}
/**
* Rotate the current piece (for gamepad/keyboard support)
*/
fun rotate() {
if (!isActive()) return
gameBoard.rotate()
gameHaptics?.vibrateForPieceMove()
invalidate()
}
/**
* Perform a soft drop (move down faster) (for gamepad/keyboard support)
*/
fun softDrop() {
if (!isActive()) return
gameBoard.softDrop()
gameHaptics?.vibrateForPieceMove()
invalidate()
}
/**
* Perform a hard drop (instant drop) (for gamepad/keyboard support)
*/
fun hardDrop() {
if (!isActive()) return
gameBoard.hardDrop()
// Hard drop haptic feedback is handled by the game board via onPieceLock
invalidate()
}
/**
* Hold the current piece (for gamepad/keyboard support)
*/
fun holdPiece() {
if (!isActive()) return
gameBoard.holdPiece()
gameHaptics?.vibrateForPieceMove()
invalidate()
}
} }

View file

@ -0,0 +1,372 @@
package com.mintris.game
import android.os.SystemClock
import android.view.InputDevice
import android.view.KeyEvent
import android.view.MotionEvent
import android.util.Log
import android.content.Context
import android.os.Build
import android.os.VibrationEffect
import android.view.InputDevice.MotionRange
import android.os.Vibrator
/**
* GamepadController handles gamepad input for the Mintris game.
* Supports multiple gamepad types including:
* - Microsoft Xbox controllers
* - Sony PlayStation controllers
* - Nintendo Switch controllers
* - Backbone controllers
*/
class GamepadController(
private val gameView: GameView
) {
companion object {
private const val TAG = "GamepadController"
// Deadzone for analog sticks (normalized value 0.0-1.0)
private const val STICK_DEADZONE = 0.25f
// Cooldown times for responsive input without repeating too quickly
private const val MOVE_COOLDOWN_MS = 100L
private const val ROTATION_COOLDOWN_MS = 150L
private const val HARD_DROP_COOLDOWN_MS = 200L
private const val HOLD_COOLDOWN_MS = 250L
// Rumble patterns
private const val RUMBLE_MOVE_DURATION_MS = 20L
private const val RUMBLE_ROTATE_DURATION_MS = 30L
private const val RUMBLE_HARD_DROP_DURATION_MS = 100L
private const val RUMBLE_LINE_CLEAR_DURATION_MS = 150L
// Check if device is a gamepad
fun isGamepad(device: InputDevice?): Boolean {
if (device == null) return false
// Check for gamepad via input device sources
val sources = device.sources
return (sources and InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD ||
(sources and InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK
}
// Get a list of all connected gamepads
fun getGamepads(): List<InputDevice> {
val gamepads = mutableListOf<InputDevice>()
val deviceIds = InputDevice.getDeviceIds()
for (deviceId in deviceIds) {
val device = InputDevice.getDevice(deviceId)
if (device != null && isGamepad(device)) {
gamepads.add(device)
}
}
return gamepads
}
// Check if any gamepad is connected
fun isGamepadConnected(): Boolean {
return getGamepads().isNotEmpty()
}
// Get the name of the first connected gamepad
fun getConnectedGamepadName(): String? {
val gamepads = getGamepads()
if (gamepads.isEmpty()) return null
return gamepads.first().name
}
// Get information about all connected gamepads
fun getConnectedGamepadsInfo(): List<String> {
return getGamepads().map { it.name }
}
// Check if device supports vibration
fun supportsVibration(device: InputDevice?): Boolean {
if (device == null) return false
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return false
return device.vibratorManager?.vibratorIds?.isNotEmpty() ?: false
}
}
// Timestamps for cooldowns
private var lastMoveTime = 0L
private var lastRotationTime = 0L
private var lastHardDropTime = 0L
private var lastHoldTime = 0L
// Track current directional input state
private var isMovingLeft = false
private var isMovingRight = false
private var isMovingDown = false
// Callback interfaces
interface GamepadConnectionListener {
fun onGamepadConnected(gamepadName: String)
fun onGamepadDisconnected(gamepadName: String)
}
// Listener for gamepad connection
private var connectionListener: GamepadConnectionListener? = null
// Currently active gamepad for rumble
private var activeGamepad: InputDevice? = null
/**
* Set a listener for gamepad connection events
*/
fun setGamepadConnectionListener(listener: GamepadConnectionListener) {
connectionListener = listener
}
/**
* Function to check for newly connected gamepads.
* Call this periodically from the activity to detect connection changes.
*/
fun checkForGamepadChanges(context: Context) {
// Implementation would track previous and current gamepads
// and notify through the connectionListener
// This would be called from the activity's onResume or via a handler
}
/**
* Vibrate the gamepad if supported
*/
fun vibrateGamepad(durationMs: Long, amplitude: Int) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return
val gamepad = activeGamepad ?: return
if (supportsVibration(gamepad)) {
try {
val vibrator = gamepad.vibratorManager
val vibratorIds = vibrator.vibratorIds
if (vibratorIds.isNotEmpty()) {
val effect = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
VibrationEffect.createOneShot(durationMs, amplitude)
} else {
// For older devices, fall back to a simple vibration
VibrationEffect.createOneShot(durationMs, VibrationEffect.DEFAULT_AMPLITUDE)
}
// Create combined vibration for Android S+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val combinedVibration = android.os.CombinedVibration.createParallel(effect)
vibrator.vibrate(combinedVibration)
}
}
} catch (e: Exception) {
Log.e(TAG, "Error vibrating gamepad", e)
}
}
}
/**
* Vibrate for piece movement
*/
fun vibrateForPieceMove() {
vibrateGamepad(RUMBLE_MOVE_DURATION_MS, 50)
}
/**
* Vibrate for piece rotation
*/
fun vibrateForPieceRotation() {
vibrateGamepad(RUMBLE_ROTATE_DURATION_MS, 80)
}
/**
* Vibrate for hard drop
*/
fun vibrateForHardDrop() {
vibrateGamepad(RUMBLE_HARD_DROP_DURATION_MS, 150)
}
/**
* Vibrate for line clear
*/
fun vibrateForLineClear(lineCount: Int) {
val amplitude = when (lineCount) {
1 -> 100
2 -> 150
3 -> 200
else -> 255 // For tetris (4 lines)
}
vibrateGamepad(RUMBLE_LINE_CLEAR_DURATION_MS, amplitude)
}
/**
* Process a key event from a gamepad
* @return true if the event was handled, false otherwise
*/
fun handleKeyEvent(keyCode: Int, event: KeyEvent): Boolean {
// Skip if game is not active
if (!gameView.isActive()) return false
val device = event.device
if (!isGamepad(device)) return false
// Update active gamepad for rumble
activeGamepad = device
val currentTime = SystemClock.uptimeMillis()
when (event.action) {
KeyEvent.ACTION_DOWN -> {
when (keyCode) {
// D-pad and analog movement
KeyEvent.KEYCODE_DPAD_LEFT -> {
if (!isMovingLeft && currentTime - lastMoveTime > MOVE_COOLDOWN_MS) {
gameView.moveLeft()
vibrateForPieceMove()
lastMoveTime = currentTime
isMovingLeft = true
return true
}
}
KeyEvent.KEYCODE_DPAD_RIGHT -> {
if (!isMovingRight && currentTime - lastMoveTime > MOVE_COOLDOWN_MS) {
gameView.moveRight()
vibrateForPieceMove()
lastMoveTime = currentTime
isMovingRight = true
return true
}
}
KeyEvent.KEYCODE_DPAD_DOWN -> {
if (!isMovingDown && currentTime - lastMoveTime > MOVE_COOLDOWN_MS) {
gameView.softDrop()
vibrateForPieceMove()
lastMoveTime = currentTime
isMovingDown = true
return true
}
}
KeyEvent.KEYCODE_DPAD_UP -> {
if (currentTime - lastHardDropTime > HARD_DROP_COOLDOWN_MS) {
gameView.hardDrop()
vibrateForHardDrop()
lastHardDropTime = currentTime
return true
}
}
// Rotation buttons - supporting multiple buttons for different controllers
KeyEvent.KEYCODE_BUTTON_A,
KeyEvent.KEYCODE_BUTTON_X,
KeyEvent.KEYCODE_BUTTON_B -> {
if (currentTime - lastRotationTime > ROTATION_COOLDOWN_MS) {
gameView.rotate()
vibrateForPieceRotation()
lastRotationTime = currentTime
return true
}
}
// Hold piece buttons
KeyEvent.KEYCODE_BUTTON_Y,
KeyEvent.KEYCODE_BUTTON_L1,
KeyEvent.KEYCODE_BUTTON_R1 -> {
if (currentTime - lastHoldTime > HOLD_COOLDOWN_MS) {
gameView.holdPiece()
vibrateForPieceRotation()
lastHoldTime = currentTime
return true
}
}
}
}
KeyEvent.ACTION_UP -> {
when (keyCode) {
KeyEvent.KEYCODE_DPAD_LEFT -> {
isMovingLeft = false
return true
}
KeyEvent.KEYCODE_DPAD_RIGHT -> {
isMovingRight = false
return true
}
KeyEvent.KEYCODE_DPAD_DOWN -> {
isMovingDown = false
return true
}
}
}
}
return false
}
/**
* Process generic motion events (for analog sticks)
* @return true if the event was handled, false otherwise
*/
fun handleMotionEvent(event: MotionEvent): Boolean {
// Skip if game is not active
if (!gameView.isActive()) return false
val device = event.device
if (!isGamepad(device)) return false
// Update active gamepad for rumble
activeGamepad = device
val currentTime = SystemClock.uptimeMillis()
// Process left analog stick
val axisX = event.getAxisValue(MotionEvent.AXIS_X)
val axisY = event.getAxisValue(MotionEvent.AXIS_Y)
// Apply deadzone
if (Math.abs(axisX) > STICK_DEADZONE) {
if (axisX > 0 && !isMovingRight && currentTime - lastMoveTime > MOVE_COOLDOWN_MS) {
gameView.moveRight()
vibrateForPieceMove()
lastMoveTime = currentTime
isMovingRight = true
isMovingLeft = false
return true
} else if (axisX < 0 && !isMovingLeft && currentTime - lastMoveTime > MOVE_COOLDOWN_MS) {
gameView.moveLeft()
vibrateForPieceMove()
lastMoveTime = currentTime
isMovingLeft = true
isMovingRight = false
return true
}
} else {
// Reset horizontal movement flags when stick returns to center
isMovingLeft = false
isMovingRight = false
}
if (Math.abs(axisY) > STICK_DEADZONE) {
if (axisY > 0 && !isMovingDown && currentTime - lastMoveTime > MOVE_COOLDOWN_MS) {
gameView.softDrop()
vibrateForPieceMove()
lastMoveTime = currentTime
isMovingDown = true
return true
}
} else {
// Reset vertical movement flag when stick returns to center
isMovingDown = false
}
// Check right analog stick for rotation
val axisZ = event.getAxisValue(MotionEvent.AXIS_Z)
val axisRZ = event.getAxisValue(MotionEvent.AXIS_RZ)
if (Math.abs(axisZ) > STICK_DEADZONE || Math.abs(axisRZ) > STICK_DEADZONE) {
if (currentTime - lastRotationTime > ROTATION_COOLDOWN_MS) {
gameView.rotate()
vibrateForPieceRotation()
lastRotationTime = currentTime
return true
}
}
return false
}
}

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#AA000000" />
<corners android:radius="16dp" />
<stroke android:width="2dp" android:color="#FFFFFF" />
</shape>

View file

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp"
android:background="@drawable/dialog_background">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Gamepad Controls"
android:textSize="24sp"
android:textStyle="bold"
android:textColor="#FFFFFF"
android:layout_marginBottom="16dp"
android:gravity="center"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Movement"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="#FFFFFF"
android:layout_marginBottom="8dp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="D-Pad Left/Right or Left Stick: Move piece"
android:textSize="16sp"
android:textColor="#FFFFFF"
android:layout_marginBottom="4dp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="D-Pad Down or Left Stick Down: Soft drop"
android:textSize="16sp"
android:textColor="#FFFFFF"
android:layout_marginBottom="4dp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="D-Pad Up: Hard drop"
android:textSize="16sp"
android:textColor="#FFFFFF"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Actions"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="#FFFFFF"
android:layout_marginBottom="8dp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="A/B/X or Right Stick: Rotate piece"
android:textSize="16sp"
android:textColor="#FFFFFF"
android:layout_marginBottom="4dp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Y, L1, or R1: Hold piece"
android:textSize="16sp"
android:textColor="#FFFFFF"/>
</LinearLayout>
<Button
android:id="@+id/gamepad_help_dismiss_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Got it!"
android:layout_gravity="center"
android:paddingLeft="24dp"
android:paddingRight="24dp"/>
</LinearLayout>