Продолжаем изучение Java Android. На прошлом курсе мы сделали приложение “Быки и коровы”. Сама игра интересная, но нет в ней какой-нибудь мало-мальской динамики. Давайте её немного доработаем и приступим к более живеньким проектам.
Сделаем так, чтобы на ход игроку отводилось определенное время, например 30 секунд. Если игрок не успевает сделать ход, то он проиграл. Но игрока нужно подтрунивать, а для этого вывести на экран что-то похожее на таймер, чтобы игрок его видел, спешил, старался быстрее думать, но в общем всё, как в жизни.
Сначала я размещу View, куда буду потом отправлять каждый отсчет таймера:
После этого нужно сделать так, чтобы после первого хода он считал, а с каждым следующим сбрасывался и считал снова.
Тут следует сделать теперь небольшое отступление. В Android программа выполняется нелинейно. Если говорить очень просто, то процесс выполнения следующий:
- Возникает какое-нибудь событие, запускается его обработчик, возвращается из выполнения.
- Возникает новое событие, выполняется, возвращается и т.д.
Часто бывает так, что во время одного события происходит другое, ещё не дав ему завершиться, например, вы играете на телефоне в шахматы, вдруг приходит входящий звонок, который сворачивает шахматы и запускает приложение телефона. Звонок завершается, возвращается всё опять к шахматам. Т.е. события выстраиваются в очередь выполнения в разных приоритетах. Однако, есть события, которые должны выполняться независимо от очереди, либо в собственной ветке очередей. Например, вы переходите в телефоне в браузер, переходите на какую-то страницу и сразу начинаете её листать, но она при этом продолжает дозагружать картинки. Часто наверно такое встречали? Т.е. программа не заставляет вас скучно ждать пока не загрузит всю страницу целиком, а даёт возможность с ней работать по мере поступления контента. Такая работа называется параллельной. Т.е. есть пользователь и его взаимодействие с устройством, и есть что-то внутри, что должно работать параллельно. Всё заточено на то, чтобы не заставлять пользователя чего-то ждать, по крайней мере, чтобы пользователь мог изменить ход или логику работы в любой момент времени. Например, страница долго не загружается и пользователю надоело ждать, он должен в любой момент прервать этот процесс – закрыть страницу и возможно поискать другую, которая будет грузиться быстрее. Если пользователю мешать управлять устройством, либо делать это с запаздыванием, с плохим откликом, работать будет некомфортно, будет создаваться ощущение медлительности устройства. По этому совместная работа устройства и пользователя вынесена для разработчиков в отдельную парадигму и называется поток пользовательского интерфейса UIThread (User Interface Thread). Это поток, которому отводится определенное комфортное для пользователя время, как правило измеряемое миллисекундами. За это время программа должна как-то успеть отреагировать на действия пользователя и ответить на экране, если это необходимо. Всё, что превосходит это комфортное время, называется термином “Длительное выполнение” и должно быть распараллелено, т.е. вынесено в отдельный поток выполнения. Говоря иначе, если время чего-то предполагается таким, что пользователь может заметить какую-то задержку реакции, это нужно запускать отдельным потоком, и уже по мере его завершения как-то дать понять пользователю, что оно завершено, а НЕ(!) блокировать работу устройства от начала выполнения и до конца, пока оно не завершиться. Возвращаясь к примеру о браузере и странице это выглядит так – пользователь ввел адрес страницы и нажал кнопку “Перейти”. В этот момент браузер быстро принял этот адрес из поля EditText, быстро создаёт параллельный поток, передает ему этот адрес и запускает его, возвращая пользователю возможность продолжать работу с устройством. Само создание нового потока занимает столь малое время, что пользователь даже не успевает заметить, но в это время уже параллельный поток делаёт длительную операцию по обращению к серверу с нужной страницей, загрузки её в браузер, распознавание кода, дозагрузка картинок и т.п., время от времени передавая в пользовательский поток малыми порциями элементы загрузки, например, показать очередную картинку. В нашей задаче должно быть всё тоже самое. Пользователь сохраняет контроль над устройством, но параллельно приложение должно отсчитывать таймер и периодически сообщать его новое значение в пользовательский поток, который и будет уже его отображать на экране.
Самое простое создание отдельного потока – это создание экземпляра класса Runnable, давайте его добавим в наш основной класс MainActivity сразу после generateComNumber():
Теперь вначале класса MainActivity поместим несколько переменных класса:
И ещё добавим один метод где-нибудь тоже после generateComNumber():
Теперь давайте разбираться что тут для чего. Класс GameTimer наследуется от Runnable и обязательно должен реализовывать метод run(). Этот метод и есть то самое, что будет внутри него выполняться параллельно. А выполняться там будет вечный цикл, который будет прерываться каждую итерацию на 1 секунду и снова, и снова повторяться. Вся его задача – это передать короткое сообщение пользовательскому потоку. Это сообщение не содержит в себе никакой информации, т.к. сам факт этого сообщения говорит о том, что снова параллельный поток подождал 1 секунду времени и повторяет итерацию. А ведь нам это и надо, чтобы кто-то пользовательский интерфейс взбадривал каждую секунду.
Переменная sender и соответствующая константа SENDER_REFRESH совсем не обязательны, в нашем случае весь метод mainMessage() мог использоваться для реализации функционала отсчета, однако я решил показать как нужно получать параметры из другого потома, если вдруг нужно передать ещё какую-нибудь информацию.
Теперь нужно создать экземпляр этого класса и передать этот экземпляр в конструктор специального системного объекта Thread. Чтобы не вносить сумятицу, поток этот будет создаваться в специальном методе, а завершаться в противоположном (см.далее):
Можно заметить странную конструкцию у объявления переменной mainHandler. Она хранит экземпляр специального класса, который опрашивает пул поступающих сообщений, в частности будет принимать сообщения от нашего класса GameTimer и вызывать mainMessage() с этим параметром; это происходит благодаря встраиванию класса с имплементацией (метод handleMessage() как раз и есть реализация).
Чтобы параллельный потом не остался “висеть” в памяти и фанатично отматывать километры кода даже после завершения игры (он же параллельный независимый поток), нужно при завершении приложения его тоже ПРАВИЛЬНО завершить. Для этого достаточно вызвать уже заранее подготовленный нами метод:
Если посмотрим внимательно, этот метод stopAll() наш, и делает он всего лишь только то, что в переменную isRunnig помещает false, “вечный” цикл прерывается и поток выходит из выполнения, сам экземпляр класса потока уничтожится системой автоматически при завершении Активити.
Метод onStop() вызывается когда происходит остановка работы Активити (в отличии onStart(), который автоматически вызывается при старте).
Вообще порядок вызовов методов “жизни” Активити называется lifecycle и описан тут:
https://developer.android.com/guide/components/activities/activity-lifecycle
, однако по представленной там картинке и так становится понятным как всё остальное:
Тут хорошо видно, что после onStop() может быть одно из трех, но по нашему алгоритму либо окончательно выйдет из приложения, либо возвратится и повторно создаст наш таймер.
Задание по уроку:
1. Теперь, когда вы знаете метод, который у вас будет постоянно вызываться каждую секунду, реализуйте функционал уменьшения таймера и его сброс каждый ход.
2. Самостоятельно найдите описание и назначение контейнера RelativeLayout.
3. Попробуйте сделать так, чтобы когда оставшееся время становилось менее 10 секунд, таймер начинал показывать десятые доли секунды. Используйте RelativeLayout для отображения долей секунды в уменьшенном виде:
В динамике это выглядит так:
Если всё получилось как нужно – молодцы! Если нет, давайте разберёмся что не так.
Урок 10 Урок 12
El Vinto, 2023 (Copyright)