Урок 8: setOnClickListener() — обработка нажатий, property access

Взаимодействие с элементами экрана

Мы научились обращаться к элементам экрана, теперь начинаем взаимодействовать с ними. И сейчас перед нами стоит задача научиться реагировать на нажатия кнопок и изменять контент на экране в зависимости от действия. Мы учимся работать с инструментами, поэтому, чтобы не занимать много времени допускаем некоторые педагогические упрощения. Учитывайте, здесь код не доводится до production-ready уровня. Это заняло бы больше времени. А более целостные задания и дотошное ревью ожидает вас на практике.

Окей. Экран изучения слова открывается перед нами в виде, который продемонстрирован в макете 01.1. Все слова помечены серым, отображается кнопка SKIP.

Экран изучения слова

Предлагаю привести верстку в это исходное положение. Для этого скроем атрибутом visibility инфо блок и вернем кнопку пропуска.

Скроем атрибутом 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 элемент.

Идем по ТЗ:

  1. Для обводки контура правильно выбранного ответа будем задавать новый ShapeDrawable с зеленым контуром, который я предварительно создал. Также я уже вынес в ресурсы соответствующие цвета из макета, чтобы не тратить на это время. Это shape_rounded_containers_correct. Процедура обращения к ресурсам аналогичная к цвету из прошлого урока. За исключением того, что поменялся тип. Мы задаем ShapeDrawable, поэтому изменился метод и указание типа drawable поле класса R.
  2. По аналогии задаем новый фон для контейнера с цифрой. Используем shape_rounded_variants_correct.
  3. Также у нас меняется цвет текста цифры и слова — это делается с помощью метода 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

Обучающийбот
Обучающий
бот

Тренажер и самоучитель по Котлин – бесплатные тесты и практика

Поделиться уроком

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *