После того как вы разместили элементы отображения, задали им необходимый порядок на экране, давайте снова запустим приложение посмотреть его в двух режимах, в портретном и альбомном. Делается это при помощи кнопок поворота виртуального устройства:
В результате должно получиться как-то так:
Портретный режим:
Альбомный режим:
После этого хорошо стало видно, что игра очень плохо смотрится при такой настройке в альбомном режиме, по этому проще будет вообще такую возможность сразу отключить. Если вы замечали, некоторые приложения так и построены, как ни верти телефон, интерфейс не поворачивается. Ничего в этом плохого нет, особенно когда понимаешь, что в таком расположении будет просто не удобно при каждом повороте адаптироваться. Тут в альбомном режиме придётся часть интерфейса преносить вправо или влево и нарушается восприятие логики игры. Хотя, при желании можно и попробовать, но не сейчас. По этому познакомимся еще с одним файлом проекта под названием AndroidManifest.xml. Это файл описания структуры нашего приложения, его названия и еще много чего, что потом будет передано в итоговый файл для публикации приложения. Именно в этом файле также располагаются и все требования прав, например, такие как разрешение на доступ к дискам, доступ в интернет, доступ к камере устройства и т.д. Файл также оформлен в формате XML, с которым мы начали знакомиться на прошлом уроке, но только он описывает другие данные, но по аналогичному принципу. Сейчас в этом файле вам нужно найти контейнер, отвечающий за окно, которое делали в прошлом уроке, и добавьте новый атрибут с необходимым значением:
После этого как экран не вертите, окно будет в режиме портрета:
Верните положение виртуального телефона обратно в привычное:
Отройте вкладку с кодом, должен он выглядеть примерно так:
Начинаем разбираться. Весь основной код этого файла размещается внутри объекта “public class MainActivity…” между его фигурными скобками. Кто не читал мою статью:
Начало программирования вообще с нуля. Часть 1
крайне рекомендую. Я в этой статье объяснял что такое классы и для чего нужны. Язык Java не исключение, он тоже позволяет описывать такие объекты, как классы. Наш класс называется MainActivity и расширяет класс AppCompatActivity. Как всё происходит. При нажатии на иконку приложения в вашем мобильном телефоне для его запуска, или запуске приложения через виртуальную среду по кнопке “Run” (как вы уже это несколько раз делали), происходит сначала анализ файла AndroidManifest.xml, который мы только что смотрели и указывали там на портретное представление. Однако, там есть ещё один контейнер, который указывает, какой объект нужно активировать при запуске приложения:
Находится он внутри контейнера activity с атрибутом имени “.MainActivity”. Как мы уже знаем, что всё что находится внутри какого-то контейнера XML, относится именно к этому контейнеру. Следовательно выделенный контейнер intent-filter со всем его содержимым, относится именно к “.MainActivity”. Эти данные как раз и будут сообщать системе Android, что при запуске нужно создать объект класса MainActivity. Вы наверно спросите,- А зачем ему об этом сообщать, если ничего другого больше тут запустить нельзя?- Да, здесь действительно нельзя, но чаще всего можно. Например, вы наверно замечали, что при работе в каком-нибудь приложение вы переходите из одного окна в другое, а потом можете возвратиться назад, затем снова кликаете куда-нибудь, и текущее окно опять сменяется на другое, из которого тоже потом можно вернуться назад. Может показаться, что у нас есть один некий код, который по разным условиям просто отображает на экране новый интерфейс. Так, например, было до изобретения Андроида. Был один огромный код, который отвечал за всю программу что в ней отображается на каждом этапе (картинка входа, начальные настройки, может реклама какая, и т.д.) Пользователь кликал всё время “Далее” или “ОК”, но код был один единый на все. В Андроиде всё не так. Каждое окно принято разделять на отдельный структурный объект приложения, называемый Активити. Весь фокус в том, что когда вы перемещаетесь по приложению и открываются разные окна, это происходит приостановка работы одного Активити и запуск другого, который также помнит с какого другого Активити на него перешли. Потом вы нажимаете на кнопку “Возврат”, текущий Активити закрывается, а предыдущему снова передаётся управление. Например, рекламная заставка перед запуском приложения у многих разработчиков это тоже отдельный Активити. Как раз он и указан в манифесте как запускаемый. Например, у меня манифест приложения “Быки и коровы” выглядит так:
Сначала запускается PreActivity, где оставлена возможность показать рекламное сообщение. Оно несколько секунд его покажет, а затем при помощи специального кода автоматически откроет MainActivity. Класс RulesActivity – это Активити, которые вы тоже будете делать, в нём будут отображаться правила игры, если пользователь захочет их почитать. Вот по этому, возвращаясь к нашему вопросу, зачем указывать какой Активити нужно запускать при старте приложения, и нужно строго указать. Если этого не сделать, система не будет разбираться есть ли ещё какие Активити или их нет, сколько их,- не указали что запускать – вообще не запустит никакой Активити.
Вернемся к коду. Что происходит дальше. Система прочитала файл манифеста, узнала, что для запуска ей нужен класс MainActivity, она создает объект этого класса и вызывает метод этого класса onCreate():
Обратите внимание, что перед началом метода стоит аннотация @Override. Так делается чтобы не запутать компилятор и самому не запутаться. Это слово означает, что мы хотим в этом классе воспроизвести такой же метод, который есть в родительском классе AppCompatActivity или где-то ещё выше по уровню.
Вызов super.onCreate() необходим для правильной инициализации Активити.
Вызов setContentView передаёт Активити наш файл activity_main.xml для того чтобы система проанализировала его и на основании его данных создало интерфейс нашего Активити. Если вы закомментируете эту команду, то после запуска будет просто пустой экран:
По этому пока не будем так делать и вернём как было.
Попробуем теперь как-то получить доступ к нашему интерфейсу программным способом. Напишите следующий код внутрь метода onCreate:
После перезапуска должно получиться вот так:
Как вы поняли, таким образом мы получаем доступ к элементам интерфейса. Функция findViewById() пытается найти по идентификатору элемент нашего Активити. Однако, как я говорил уже на прошлом уроке, все элементы интерфейса – это классы, которые расширяют класс View, или как ещё часто говорят наследует класс. По этому findViewById() возвращает не именно класс, того элемента, который закреплен за этим идентификатором, а их родительский класс. Так делается исходя из рационального подхода, иначе пришлось делать на каждый поиск по ID отдельный метод со своим типом возвращаемого значения. Гораздо проще сделать одну, возвращающую родительский тип класса, а программист сам разберется к какому типу определенного элемента ему привести. Так и происходит в коде выше:
(TextView) findViewById(R.id.textViewNumber)
здесь (TextView) как раз и означает – привести к типу класса TextView. Это нам нужно для того чтобы мы смогли выполнить следующий метод setText(), применимый уже к объекту TextView, который устанавливает нужный текст. Т.е. у класса View нет такого метода, например, нельзя установить текст на контейнер ConstraintLayout, т.к. я говорил, что этот контейнер ничего не отображает, а просто расставляет элементы в нужном порядке; при этом он тоже наследуется от View. Иными словами, компилятор не даст нам сделать вот так:
Красная надпись говорит, что в таком виде скомпилирован код не будет, это ошибка
Если вы неправильно укажите приводимый тип, то скорее всего это вызовет ошибку выполнения уже во время выполнения этого кода в приложении (про ошибку выполнения я так же писал в статье о программировании с нуля).
Ошибка выполнения может также произойти, если findViewById не нашёл такой идентификатор в вашем файле activity_main.xml (тогда результат будет null), например, если такой же идентификатор был в файле другого Активити. Да, компилятор не анализирует, откуда вы тянете этот идентификатор, из этого Активити или другого, всё дело в том, что никто не запрещает менять интерфейс “на ходу”, по этому что вы делаете находится на вашей совести. Подсунуть вообще нигде несуществующий идентификатор не получится, Студио это хорошо отследит:
С текстовым полем и некоторыми особенностями немного разобрались. Теперь давайте посмотрим, как нам отработать нажатие на кнопки 0-9, чтобы нужные цифры появлялись в текстовом поле. С этим немного сложнее, но тоже разберёмся, это основы без которых нельзя:
Кнопки мы обозначали идентификаторами textView0…textView9. И вот следующее, если вы делали как я, и для кнопок использовали тоже TextView, то тогда получить объект кнопки вам нужно абсолютно аналогичным путём, например:
(TextView) findViewById(R.id.textView0)
Если же вы пошли своим путём и использовали для кнопок класс Button, то…правильно – нужно просто поменять приводимый вид:
(Button) findViewById(R.id.textView0)
Теперь нужно это всё заключить в отдельные круглые скобки чтобы сказать, что дальше мы будем обращаться не объекту View, который даёт findViewById, а к объекту TextView / Button, например:
((TextView) findViewById(R.id.textView0))
а вот теперь вызываем для этого приведенного класса вот такой метод:
((TextView) findViewById(R.id.textView0)).setOnClickListener(this);
Этот метод есть как и у TextView, так и у Button, и вообще он есть у родительского View, по тому является наследованным у всех элементов интерфейса. В данном примере, этот метод сообщает элементу textView0 (наша кнопка с цифрой “0”), что теперь это не просто элемент-кнопка с текстом “0”, а элемент, который будет отслеживать, не кликнул ли кто по нему пальцем, и если это так, то передать управление в этот наш Активити (ключевое слова this). По этому как только мы добавим этот метод, компилятор нас любезно попросит указать реализацию метода, который и примет на себя нажатие кнопки, а иначе будет проблема с компиляцией:
Слева видим, что появилась кнопка с выпадающим списком, в котором предлагается несколько решений проблемы. Нас интересует тот, который предлагает организовать реализацию требуемого метода. Далее среда разработки предложит более подробно пояснить реализацию чего мы хотим организовать. Тут обычно курсор сразу стоит там, где надо и остаётся только нажать ОК:
Кликнув на ОК, автоматически получим некоторые изменения в коде, но теперь появилась другая проблема, которую также можно решить автоматически:
Эта проблема говорит о том, что как бы ты – программист, решил реализовать интерфейс (это не тот интерфейс, который мы видим, это термин такой же, но смысл у него совершенно другой, это вносит небольшую путаницу, далее поясню) View.OnClickListener. Но для реализации нужно теперь выбрать методы этого интерфейса, которые ты хочешь прямо сейчас реализовать. Ни завтра, ни послезавтра, ни когда чаю попьёшь в раздумьях, а именно прямо сейчас, но реализовать их всех придётся по любому, теперь без этого никак. Благо что у этого интерфейса View.OnClickListener только один метод, требуемый для реализации:
Да, верно! Опять такое же окно. Океем и его тоже и опять наблюдаем автоматическое изменение кода:
В коде появился новый метод и все предупреждения об ошибках исчезли. Теперь рассказываю что всё это означает и что конкретно мы сделали.
Пока мы не стали вызывать метод setOnClickListener с параметром this, всё было хорошо. Но после его вызова мы сказали среде разработки следующую фразу:
Среда разработки, я знаю, что элемент экрана textView0 может отслеживать нажатие на него пальцем пользователя и вызывать метод onClick(). Сделай так, чтобы этот метод вызывался не просто где-то там внутри этой кнопки textView0, а ещё и вызывался у меня в моём классе тоже…ну заодно, не жалко же тебе?! У себя вызывал, сделал что-то, ну и мне тоже передай, я у себя тоже сделаю чего-нибудь теперь.
Такой подход называется имплементаций или по-русски – реализацией методов. Это работает только по тому, что когда разработчики Андроид делали классы TextView, Button и ещё много других, то они заранее предусмотрели, что программисту делающему приложение, явно понадобится по нажатию на эти элементы выполнять какие-нибудь полезные действия. Но программисты Андроида конечно же не могли знать заранее что это будут за действия у каждого программиста, по этому сделали лазейку и сказали, вот вам метод onClick(), если хотите, я буду его вызывать для вас после нажатия, но как вы будете использовать его, решайте сами в своём коде.
Интерфейсом в имплементации называется специальная конструкция кода, которую программисты класса View объявили в своём коде; как её делать я опять-таки буду рассказывать немного позже, когда наглядно смогу продемонстрировать его эффективность.
Возможно вы спросите, а почему система разработки сразу не создала все реализации на все элементы? Функцию onCreate она же создала сама, так почему на другие не сделала. Ответ прост – это не рационально. Зачем, например, мне отслеживать нажатие на надпись “МОИ ХОДЫ” или на текстовое поле с введенным числом..и самое главное – интерфейс View.OnClickListener не единственный у элементов. Элементы могут отслеживать не только событие нажатие на него пальцем. Есть событие длинного нажатия пальцем, у полей ввода есть события изменения текста, событие непосредственно перед изменением текста, есть события у списков, у скроллов, когда пролистывается экран, их сотни. Представляете как был бы загружен мусором код, сколько было бы в нем объявлено методов, которые в большинстве мы бы даже и не думали реализовывать и были бы нам не нужны.
Давайте вернёмся к коду. Перенесите теперь код отображения числа “1234” следующим образом:
Теперь перезапустите приложение. Число 1234 на экране не отобразилось. Теперь кликните мышкой в виртуальном устройстве по кнопке “0”. Теперь это число появилось только после нажатия на кнопку. На этом примере мы теперь можем понять как происходит взаимодействие в пользовательском интерфейсе – пользователь что-т нажимает, автоматически запускается определенный метод, мы его отрабатывает, возможно что-то отображаем на экране и т.д.
Для того чтобы все кнопки начали реагировать на нажатие, нужно у каждой вызвать метод setOnClickListener(this), однако реализация метода будет по прежнему одна, вам нужно будет только правильно определить какая именно нажата кнопка. Делается это вот по такому принципу:
Т.е. в методе onClick(), когда пользователь нажал на кнопку, определяем идентификатор той кнопки, на которую нажали, а затем командой if сравниваем с теми идентификаторами, которые мы давали этим кнопкам. Таким образом определяем какая же именно кнопка была нажата.
Но нам для нашего проекта потребуется сделать как-то так:
Вы наверно догадались, что такой код при каждом нажатии на кнопки, будет прибавлять справа к строке textNumber очередной символ с цифрой числа, а затем отображать его в поле textViewNumber. Обратите внимание как я прибавляю цифру “2”; это упрощённая запись, компилятор так позволяет делать. Обратите ещё внимание на то, что хотя мы в нашей игре говорим “загадать число”, по факту, мы работаем не с числом, а со строками, содержащими цифры. Это принципиальный факт.
Задание по уроку:
1. Добавьте код для обработки нажатия для всех кнопок
2. Самостоятельно найти в поисковых системах, как в Java Android уменьшить строку на один символ (стереть один символ справа), как узнать длину строки, как взять символ из середины строки, например, взять второй символ из строки, узнайте как правильно сравнить одну строку с другой.
Если всё получилось как нужно – молодцы! Если нет, давайте разберёмся что не так.
El Vinto, 2023 (Copyright)