Напоминаю, что на наших уроках мы разрабатывает приложение – логическую игру “Быки и коровы”. На прошлом уроке мы остановились перед тем, как начать знакомиться с таким функционалом, как списки. Да, это не просто объект или элемент пользовательского интерфейса, а целый функционал, т.к. комплекс взаимодействующих между собой методов и классов для отображения списков. Казалось бы что проще – вывести на экран один за другим элементы, содержащие строки, но вот тут и кроется вся сложность. Дело в том, что при выводе списка, Андроид выводит не строку, а целый набор элементов пользовательского интерфейса (текст, картинки, флажки и т.п.). Причём меняться они могут, что называется, налету, т.е. в процессе подготовки к выводу на экран. Сам список, тоже может меняться, модифицироваться. Данные списка – это не просто строки, это могут быть массивы различных типов. Однако, давайте начнем по порядку.
Первое, что нужно сделать, это поместить в наш пользовательский интерфейс сам элемент списка и дать ему соответствующие привязки (вообще-то мы это уже сделали ранее, просто не вникали зачем):
Привязки на первый взгляд кажутся странными, но если присмотреться внимательно, то окажется, что список тянется сразу в два направления, верх подтягивается к верхнему над ним элементу, а нижняя часть списка – к нижнему элементу. Вспомним, что самый верхний элемент на экране тянется к верху экрана, а самый нижний – к низу экрана. Таким образом, список будет вытягиваться по высоте в зависимости от того какой высоты экран. Т.е. он станет по высоте динамическим, адаптивным под каждое устройство.
Теперь перейдем к коду. Сначала я создам класс, который будет отражать данные каждого элемента списка:
Т.е. я хочу сделать так, чтобы каждый элемент списка хранил не только простой текст, а отгадываемый вариант числа и сколько у него было быков и коров. Теперь я сделаю класс, который будет наследоваться от класса обслуживающего списки:
public static class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder> {
ArrayList<RecycleElement> recycleElements;
private Context context;
private LayoutInflater inflater;
class ViewHolder extends RecyclerView.ViewHolder {
private TextView bulls;
private TextView number;
private TextView cows;
public ViewHolder(View itemView) {
super(itemView);
bulls = (TextView) itemView.findViewById(R.id.textViewBulls);
number = (TextView) itemView.findViewById(R.id.textViewBCNumber);
cows = (TextView) itemView.findViewById(R.id.textViewCows);
}
}
RecyclerViewAdapter(Context context, ArrayList<RecycleElement> recycleElements) {
this.context = context;
this.inflater = LayoutInflater.from(context);
this.recycleElements = recycleElements;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View v = inflater.inflate(R.layout.recycleelement_bc, parent, false);
return new ViewHolder(v);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, final int position) {
RecycleElement recycleElement = recycleElements.get(position);
String bulls = “”;
String cows = “”;
if (recycleElement.bulls == 1) {
bulls = “1 бык”;
} else if (recycleElement.bulls > 1) {
bulls = “”+recycleElement.bulls+” быка”;
}
if (recycleElement.cows == 1) {
cows = “1 корова”;
} else if (recycleElement.cows > 1) {
cows = “”+recycleElement.cows+” коровы”;
}
holder.bulls.setText(bulls);
holder.number.setText(recycleElement.number);
holder.cows.setText(cows);
}
@Override
public int getItemCount() {
return recycleElements.size();
}
}
В этом коде, возможно, вам покажется всё сложным, по этому пока особо не вникайте в некоторые конструкции языка, чуть позже всё это разберем. Сейчас остановимся на некоторых моментах. Это класс RecyclerViewAdapter, который мы создаём, наследуется от стандартного RecyclerView с той целью, чтобы могли взять некоторые части управления на себя, а именно – формирование каждого элемента списка из макета (аналогично active_main.xml для формирования пользовательского интерфейса). За формирование отображение отвечает специальный функционал inflater. В качестве одного из параметров ему передаётся ресурс с xml, который и хранит в себе набор элементов пользовательского интерфейса для каждого элемента списка. В данном случае это ресурс R.layout.recycleelement_bc. Добавьте его к проекту следующим образом. Кликните правой кнопкой мыши по папке res и выберите следующий пункт контекстного меню:
Дайте ему имя recycleelement_bc в поле “Layout File Name”. После этого будет создан новый файл проекта и он появится в уже знакомой вкладке; перейдем в ней сразу в подвкладку Code:
Замените находящейся в ней XML на следующий:
Заметьте, что у элементов появились незнакомые атрибуты:
- gravity – указывает к какому краю TextView будет прислоняться находящийся внутри текст. “end|center_vertical” указывает что одновременно по концу текста (в данном случае по правому краю) по горизонтали и по центру по вертикали.
- layout_margin – указывает, что между краями TextView и самим текстом должен быть отступ со всех 4-х сторон равный 4 условные единицы (это чтобы текст одного TextView визуально не примыкал вплотную к тексту другого TextView или иного элемента.
Тут есть ещё одна особенность. Раньше значения параметра gravity были не start и end (начало и конец), а назывались left и right (лево и право). Но потом почему-то называние было изменено. Найти где-то конкретное описание этому изменению мне не удалось, но полагаю связано это с тем, что письменность в некоторых иностранных языках идет не слева-направо, а справа-налево, как например, в арабском. Я думаю, новые значения параметров сделаны как раз для адаптации к этому, что бы в зависимости от языка не приходилось менять left-right местами вручную. Как понимаете, на форумах можно найти по прежнему старые варианты значений.
Теперь вновь возвратимся к классу RecyclerViewAdapter. Заметьте ещё одну интересную конструкцию:
ArrayList<RecycleElement> recycleElements;
В Java Android есть массивы, вы их уже видели, но есть и ещё один похожий объект – ArrayList. Особой разницы между ними нет, кроме того, что массив заранее имеет определенный размер, а ArrayList динамически может менять размер. Т.е. если бы в данном случае мы захотели использовать массив, он выглядел бы как-то так:
RecycleElement recycleElements[ТутКакоетоЧислоЭлементов];
Однако, для списка мы не можем использовать массив, т.к. не знаем заранее до какого размера будет разрастаться наш список. Сразу резервировать память под большое число не рационально. Вообще, если сравнивать их между собой с точки зрения рациональности, то получается такая зависимость (имеется ввиду для ArrayList и массивов большого размера):
Массив:
- Высокая производительность
- Большой расход памяти
ArrayList:
- Низкая производительность
- Низкий расход памяти
Т.е. если для массива вам сразу нужно выделить память такого размера, чтобы влезли туда все элементы, которых может не быть сейчас, но могут появиться потом, то для ArrayList этого не надо, он заполняет память по мере необходимости, но в ущерб скорости работы. Каждый обладает своими плюсами и минусами и именно вам выбирать что использовать в данных определенный момент времени.
Далее, метод onBindViewHolder в этом нашем классе списка – это имплементация (реализация) базового класса, которую, как мы помним, нужно делать всегда. Этот метод как раз отвечает за вывод на экран каждого элемента списка буквально перед самим его выводом. По этому мы можем кое что поменять и заполнить элементы списка так, как нам нужно. В качестве одно из параметров этого метода выступает целочисленный параметр position, в котором система сообщает какой именно сейчас выводится элемент списка. И вот как раз мы в этот момент берём данные этого элемента из нашего ArrayList, немного модифицируем и выводим в нужные элементы элемента списка (извините за тавтологию).
Взять элемент по текущей позиции:
RecycleElement recycleElement = recycleElements.get(position);
Сделать строки текста, понятные для игрока с учетом правил русского языка:
Запихнуть нужный текст в нужный элемент:
Теперь давайте разберемся, откуда программа знает что такое вот это:
Это мы объявили и проинициализировали в нашем классе чуть ранее:
В параметр itemView системой будет передан как раз тот набор элементов, которые мы описали в файле recycleelement_bc.xml. По этому мы должны уже знакомой функцией findViewById() искать не в activity_main или где-то ещё, а именно в нашем recycleelement_bc.xml, ведь его мы тоже передали нашему списку, если помните, вот тут:
Теперь осталось только правильно объявиться и все переменные и список будет готов. Объявите три переменные классе MainActivity:
Да, всё правильно, если вы заметили recycleElements мы уже объявляли, но внутри нашего класса списка и без инициализации, здесь инициализацию (new ArrayList() ) важна, без неё не заработает.
Далее в метод onCreate() добавьте код:
recyclerView – будет хранить в себе элемент пользовательского интерфейса – список; вспомните файл activity_main.xml. Создайте для него менеджер LinearLayoutManager как показано, это тоже необходимо, иначе список не будет обновляться.
recyclerViewAdapter будет хранить экземпляр нашего класса RecyclerViewAdapter, в качестве одного параметра мы передаём текущий класс, а в качестве второго наш объявленный и проинициализированный список. Посмотрите сейчас почему именно два параметра, ведь когда мы создавали наш класс, мы именно так и указали, что для создания экземпляра нам потребуется именно два этих параметра:
Первый был Context (к которому компилятор автоматически привёл наш this, а второй – список ArrayList).
Далее выполняем:
recyclerView.setAdapter(recyclerViewAdapter);
Это позволяет связать наш элемент пользовательского интерфейса recycleView с нашим новый классом. Теперь, когда на экране нужно будет системе перерисовать наш список, наряду с другими элементами, она будет понимать, что для исходных данных этого списка, нужно использовать какой-то класс программиста, в который нужно передавать управление перед отображением каждого элемента списка. Возвращаясь к одному из предыдущих уроков, когда программисты Андроид делали систему, они конечно же не знали как программисты приложений будут выводить списки и какие данные захотят отображать, в каком виде будет это отображение, по этому просто оставили лазейку, мол вот я вывожу элемент списка, могу, программист, и тебе дать возможность там что-то изменить, если хочешь; мы просто этим воспользовались.
Следующая команда
recyclerViewAdapter.notifyDataSetChanged();
запускает процесс регенерации списка. Именно тут она не нужна (только если вы не хотите задать начальный список и сразу же это показать). Однако, познакомить с ней я решил сейчас. Т.е. как понимаете, это команда сообщает системе следующее – “Система, мой список уже недостоверен, какие-то данные там изменены, может какие-то элементы уже удалены, может что-то появилось новое, перерисуй мне весь список заново”.
Теперь измените немного код обработки кнопки ввода числа в методе onClick():
Заметьте, что здесь notifyDataSetChanged() как раз выполняет свою функцию – перезапускает отрисовку элементов (у нас появился новый элемент списка и надо отобразить обновленный список). Перезапустите приложение, после ввода чисел и нажатия на кнопку ввода, должно получаться как-то так:
Задание по уроку:
1. Сделайте так, чтобы если в варианте числа 4 коровы, то в списке он отмечались всегда зеленым цветов
2. Добавьте в список ещё колонку со значком галочка. Сделайте так, чтобы если в варианте числа сумма быков и коров была равно четырем, то у всей этой строки фон был жёлтого цвета.
3. Самостоятельно разберитесь с тем, как правильно получить размер списка ArrayList, как его очистить. Сделайте так, что если игрок совершил 12 попыток и не смог угадать число, то он проиграл, а на экран будет выведена соответствующая надпись при помощи Toast, и игра начнётся заново.
4. Вопрос “на засыпку”. Почему для правил русского языка я сделал только варианты “1 бык”, а если больше единицы, то окончание “-а” в слове “быка”, ну типа 2 быка, 3 быка, почему я не сделал ещё, ведь правильно говорить “5 быкОВ”, “6 быкОВ”, почему нет такого варианта? Также и с коровами.
Если всё получилось как нужно – молодцы! Если нет, давайте разберёмся что не так.
Урок 6 Урок 8
El Vinto, 2023 (Copyright)