Взаимодействие с элементами экрана
Мы научились обращаться к элементам экрана, теперь начинаем взаимодействовать с ними. И сейчас перед нами стоит задача научиться реагировать на нажатия кнопок и изменять контент на экране в зависимости от действия. Мы учимся работать с инструментами, поэтому, чтобы не занимать много времени допускаем некоторые педагогические упрощения. Учитывайте, здесь код не доводится до production-ready уровня. Это заняло бы больше времени. А более целостные задания и дотошное ревью ожидает вас на практике.
Окей. Экран изучения слова открывается перед нами в виде, который продемонстрирован в макете 01.1. Все слова помечены серым, отображается кнопка SKIP.
Предлагаю привести верстку в это исходное положение. Для этого скроем атрибутом visibility инфо блок и вернем кнопку пропуска.
Теперь воспроизведем сценарий по которому мы действуем. При разработке программы лучше всего сначала проговорить (или написать на листочке) что нужно сделать.
- При клике на правильное слово мы
- Окрашиваем фон цифры варианта и контур контейнера в зеленый цвет, цифру в белый
- Скрываем кнопку пропуска
- Отображаем блок с успешным блоком и кнопкой продолжения.
- При клике на неправильное слово мы
- Окрашиваем контейнер и его элементы в красный
- Подсвечиваем зеленым правильное слово
- Показываем красный блок, информирующий что вариант неверный и кнопку продолжить.
- По кнопке продолжить сбрасываем состояние до исходного. Так как больше пока что показывать нечего.
Ну и теперь придется немного заняться программированием. Обращаю внимание, что сейчас мы все сделаем в одном-двух файлах, чтобы просто заработало. Правильная архитектура с применением стейтов и ViewModel будет дорабатываться в соответствующих уроках. Пока что мы учимся работать с элементами экрана.
Хорошо. Из описания нашего ТЗ можно выделить 3 состояния контейнера.
- Нейтральный ответ (когда ничего не выбрано)
- Корректный ответ
- Некорректный ответ
Обработка правильного ответа
Начнем с простого. Допустим 3 элемент — это правильный ответ (так и есть на самом деле). Чтобы применить к нему особые атрибуты, сперва необходимо отловить нажатие по этому контейнеру.
В Android все действия, связанные с пользовательским вводом, обрабатываются через обработчики событий. Когда пользователь производит какое-то действие на экране, например, нажимает на кнопку, система генерирует событие. Обработчик событий реагирует на это событие и мы можем задать любое действие для выполнения.
Обработчик событий можно привязать к любому view на экране. В нашем случае это контейнер, поэтому сперва задаем id элементу лейаута — layoutAnswer3. Сразу же зададим id для внутренних элементов которым надо будет передавать окрашивание по клику — tvVariantNumber3 и tvVariantValue3.
Обратимся к вьюхе через созданный только что идентификатор. Мы будем отслеживать клик, поэтому для его отлова используется метод setOnClickListener()
. Выбираем тот, что с лямбдой.
Видим сигнатуру лямбды — it с типом View. Эта view и есть тот объект, к которому мы обращаемся, то есть контейнер слова. И все что происходит внутри фигурных скобок будет выполнено при клике по этому контейнеру.
Для демонстрации я скрою элемент вью (а именно контейнер), задав значение проперти false.
binding.layoutAnswer3.setOnClickListener {
it.isVisible = false
}
isVisible = false это альтернатива установки атрибута visibility со значением gone. То есть «вью» полностью пропадает с макета и не занимает места.
Но, конечно, это не то что нам нужно. Выше я выделил состояния. Предлагаю выделить для них соответствующие функции и начнем с создания метода markAnswerCorrect(), который будем вызывать при клике на 3 элемент.
Идем по ТЗ:
- Для обводки контура правильно выбранного ответа будем задавать новый ShapeDrawable с зеленым контуром, который я предварительно создал. Также я уже вынес в ресурсы соответствующие цвета из макета, чтобы не тратить на это время. Это shape_rounded_containers_correct. Процедура обращения к ресурсам аналогичная к цвету из прошлого урока. За исключением того, что поменялся тип. Мы задаем ShapeDrawable, поэтому изменился метод и указание типа drawable поле класса R.
- По аналогии задаем новый фон для контейнера с цифрой. Используем shape_rounded_variants_correct.
- Также у нас меняется цвет текста цифры и слова — это делается с помощью метода setTextColor(). Он применяется непосредственно к view. Передаем ссылку на элемент цвета из нашего файла с ресурсами
Отлично, сделаем паузу и можно проверить работоспособность.
private fun markAnswerCorrect() {
binding.layoutAnswer3.background = ContextCompat.getDrawable(
this@MainActivity,
R.drawable.shape_rounded_containers_correct,
)
binding.tvVariantNumber3.background = ContextCompat.getDrawable(
this@MainActivity,
R.drawable.shape_rounded_variants_correct,
)
binding.tvVariantNumber3.setTextColor(
ContextCompat.getColor(
this@MainActivity,
R.color.white,
)
)
binding.tvVariantValue3.setTextColor(
ContextCompat.getColor(
this@MainActivity,
R.color.correctAnswerColor,
)
)
}
Идем далее по ТЗ.
При правильном ответе нужно скрыть кнопку пропуска слова — добавляем id кнопки, обращаемся к ней и устанавливаем значение isVisible = false.
Для инфо блока меняем фон, иконку и текст, которые я уже добавил в ресурсы. И обращаю внимание, что весь код этого проекта можно посмотреть по ссылке https://t.me/ievetrov_dev.
Добавляем id для контейнера, для иконки уже проставлен, но нужно более релевантное название и соблюдение стиля именования. Поменяем его через рефакторинг, чтобы ничего не сломалось. Также добавляем айдишники для вью результативного сообщения и кнопки продолжения.
Задаем иконку, текст правильного ответа и в конце показываем все это дело. Visibility правильнее вызывать именно в конце, чтобы не было мельканий и задержек при отрисовке элементов. Особенно, если данные приходят из сети, базы и т.д.
private fun markAnswerCorrect() {
binding.layoutAnswer3.background = ContextCompat.getDrawable(
this@MainActivity,
R.drawable.shape_rounded_containers_correct,
)
binding.tvVariantNumber3.background = ContextCompat.getDrawable(
this@MainActivity,
R.drawable.shape_rounded_variants_correct,
)
binding.tvVariantNumber3.setTextColor(
ContextCompat.getColor(
this@MainActivity,
R.color.white,
)
)
binding.tvVariantValue3.setTextColor(
ContextCompat.getColor(
this@MainActivity,
R.color.correctAnswerColor,
)
)
binding.btnSkip.isVisible = false
binding.layoutResult.setBackgroundColor(
ContextCompat.getColor(
this@MainActivity,
R.color.correctAnswerColor,
)
)
binding.ivResultIcon.setImageDrawable(
ContextCompat.getDrawable(
this@MainActivity,
R.drawable.ic_correct,
)
)
binding.tvResultMessage.text = resources.getString(R.string.title_correct)
binding.btnContinue.setTextColor(
ContextCompat.getColor(
this@MainActivity,
R.color.correctAnswerColor,
)
)
binding.layoutResult.isVisible = true
}
Property access syntax
Вы, должно быть, обратили внимание на способы задавания Drawable.
Для лейаута мы писали так:
binding.layoutAnswer3.background = ContextCompat.getDrawable(
this@MainActivity,
R.drawable.shape_rounded_containers_correct,
)
Для ImageView пишем так:
binding.ivResultIcon.setImageDrawable(
ContextCompat.getDrawable(
this@MainActivity,
R.drawable.ic_correct,
)
)
Пусть вас это не смущает. В первом случае мы используем синтаксис доступа к свойствам (property access syntax). Наведите курсор на background и в появившейся справке увидим, что это на самом деле замена вызова метода сеттера setBackground(). Причем мы даже можем его прописать явно.
binding.layoutAnswer3.setBackground(
ContextCompat.getDrawable(
this@MainActivity,
R.drawable.shape_rounded_containers_correct,
)
)
Сеттер setBackground() будет выполнять то же самое действие, но среда разработки предлагает использовать упрощенный синтаксис, чтобы обращаться напрямую к свойству. Поэтому используется знак “равно” — мы присваиваем значение свойству.
Именно такое же свойство стоит при обращении к ресурсам, где мы присваиваем текст в инфо блок. Если навести курсор на resources, высветится метод getResources(). Здесь мы обращаемся к объектам ресурсов и с помощью метода getString(), в который отправляем id строки — получаем соответствующее значение строки на необходимом языке.
Надеюсь, вы изучили мой курс по основам Kotlin и сейчас нет проблем с пониманием того, что происходит в рамках языка. Эти кирпичики и нужны были, чтобы сейчас не отвлекаться от изучения Android. А те кто прошел практику и курсовую смогут применить бОльшую часть своего кода далее. При реализации тренажера изучения иностранных слов.
Можно сделать промежуточный коммит и проверим что же у нас получилось. Запускаем и жмем на правильный вариант ответа. Пока все отрабатывает корректно.
Обработка неправильного ответа
Чтож, теперь повесим слушатель на какой-нибудь неправильный вариант ответа, пока один только ради демонстрации. И пусть это будет первый вариант.
Алгоритм тот же самый:
- Сначала проставим “айдишники” для этого элемента.
- Обращаемся к контейнеру по его id, вызываем setOnClickListener() и создаем соответствующий метод.
- Скопируем содержимое предыдущего метода. Меняем id и значения на заранее подготовленные для некорректного состояния.
Все цвета и новую иконку берем из макета (ее я тоже уже импортировал), который доступен в описании. Также обращаю внимание, что цвет инфо блока отличается от варианта ответа, не забудьте добавить его в палитру. А еще нужно поменять цвет текста кнопки. Вызываем соответствующий метод и повторяем процедуру обращения к ресурсам.
Итого получился второй метод markAnswerWrong(), который меняет состояние первого элемента и инфо блока при клике на первый неверный вариант ответа.
private fun markAnswerWrong() {
binding.layoutAnswer1.background = ContextCompat.getDrawable(
this@MainActivity,
R.drawable.shape_rounded_containers_wrong,
)
binding.tvVariantNumber1.background = ContextCompat.getDrawable(
this@MainActivity,
R.drawable.shape_rounded_variants_wrong,
)
binding.tvVariantNumber1.setTextColor(
ContextCompat.getColor(
this@MainActivity,
R.color.white,
)
)
binding.tvVariantValue1.setTextColor(
ContextCompat.getColor(
this@MainActivity,
R.color.wrongAnswerColorVariant,
)
)
binding.btnSkip.isVisible = false
binding.layoutAnswerInfo.setBackgroundColor(
ContextCompat.getColor(
this@MainActivity,
R.color.wrongAnswerColor,
)
)
binding.ivResultIcon.setImageDrawable(
ContextCompat.getDrawable(
this@MainActivity,
R.drawable.ic_wrong,
)
)
binding.tvResultMessage.text = resources.getString(R.string.title_wrong)
binding.btnContinue.setTextColor(
ContextCompat.getColor(
this@MainActivity,
R.color.wrongAnswerColor
)
)
binding.layoutAnswerInfo.isVisible = true
}
Помним, что у нас есть еще исходное состояние — нейтральное. Вызывать нейтральный стиль будем по кнопке Continue. Добавим и для него метод markAnswerNeutral(). Внутри нужно привести 1 и 3 элемент в изначальную палитру, а также скрыть инфо блок с результатом ответа и показать кнопку Skip. Логика проверки ответа и подстановки нового слова пока отсутствует, но метод нам еще пригодится.
Обработка нейтрального состояния, apply
В целом тут все должно быть понятно. Обращаемся к “айдишникам”, меняем цвета и ShapeDrawable на исходные. Но хотелось для демонстрации сделать реализацию поизящнее, лишний раз продемонстрировав красоту Kotlin.
- Сначала объявим блок with(binding), чтобы сократить код за счет отсутствия повторений вызова binding.
- Далее мы имеем два лейаута layoutAnswer1 и layoutAnswer3 к которым нужно применить одну и ту же стилизацию. А когда действия необходимо повторить нужен цикл. Объявим цикл for и в нем же создаем список из требуемых для обработки лейаутов.
- Теперь просто применим свойство background к переменной layout.
for (layout in listOf(layoutAnswer1, layoutAnswer3)) {
layout.background = ContextCompat.getDrawable(
this@MainActivity,
R.drawable.shape_rounded_containers,
)
}
- Отлично. То же самое провернем и для значений слов, окрашивая их обратно в серый цвет.
for (textView in listOf(tvVariantValue1, tvVariantValue3)) {
textView.setTextColor(
ContextCompat.getColor(
this@MainActivity,
R.color.textVariantsColor,
)
)
}
- Наконец обработаем TextView с цифрой. Здесь понадобится две модификации на одно вью. Можно вызывать их по отдельности, а можно применить специальную конструкцию apply {}. Это extension функция, которая применяется к объекту и вызывает для него методы внутри фигурных скобок. Таким образом один раз обратившись к переменной textView, устанавливаем и фон, и цвет текста.
for (textView in listOf(tvVariantNumber1, tvVariantNumber3)) {
textView.apply {
background = ContextCompat.getDrawable(
this@MainActivity,
R.drawable.shape_rounded_variants,
)
setTextColor(
ContextCompat.getColor(
this@MainActivity,
R.color.textVariantsColor,
)
)
}
}
- Наконец, убираем отображение инфоблока и возвращаем кнопку Skip.
Посмотрим что получилось. Все хорошо, захардкоженные элементы корректно меняют свой стиль.
Для тех, кто собрался стать Android-разработчиком
Пошаговая
схема
Описание процесса обучения от основ Kotlin до Android-разработчика
Бесплатные
уроки
Авторский бесплатный курс по основам языка программирования Kotlin
Обучающий
бот
Тренажер и самоучитель по Котлин – бесплатные тесты и практика