Всем привет! В этой статье для начинающих я постараюсь шаг за шагом помочь вам создать настольное приложение с помощью Jetpack Compose. Мы создадим классическую игру «Камень-ножницы-бумага», чтобы она стала для вас увлекательной и познавательной. Итак, начинаем учиться!
Полный код: https://github.com/basilinnia/RockPaperScissors-DesktopGame.git
1. Начиная с пользовательского интерфейса
Чтобы вам было легче следить, я подготовил файл Figma со всем, что у нас есть, дизайном главного экрана и игрового экрана. Мы реализуем оба экрана в двух разных темах: темной и светлой.
Вот ссылка на файл Figma: интерфейс Figma Rock-Paper-Scissors
2. Создание проекта и темы
Я использую IntelliJ IDEA Community Edition 2022.1.2 для этого проекта. И мой проект выглядит так:
После создания моего проекта я добавляю расширенную зависимость значков, которую я буду использовать позже:
И это моя структура проекта:
По сути, я храню свои файлы шрифтов и движущиеся изображения в ресурсах, а каталог темы содержит «Theme.kt», в котором мы определяем темные и светлые цвета темы, и Font.kt, который содержит шрифты. Вы можете получить файлы изображений и шрифтов из моего репозитория. Начнем с файла Theme.kt:
package theme import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.material.darkColors import androidx.compose.material.lightColors import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color // You can change the colors val LightTheme = lightColors( //affect the surfaces of components surface = Color(0xffD9D9D9), onSurface = Color(0xff070A52), //for key components across the UI primary = Color(0xffDF2E38), secondaryVariant = Color.Black, // that sits on top of primary onPrimary = Color.White, // that sits on top of secondary onSecondary = Color(0xffAF2D2D) ) val DarkTheme = darkColors( surface = Color(0xff353535), onSurface = Color(0xffBEBFD1), primary = Color(0xff2751A3), onPrimary = Color.White, secondaryVariant = Color.White, onSecondary =Color(0xff6881D8) ) @Composable fun AppTheme( // Shows which theme are you in isDark: Boolean = true, content: @Composable () -> Unit, ) { MaterialTheme( colors = if (isDark) DarkTheme else LightTheme // Use the appropriate color list ) { Surface { content() } } }
Чтобы понять, что делают эти цветовые схемы, вы можете проверить эту документацию и узнать больше. Затем у нас есть Fonts.kt, в основном мы получаем файлы шрифтов из нашего каталога resources , а затем добавляем вес шрифта:
val RedHatDisplay = FontFamily( Font("fonts/RedHatDisplay/RedHatDisplay-Medium.ttf", FontWeight.Medium), Font("fonts/RedHatDisplay/RedHatDisplay-SemiBold.ttf", FontWeight.SemiBold), )
Вы можете проверить файлы шрифтов и получить их отсюда: https://fonts.google.com
3. Начиная с главного экрана
- Аннотация
@Preview
сообщает Android Studio, что этот компонуемый объект должен отображаться в режиме конструктора этого файла. - Компонуемая функция
App
— это точка входа приложения. Эти переменные используются для отслеживания темы приложения (темный или светлый режим) и текущего состояния экрана (отображается ли основной экран или экран игры). - Функция
main()
устанавливает окно приложения с заголовком «Камень, ножницы, бумага» и фиксированным размером окна 800x650 dp. Затем он вызывает компонуемыйApp()
для запуска приложения.
@Composable @Preview fun App() { //we're using "remember" because app's theme and screen //state persist during updates. val themeState = remember { mutableStateOf(true) } val screenState = remember { mutableStateOf(true) } AppTheme(isDark = themeState.value) { Box( modifier = Modifier.fillMaxSize().padding(vertical = 16.dp), contentAlignment = Alignment.TopEnd ) { // basically this is our theme toggler icon IconButton( modifier = Modifier.padding(horizontal = 16.dp), //theme changes on every click onClick = ({ themeState.value = !themeState.value }) ) { Icon( if (themeState.value) Icons.Outlined.LightMode else Icons.Outlined.DarkMode, contentDescription = "Icon", modifier = Modifier.size(48.dp) ) } //change screen if (screenState.value) { MainScreen { screenState.value = false } } else { GameScreen { screenState.value = true } } } } } //takes an lambda function which returns nothing as a parameter @Composable fun MainScreen(navigateToGameScreen: ()-> Unit ) { //Main Screen code } fun main() = application { Window(onCloseRequest = ::exitApplication, title = "Rock Paper Scissors", state = rememberWindowState(size = DpSize(width = 800.dp, height = 650.dp)) // Set the window size here ) { App() } }
По сути, после нажатия кнопки в компоновке MainScreen мы переключаемся на игровой экран:
Button( shape = RoundedCornerShape(14), onClick = {navigateToGameScreen()}, modifier = Modifier.padding(end = 30.dp).size(120.dp, 80.dp) ) { Text( color = Color.White, fontSize = 22.sp, fontFamily = Nunito, fontWeight = FontWeight.Bold, text = "PLAY" ) }
Итак, это наш главный экран:
4. Создание игрового экрана
Чтобы избежать сложных длинных кодов, я разделил GameScreen на такие части:
// available moves val moves = listOf("ROCK", "PAPER", "SCISSORS") @Composable fun GameScreen(navigateToMainScreen: () -> Unit) { val (playerMove, setPlayerMove) = remember { mutableStateOf("ROCK") } val computerMove = remember{mutableStateOf("ROCK")} val playerScore = remember { mutableStateOf(0) } val computerScore = remember { mutableStateOf(0) } Column(modifier = Modifier.fillMaxWidth()) { // Other screen elements like reset button and scores } // Shows the result Text(getWinner(playerMove, computerMove.value), fontFamily = RedHatDisplay, fontSize = 46.sp, fontWeight = FontWeight.Bold, color = colors.onSecondary) // Shows the images of moves CurrentMove(playerMove,computerMove.value) // Move buttons Moves(setPlayerMove, computerMove, playerScore, computerScore) } } }
Во-первых, давайте запустим функции, которые обновляют другие элементы, функция getWinner сравнивает ходы и возвращает строковое сообщение в соответствии с победителем, вкратце это логика игры:
fun getWinner(playerMove: String, computerMove: String): String { return when { playerMove == computerMove -> "DRAW" (playerMove == "ROCK" && computerMove == "SCISSORS") || (playerMove == "PAPER" && computerMove == "ROCK") || (playerMove == "SCISSORS" && computerMove == "PAPER") -> "YOU WON\uD83C\uDF89!" else -> "COMPUTER WON\uD83C\uDF89!" } }
Затем мы проверяем текст, который возвращает функция getWinner, и затем решаем, счет какого игрока будет увеличен в соответствии с текстом:
fun updateScores(winner: String, playerScore:MutableState<Int>, computerScore:MutableState<Int>) { when (winner) { "YOU WON\uD83C\uDF89!"-> playerScore.value += 1 "COMPUTER WON\uD83C\uDF89!"->computerScore.value += 1 } }
Затем у нас есть компонуемый Moves, который обновляет текущий ход, затем выполняет сравнение между игроками, а затем возвращает что-то в соответствии с победившей стороной при каждом нажатии:
@Composable fun Moves(setMove: (String) -> Unit, computerMove: MutableState<String>, playerScore:MutableState<Int>, computerScore:MutableState<Int>) { Text("Choose your move, rock paper or scissors?", color = Color.Gray) Row(modifier = Modifier.fillMaxWidth().padding(vertical = 25.dp), horizontalArrangement = Arrangement.SpaceAround) { for (move in moves) { Button( shape = RoundedCornerShape(14), onClick = { setMove(move) computerMove.value = moves.random() updateScores(getWinner(move, computerMove.value), playerScore, computerScore) }, modifier = Modifier.size(180.dp, 60.dp) ) { Text(fontWeight = FontWeight.ExtraBold, fontFamily = Nunito, text = move) } } } }
Кроме того, мы упомянули изображения ходов в нашем каталоге ресурсов, с помощью этой функции, по сути, мы получаем имена ходов и меняем изображения в соответствии с именами:
@Composable fun CurrentMove(playerMove: String, computerMove:String) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceAround, verticalAlignment = Alignment.CenterVertically ) { Image(painter = painterResource("moves/$playerMove.svg"), contentDescription = null, modifier = Modifier.size(230.dp), colorFilter = ColorFilter.tint(colors.secondaryVariant) ) Text(fontWeight = FontWeight.ExtraBold, fontFamily = Nunito, text = "VS") Image( painter = painterResource("moves/$computerMove.svg"), contentDescription = null, // set color from the color scheme in our themes colorFilter = ColorFilter.tint(colors.secondaryVariant), // rotating the second image to better UI modifier = Modifier.size(230.dp).rotate(180f) ) } }
Наконец, это полный код GameScreen, в котором мы объединили все составные части:
@Composable fun GameScreen(navigateToMainScreen: () -> Unit) { val (playerMove, setPlayerMove) = remember { mutableStateOf("ROCK") } val computerMove = remember{mutableStateOf("ROCK")} val playerScore = remember { mutableStateOf(0) } val computerScore = remember { mutableStateOf(0) } Column(modifier = Modifier.fillMaxWidth()) { Row(verticalAlignment = Alignment.CenterVertically) { // go back to the main screen IconButton(onClick = navigateToMainScreen) { Icon(Icons.Outlined.ArrowBack, contentDescription = "back") } Text( fontWeight = FontWeight.ExtraBold, fontSize = 26.sp, text = "Rock Paper Scissors with Compose", fontFamily = Nunito ) } Column( modifier = Modifier.fillMaxSize().padding(horizontal = 15.dp, vertical = 15.dp), verticalArrangement = Arrangement.SpaceBetween, horizontalAlignment = Alignment.CenterHorizontally ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { // resets the scores TextButton(onClick = { playerScore.value = 0 computerScore.value = 0 }) { Text( color = colors.secondaryVariant, fontFamily = SignikaNegative, fontSize = 20.sp, text = "Reset The Tour", fontWeight = FontWeight.SemiBold ) } Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceAround ) { // shows the player score Text( fontFamily = RedHatDisplay, color = colors.secondaryVariant, text = "PLAYER SCORE: ${playerScore.value}", fontSize = 15.sp ) // shows the computer score Text( fontFamily = RedHatDisplay, modifier = Modifier.padding(start = 120.dp), color = colors.secondaryVariant, text = "COMPUTER SCORE: ${computerScore.value}", fontSize = 15.sp ) } } // add other composables Text(getWinner(playerMove, computerMove.value), fontFamily = RedHatDisplay, fontSize = 46.sp, fontWeight = FontWeight.Bold, color = colors.onSecondary) CurrentMove(playerMove,computerMove.value) Moves(setPlayerMove, computerMove, playerScore, computerScore) } } }
И это результат! Надеюсь, вам понравилось! Если у вас есть вопросы или рекомендации, дайте мне знать!