Первые шаги в Android-программировании
Всегда самое сложное в любом занятии – это начать. Это касается и начала разработки приложений для операционной системы Android. Ведь не всегда понятно, что написано в тех или иных руководствах – банально сложно воспринять должным образом все нужные понятия, а первые шаги описаны зачастую недостаточно детально и хорошо – так как пишут их уже матёрые программисты, и они сложные для восприятия новичком.
Этот досадный недостаток мы попытаемся исправить при помощи данной статьи.
Итак, статья будет посвящена полному циклу создания Android-приложения, от начала и до конца. Писать мы будем простую игру «Крестики-Нолики» для одного экрана с использованием Activity. Но чтобы не перегружать вас терминами с самого начала, всё по порядку.
Если опыта разработки с использованием языка Java у вас нет – это не проблема, так как достаточно понимания общей сути работы с тем же PHP или другими языками. В любом случае, всегда всё будет наглядно видно на примерах.
Для разработки нужны инструменты. А для разработки приложений под ОС Android их три, это: JDK (Java Development Kit), Android SDK and AVD Manager, а также IDE с поддержкой разработки для системы Android (либо Eclipse с плагином ADT, либо IntelliJ IDEA Community Edition, либо Netbeans с плагином nbandroid). Перечисленные утилиты очень желательно устанавливать именно в таком порядке. Из IDE использовать сразу все смысла нет никакого – нужно выбрать одну. Мы будем работать на примере IntelliJ IDEA Community Edition – данная среда разработки достаточно удобна и развита.
Итак, нужно запустить виртуальное устройство. Запускаем AVD Manager, устанавливаем дополнительные пакеты (это SDK разных версий) и приступаем к самому созданию виртуального Android-девайса, задав интересующие параметры.
Список виртуальных устройств:
Теперь приступим к созданию нового проекта: «File» -> «New Project»:
Чтобы проект был собран, скомпилирован и запущен на нашем виртуальном устройстве, нужно нажать F6.
Разберем структуру проекта. Её видно на последнем скриншоте. Разберем папки, которые нас интересуют применимо конкретно к данному проекту: gen, res, src.
Директория gen содержит файлы, которые генерируются автоматически в процессе сборки проекта, и вручную изменить их не получится.
Директория res нужна для хранения ресурсов приложения, например картинок, текстов, layouts (макетов интерфейса) и так далее.
Директория src – это основная «строительная площадка», где происходит большая часть работы. Именно здесь хранятся файлы с исходным кодом.
Как только происходит создание Activity, то есть экрана приложения, вызывается метод onCreate(). IDE сразу заполняет его следующими двумя строками кода:
super.onCreate(savedInstanceState); setContentView(R.layout.main);
Метод setContentView (то же самое, что this.setContentView) устанавливаем для текущего экрана соответствующий .xml-макет. Впредь будем называть «макеты» как «layout», а «экраны», соответственно, — «Activity». Так вот, в нашем приложении Layout будет иметь следующий вид:
<?xml version="1.0" encoding="utf-8"?> <TableLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:id="@+id/main_l" android:gravity="center" > </TableLayout>
Для нашего приложения отлично подойдет TableLayout. Любому ресурсу можно присвоить Id. В нашем случае TableLayout присвоен id равный main_l. Используя метод findViewById() получаем доступ к виду:
private TableLayout layout; // это свойство класса KrestikinolikiActivity public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); layout = (TableLayout) findViewById(R.id.main_l); buildGameField(); }
Теперь переходим к реализации метода buildGameField(). Чтобы сделать это, нам нужно сгенерировать поле в виде матрицы. Это будет делать класс Game. Но сначала создаем класс Square для ячеек, а также класс Player, объекты из которого будут заполнять эти самые ячейки. Итак, Square.java:
package com.example; public class Square { private Player player = null; public void fill(Player player) { this.player = player; } public boolean isFilled() { if (player != null) { return true; } return false; } public Player getPlayer() { return player; } } А вот Player.java: package com.example; public class Player { private String name; public Player(String name) { this.name = name; } public CharSequence getName() { return (CharSequence) name; } }
Как уже говорилось, все созданные для нашего приложения классы находятся в папке src.
Game.java:
package com.example; public class Game { /** * поле */ private Square[][] field; /** * Конструктор * */ public Game() { field = new Square[3][3]; squareCount = 0; // заполнение поля for (int i = 0, l = field.length; i < l; i++) { for (int j = 0, l2 = field[i].length; j < l2; j++) { field[i][j] = new Square(); squareCount++; } } } public Square[][] getField() { return field; } }
Инициализируем класс Game.java в конструкторе KrestikinolikiActivity:
public KrestikinolikiActivity() { game = new Game(); game.start(); // будет реализован позже }
Метод buildGameField() из класса KrestikinolikiActivity динамически добавляет строки и колонки в таблицу, которая в нашем случае является игровым полем:
private Button[][] buttons = new Button[3][3]; //(....) private void buildGameField() { Square[][] field = game.getField(); for (int i = 0, lenI = field.length; i < lenI; i++ ) { TableRow row = new TableRow(this); // создание строки таблицы for (int j = 0, lenJ = field[i].length; j < lenJ; j++) { Button button = new Button(this); buttons[i][j] = button; button.setOnClickListener(new Listener(i, j)); // установка слушателя, реагирующего на клик по кнопке row.addView(button, new TableRow.LayoutParams(TableRow.LayoutParams.WRAP_CONTENT, TableRow.LayoutParams.WRAP_CONTENT)); // добавление кнопки в строку таблицы button.setWidth(107); button.setHeight(107); } layout.addView(row, new TableLayout.LayoutParams(TableLayout.LayoutParams.WRAP_CONTENT, TableLayout.LayoutParams.WRAP_CONTENT)); // добавление строки в таблицу } }
Создаем объект, который реализует интерфейс View.OnClickListener. Создаем вложенный класс Listener, который будет виден только из KrestikinolikiActivity:
public class Listener implements View.OnClickListener { private int x = 0; private int y = 0; public Listener(int x, int y) { this.x = x; this.y = y; } public void onClick(View view) { Button button = (Button) view; } }
Теперь реализуем логику нашей игры «Крестики-нолики» по всем известным правилам:
public class Game { /** * игроки */ private Player[] players; /** * поле */ private Square[][] field; /** * начата ли игра? */ private boolean started; /** * текущий игрок */ private Player activePlayer; /** * Считает колличество заполненных ячеек */ private int filled; /** * Всего ячеек */ private int squareCount; /** * Конструктор * */ public Game() { field = new Square[3][3]; squareCount = 0; // заполнение поля for (int i = 0, l = field.length; i < l; i++) { for (int j = 0, l2 = field[i].length; j < l2; j++) { field[i][j] = new Square(); squareCount++; } } players = new Player[2]; started = false; activePlayer = null; filled = 0; } public void start() { resetPlayers(); started = true; } private void resetPlayers() { players[0] = new Player("X"); players[1] = new Player("O"); setCurrentActivePlayer(players[0]); } public Square[][] getField() { return field; } private void setCurrentActivePlayer(Player player) { activePlayer = player; } public boolean makeTurn(int x, int y) { if (field[x][y].isFilled()) { return false; } field[x][y].fill(getCurrentActivePlayer()); filled++; switchPlayers(); return true; } private void switchPlayers() { activePlayer = (activePlayer == players[0]) ? players[1] : players[0]; } public Player getCurrentActivePlayer() { return activePlayer; } public boolean isFieldFilled() { return squareCount == filled; } public void reset() { resetField(); resetPlayers(); } private void resetField() { for (int i = 0, l = field.length; i < l; i++) { for (int j = 0, l2 = field[i].length; j < l2; j++) { field[i][j].fill(null); } } filled = 0; } }
Естественно, побеждает в «Крестики-нолики» тот, кто выстраивает линию из крестиков или ноликов на всю длину поля по горизонтали, вертикали или диагонали. В первую очередь навязывается мысль написать отдельные методы для каждого случая. Для этого отлично подходит pattern Chain of Responsibility. Определим наш интерфейс:
package com.example; public interface WinnerCheckerInterface { public Player checkWinner(); }
Класс Game обязан выявлять победителя, то он и реализует данный интерфейс. А теперь создадим виртуальных «смотрителей», каждый из которых будет производить проверку своей стороны. Всех их будет реализовать интерфейс WinnerCheckerInterface. Итак, WinnerCheckerHorizontal.java:
package com.example; public class WinnerCheckerHorizontal implements WinnerCheckerInterface { private Game game; public WinnerCheckerHorizontal(Game game) { this.game = game; } public Player checkWinner() { Square[][] field = game.getField(); Player currPlayer; Player lastPlayer = null; for (int i = 0, len = field.length; i < len; i++) { lastPlayer = null; int successCounter = 1; for (int j = 0, len2 = field[i].length; j < len2; j++) { currPlayer = field[i][j].getPlayer(); if (currPlayer == lastPlayer && (currPlayer != null && lastPlayer !=null)) { successCounter++; if (successCounter == len2) { return currPlayer; } } lastPlayer = currPlayer; } } return null; } }
Далее, WinnerCheckerVertical.java:
package com.example; public class WinnerCheckerVertical implements WinnerCheckerInterface { private Game game; public WinnerCheckerVertical (Game game) { this.game = game; } public Player checkWinner() { Square[][] field = game.getField(); Player currPlayer; Player lastPlayer = null; for (int i = 0, len = field.length; i < len; i++) { lastPlayer = null; int successCounter = 1; for (int j = 0, len2 = field[i].length; j < len2; j++) { currPlayer = field[j][i].getPlayer(); if (currPlayer == lastPlayer && (currPlayer != null && lastPlayer !=null)) { successCounter++; if (successCounter == len2) { return currPlayer; } } lastPlayer = currPlayer; } } return null; } }
И, наконец, WinnerCheckerDiagonalLeft.java:
package com.example; public class WinnerCheckerDiagonalLeft implements WinnerCheckerInterface { private Game game; public WinnerCheckerDiagonalLeft(Game game) { this.game = game; } public Player checkWinner() { Square[][] field = game.getField(); Player currPlayer; Player lastPlayer = null; int successCounter = 1; for (int i = 0, len = field.length; i < len; i++) { currPlayer = field[i][i].getPlayer(); if (currPlayer != null) { if (lastPlayer == currPlayer) { successCounter++; if (successCounter == len) { return currPlayer; } } } lastPlayer = currPlayer; } return null; } }
Не забываем про вторую диагональ. WinnerCheckerDiagonalRight.java:
package com.example; public class WinnerCheckerDiagonalRight implements WinnerCheckerInterface { private Game game; public WinnerCheckerDiagonalRight(Game game) { this.game = game; } public Player checkWinner() { Square[][] field = game.getField(); Player currPlayer; Player lastPlayer = null; int successCounter = 1; for (int i = 0, len = field.length; i < len; i++) { currPlayer = field[i][len - (i + 1)].getPlayer(); if (currPlayer != null) { if (lastPlayer == currPlayer) { successCounter++; if (successCounter == len) { return currPlayer; } } } lastPlayer = currPlayer; } return null; } }
А теперь инициализируем их в конструкторе Game:
//(....) /** * "Судьи" =). После каждого хода они будут проверять, * нет ли победителя */ private WinnerCheckerInterface[] winnerCheckers; //(....) public Game() { //(....) winnerCheckers = new WinnerCheckerInterface[4]; winnerCheckers[0] = new WinnerCheckerHorizontal(this); winnerCheckers[1] = new WinnerCheckerVertical(this); winnerCheckers[2] = new WinnerCheckerDiagonalLeft(this); winnerCheckers[3] = new WinnerCheckerDiagonalRight(this); //(....) }
Вот сама реализация checkWinner():
public Player checkWinner() { for (WinnerCheckerInterface winChecker : winnerCheckers) { Player winner = winChecker.checkWinner(); if (winner != null) { return winner; } } return null; }
Проверку победителя осуществляем после каждого сделанного хода. Добавляем код в метод onClick() класса Listener:
public void onClick(View view) { Button button = (Button) view; Game g = game; Player player = g.getCurrentActivePlayer(); if (makeTurn(x, y)) { button.setText(player.getName()); } Player winner = g.checkWinner(); if (winner != null) { gameOver(winner); } if (g.isFieldFilled()) { // в случае, если поле заполнено gameOver(); } }
Метод gameOver() реализуется в двух возможных вариантах (победа одного или другого игрока, или же ничья):
private void gameOver(Player player) { CharSequence text = "Player "" + player.getName() + "" won!"; Toast.makeText(this, text, Toast.LENGTH_SHORT).show(); game.reset(); refresh(); } private void gameOver() { CharSequence text = "Draw"; Toast.makeText(this, text, Toast.LENGTH_SHORT).show(); game.reset(); refresh(); }
Отметим, что для Java, gameOver(Player player) и gameOver() – это разные методы. Используя Builder Toast.makeText, быстро создаем и выводим уведомление. Refresh() – обновляет текущее состояние поля:
private void refresh() { Square[][] field = game.getField(); for (int i = 0, len = field.length; i < len; i++) { for (int j = 0, len2 = field[i].length; j < len2; j++) { if (field[i][j].getPlayer() == null) { buttons[i][j].setText(""); } else { buttons[i][j].setText(field[i][j].getPlayer().getName()); } } } }
Вот и всё, мы создали простенькую игру «Крестики-нолики».
Одно дело – создавать несложные программы и игры, и совсем другое – разрабатывать мобильные приложения для сегмента электронной коммерции и сферы развлечений.
Посмотрите, как осуществляется и сколько стоит разработка под android для серьезных проектов.