На прошлом уроке вы сделали обработку нажатия пользователем на кнопку для каждой кнопки пользовательского интерфейса. Однако, код стал быстро расти в объёме. Давайте посмотрим вот на что:
Кто читал мою статью о программировании с нуля (Часть 1), то помнит, что обычно принято повторяющиеся команды помещать внутрь цикла. Это делает код рациональным и более близким к восприятию (программист, читающий код, будет понимать, что внутри цикла всё тоже самое, только как-то зависит от самого цикла). В нашем примере (а у вас должна быть уже написана обработка на каждую клавишу), мы должны объявить инициализацию реализации нажатия на каждую кнопку. Давайте теперь мы попробуем сделать так. Сначала создадим массив целых чисел и сразу же заполним его значениями:
Т.е. мы просто в массив перенесли идентификаторы всех наших цифровых(!) кнопок. Обратите внимание, что идентификатор – это целое число (иначе бы среда разработки начала “ругаться”).
Теперь внутри onCreate() поменяйте код инициализации на такой:
Согласитесь, так выглядит намного компактнее. Почему я не внёс три служебные кнопки в массив, по аналогии, будет понятно далее.
Теперь, конечно, хотело бы также оптимизировать и обработку нажатия. И тут появляется несколько возможных решений. Если бы у нас массив содержал сразу соответствие с цифрой числа и содержал бы только кнопки цифр 0-9 мы могли бы как-то это представить циклом. Можно конечно расписать каждую кнопку отдельно. Я так тоже часто делаю, чтобы легко было искать нужное с точки зрения программирования. Хотя если проявить творчество, то можно сделать и по третьему варианту, давайте его рассмотрим (но все варианты одинаково возможны); заодно сразу сделаем заготовки для служебных кнопок:
Рассмотрим какие изменения произошли. Первое, что мы сделали – это вынесли получение идентификатора кнопки в отдельную переменную id. Благодаря этому вызов функции getId() будет происходить только один раз. Это мы оптимизировали с точки зрения скорости выполнения. Далее определяем, если переменная textNumber, которая накапливает нажатия на цифровые кнопки, по длине меньше 4, то можно добавить ещё одну, ведь по правилам игрок должен загадать именно 4-х значное число. Далее запускаем цикл, в котором перебираем весь массив идентификаторов наших кнопок и сравнивает, идентификатор нажатой кнопки не равен ли идентификатору из массива. Если это так, то прибавим к textNumber индекс элемента массива.
Тут хочу обратить внимание вот на что – индексы массивов считаются от нуля (самый первый элемент массива с индексом ноль), и значения этого массива – это идентификаторы элементов наших кнопок тоже от кнопки с цифрой ноль. По этому значение идентификаторов цифровых кнопок, соответствует индексу массива. По этому-то и в сам массив мы отправили только цифровые кнопки, и не стали мешать служебные кнопки. В теории, конечно можно было бы сделать так:
Однако, это очень плохой вариант. Мы бы стали строить идеологию проекта исходя из того, что массив кроме цифровых содержит и другие кнопки, а потом, по прошествии какого-то времени, забыли бы, что мы должны использовать первые 10 элементов только(!) для цифровых кнопок и сделали бы, например, вот так:
По этому будем делать так, чтобы исключить из программирования пагубные привычки. Дело в том, что если это интерфейс пользователя, то ошибку такую мы сразу найдём (у нас по цифре ноль начнёт появляться на экране единица, а по единице – двойка). А вот если это какой-то другой массив, который например формирует какие-то расчетные данные, то заметить это можно будет только по прошествии многих часов поиска проблемы, ну или например, когда…Луна-XX вдруг полетит по другой траектории и накроет труд тысяч людей медным тазом…
Идем по анализу кода далее. Внимательный слушатель курса заметит один момент. У нас индекс массива – это целое число, а переменная textNumber – это строка, но при этом я делаю так:
Да, компилятор позволяет так делать, хотя по всем канонам вообще правильным было бы так:
Но такое делать компилятор умеет не всегда правильно. Например, если вы сделаете как-то так:
это приведёт к крэшу приложения (остановка работы по фатальной ошибке с принудительным выходом из приложения) . Вся проблема тут была в том, что компилятор подумал, что в качестве целочисленного значения вы передаёте ему не число 34, а идентификатор ресурса (кто разобрался как делать домашнее задание одного из прошлых уроков, понял о чём это я), который как раз тоже является целым числом. Поскольку идентификатора ресурса с ID=34 в приложении не существует, это вызвало ошибку выполнения. Т.е. вот следующий код прошел бы на “Ура!”:
Так, слишком отвлеклись…В нашем примере textNumber += i компилятор понял, что мы конечно же хотим именно число привести к типу String (просто здесь наиболее вероятным было бы ожидать от программиста, что он поставит сюда именно число, а не идентификатор ресурса, потому разработчики компилятора и сделали так), а потом произвести конкатенацию (сложение строк, когда к одной строке присоединяется другая строка справа или слева).
Если условие поиска цифровой кнопки отработало, то значит нажата была именно цифровая кнопка, мы выполнили конкатенацию, теперь должны обновить на экране пользователя это число и выйти из метода (больше там сравнивать вообще не чего).
Если пользователь нажал кнопку Backspace (стереть последний символ), т.е. кнопку с идентификатором textViewBackspace и при этом текущая длина строки больше нуля (ну, когда там вообще есть что стирать), то выполняем выражение, которое стирает последний символ строки. Кто по честному выполняет домашние задания, уже знает, что функция substring() получает часть строки. В качестве первого параметра указывается позиция в строке, с которой нужно взять эту часть, а в качестве второго параметра указывается позиция до которой взять (НЕ включительно).
Теперь измените значение по-умолчанию для поля (кто этого ещё не сделал):
и перезапустите приложение. Проверьте ввод цифр и возможность их стирания. Всё должно работать.
Что мне не особо нравится в работе этого кода, так это отображение знаков “_”. Сначала они есть и это смотрится красиво, но по мере набора и стирания они пропадают, а по идее хорошо бы, если они выглядели как-то так (да и хорошо бы введенные цифры блокировались, а при стирании разблокировались):
Для начала немного изменим код обработки нажатия:
Тут среда разработки начнёт подсказывать, типа “А что это такая за странная функция? Я её не знаю”. Конечно же не знает, мы её ещё только сейчас начнём писать. По этому добавьте её ниже метода onClick() и сделайте её сразу правильно:
В этом методе мы должны сначала создать отдельную переменную numberAsText, которая будет хранить число вместе со знаками подчеркивания “_”. Так же сразу выполним цикл, который отметит все кнопки доступными для нажатия setEnabled(true) и укажем для них зеленоватый цвет. Далее выполним цикл от 1 до 4 в котором будем делать следующее. Если индекс цикла больше длины строки введенного уже нами числа, то заполнить это знаком подчеркивания (эти знаки в конце после цифр), в ином случае взять цифру из нашего числа по этому индексу (в виде целого числа) и поместить в эту нашу новую переменную numberAsText, после чего взять по этому числу как по индексу идентификатор элемента пользовательского интерфейса и отметить кнопку как недоступную для нажатия и установить на неё серый цвет фона. Далее выполняется заодно код, который определяет длину строки (длина строки по логике – это как раз число разрядов числа), и если эта длина равна 4, то отобразить кнопку “Enter”, в ином случае скрыть её. Обязательно разберитесь с этой частью кода чтобы хорошо понимать каждое действие, что мы делаем каждым выражением. Обращаю внимание, ранее мы представляли из целого числа строку и компилятор это смог сделать самостоятельно, а вот наоборот из строки сделать число он уже не может, по этому необходимо выполнить соответствующую функцию Integer.parseInt(). Это нужно нам как число, т.к. дальше нам нужно обратиться к элементу массива digButtons, где аргументом не может быть строка.
Мы с вами уже познакомились с некоторыми инициализациями переменных, пора бы теперь поговорить о них немного подробнее. Давайте сначала разберемся какие вообще могут быть переменные.
String a = “”; // это строка, мы уже несколько раз такое делали в нашем коде
int b = 0; // что-то похожее делали буквально только что, когда преобразовывали из строки в целое число
boolean c = false; // Это булева переменная, которая может принимать значение только истина или ложь (true / false)
long d = 123; // длинная целая переменная, позволяет хранить максимальное целое число в два раза большее, чем int.
float e = 12.3; // позволяет хранить нецелые числа (дробная часть отделяется точкой)
double f = 12.34353453; // Позволяет хранить нецелые числа, но в два раза с большей точностью, чем float
ClassVariable g = null; // Переменная, которая хранит ссылку на какой-то класс. Например, вот так:
Есть и ещё, но пока этих достаточно.
Теперь пришло время поговорить об области видимости переменных. У нас есть уже переменная textNumber, которую я объявил вне какого-либо метода, но внутри класса:
Такая переменная называется переменной класса и видна из любого метода класса (к ней можно обратиться из любого метода, мы так и делаем, то из onClick(), то из setNumber() ).
Есть переменные метода, например int id в методе onClick() или String numberAsText в методе setNumber(). Они видны только из того метода, где объявлены. Т.е. я, например, не могу обратиться к numberAsText из метода onClick(), среда разработки не даст это сделать.
Переменные, которые объявлены внутри объявления цикла видны в этом объявлении и везде внутри этого цикла, снаружи уже не видны. Например, int i в цикле:
for (int i = 0; i < digButtons.length; i++ /* да, тут видна i */) {
/* где-то тут тоже видна i */
}
/* а тут уже i не видна, здесь её использовать не получится */ .
Переменные, которые объявлены внутри условия, видны везде внутри этого условия, например, int num внутри else:
if (i > textNumber.length()) {
/* тут вообще не знает что такое num */
} else {
/* кстати, если что, и тут не известно что такое num */
int num;
/* тут видна num */
}
Идем теперь далее. Перед началом каждого раунда, игра должна сгенерировать 4-х значное случайное число. Это будет то самое число, которое загадал компьютер и которое предстоит узнать человеку в процессе определенного числа попыток. Функционал Java позволяет сгенерировать целое случайное число. Делается это в два этапа. Сначала объявляется переменная класса Random, а затем выполняется один из её методов, отвечающих за генерацию числа в определенном диапазоне:
Random random = new Random();
int rndNum = random.nextInt(10);
В данном примере rndNum будет содержать случайное значение в диапазоне от 0 до 9, т.е. как раз одну случайную цифру. Однако, тут вы возможно воскликните,- Э-э-э, автор, у тебя что-то с математикой не в порядке! Нам надо 4-х разрядное число, а это тогда диапазон не до 10, а до 10 000. )))…Но я наверно сделал так тоже не спроста, наверно потому что задача не так тривиальна. Помимо того, что нам нужно сгенерировать 4-х значное число, нам нужно ещё и чтобы все цифры были в нём разные)). По этому объективных решения здесь минимум два, я просто выбрал одно из них, которое счет наиболее простым в реализации…В домашнем задании попробуйте разные способы.
Иногда возникает ситуация, когда в процессе разработки нужно посмотреть значение какой-либо переменной, но выводить это на экран телефона не всегда удобно – для этого придётся создавать какой-то элемент textView для отображения, а это вносит сумятицу в разработку. По этому разработчиками был придуман метод логгирования. Давайте, например, проверим какие числа будут сгенерированы случайным методом, описанным только что, например, с 10 попыток. Делается это примерно так:
Кстати, тут может возникнуть ещё вот такая ситуация:
Т.е. среда разработки не находит функцию Log. Наведите на это слово курсор мыши, в результате появится знакомая кнопка решения проблемы:
Нужно импортировать класс. Вся проблема тут в том, что изначально в приложение на загружаются весь функционал Андроида, т.е. например, зачем загружать в приложения огромный пласт кода для работе с трехмерной графикой, если вы делаете обычный калькулятор. Также и тут, изначально среда разработки не включила функционал логгирования в ваше приложение, но уж коли вы хотите его использовать, то скажите об этом наверняка. Иногда может получиться так, что библиотека импорта на выбор будет не одна (например, если эта функция с таким именем встречается сразу в нескольких различных библиотеках и выполняет несколько разные по смыслу действия), то в этом случае придётся разбираться и возможно гуглить, откуда же её тянуть. В данном случае, библиотека для импорта только одна: android.util.Log.
Может получиться и ещё одна ситуация, когда при наведении куда-то мышью, вдруг система разработки внезапно очнётся и увидит, что решение есть, но пользовательский контекст другой:
Тут спрашивает, мол это из android.util.Log? Тогда жми одновременно на клавиши Alt+Enter!
В действительности, будет добавлена библиотека импорта (в самом верху кода):
Перезапускаем приложение, но смотрим не виртуальный мобильник, а на экран разработки. В результате получится вот так:
Ещё давайте рассмотрим небольшую конструкцию, которую я использовал, это вот такое выражение:
i++
На самом деле – это упрощённая запись i = i+1 (до этого вы могли и догадаться), т.е. взять переменную i, прибавить к ней 1, а результат снова поместить в переменную i. Например, если до этого выражения в переменной i было значение 4, то после будет значение 5.
Нельзя пройти стороной и похожую конструкцию:
++i
Которая делает абсолютно тоже самое, но только как чесать левой рукой правое ухо.
Сделайте так чтобы посмотреть разницу и перезапустите приложение (мы немного отвлеклись и поиспользуем наше приложение для некоторых тестов):
Нет, здесь нет признаков корпускулярно-волнового дуализма, когда присутствие наблюдателя меняет результат. Возможно вы догадались, почему так произошло. Правильно! num1++ прибавило к num1 значение после выполнения всего выражения, которым был в конечном счете вывод лога, а выражение ++num2 сделало инкремент (увеличение на единицу) до того как начало выполнять выражение. В этом и вся разница. Если мы сделаем так:
То результат будет абсолютно одинаковый:
По этому я часто выношу p++ или ++p за пределы выражения как самостоятельные выражения…но не всегда)). Например, добавлять значения какого-нибудь массива удобно вот так:
ar[i++] = 5;
ar[i++] = 6;
ar[i++] = 7;
А брать значение с последнего вот так:
int len = ar.length;
while (len > 0) {
value = ar[–len];
// …
}
Задание по уроку:
1. Попробуйте самостоятельно сделать метод для генерации 4-х значного числа. Для его хранения сделайте строковую переменную класса compNumber.
2. Сделайте так, чтобы при нажатии на кнопку Enter программа считала число “Быков” в угадываемом числе. Проверьте метод логгирования в разных вариантах.
3. Ответьте, какое значение p будет после выполнения следующих действий, после этого проверьте своё решение при помощи метода логгирования:
p = 3;
p = p+++p;
Если всё получилось как нужно – молодцы! Если нет, давайте разберёмся что не так.
Урок 4 Урок 6
El Vinto, 2023 (Copyright)