Создание пользовательских представлений

Эта практическая работа входит в курс «Advanced Android in Kotlin». Вы получите максимальную пользу от этого курса, выполняя задания последовательно, но это не обязательно. Все практическая работа курса перечислены на целевой странице практической работы «Advanced Android in Kotlin» .

Введение

Android предлагает большой набор подклассов View , таких как Button , TextView , EditText , ImageView , CheckBox и RadioButton . Вы можете использовать эти подклассы для создания пользовательского интерфейса, обеспечивающего взаимодействие с пользователем и отображение информации в вашем приложении. Если ни один из подклассов View не соответствует вашим потребностям, вы можете создать подкласс View , называемый пользовательским представлением.

Чтобы создать собственное представление, вы можете либо расширить существующий подкласс View (например, Button или EditText ), либо создать свой собственный подкласс View . Расширяя View напрямую, вы можете создать интерактивный элемент пользовательского интерфейса любого размера и формы, переопределив метод onDraw() для отображения этого элемента в View .

После создания пользовательского представления вы можете добавить его в макеты своих действий таким же образом, как вы добавляете TextView или Button .

В этом уроке показано, как создать пользовательское представление с нуля, расширив View .

Что вам уже следует знать

  • Как создать приложение с Activity и запустить его с помощью Android Studio.

Чему вы научитесь

  • Как расширить View , чтобы создать пользовательское представление.
  • Как нарисовать нестандартный вид круглой формы.
  • Как использовать прослушиватели для обработки взаимодействия пользователя с пользовательским представлением.
  • Как использовать пользовательское представление в макете.

Что ты будешь делать?

  • Расширьте View , чтобы создать пользовательское представление.
  • Инициализируйте пользовательское представление с помощью значений чертежа и рисования.
  • Переопределите onDraw() для рисования вида.
  • Используйте прослушиватели для обеспечения поведения пользовательского представления.
  • Добавьте пользовательское представление в макет.

Приложение CustomFanController демонстрирует, как создать собственный подкласс представления, расширяя класс View . Новый подкласс называется DialView .

Приложение отображает круглый элемент пользовательского интерфейса, напоминающий физический регулятор вентилятора, с настройками: «Выкл.» (0), «Низкая» (1), «Средняя» (2) и «Высокая» (3). При касании представления индикатор выбора перемещается на следующую позицию: 0-1-2-3, и обратно на 0. Кроме того, если выбрано значение 1 или выше, цвет фона круглой части представления меняется с серого на зелёный (что означает, что вентилятор включён).

Представления — это основные строительные блоки пользовательского интерфейса приложения. Класс View предоставляет множество подклассов, называемых виджетами пользовательского интерфейса (UI widgets) , которые охватывают многие потребности типичного пользовательского интерфейса приложения Android.

Такие элементы пользовательского интерфейса, как Button и TextView являются подклассами, расширяющими класс View . Чтобы сэкономить время и усилия на разработку, вы можете расширить один из этих подклассов View . Пользовательское представление наследует внешний вид и поведение своего родительского элемента, и вы можете переопределить поведение или аспект внешнего вида, который хотите изменить. Например, если вы расширяете EditText для создания пользовательского представления, оно будет работать так же, как представление EditText , но его также можно настроить для отображения, например, кнопки X , которая очищает текст в поле ввода.

Вы можете расширить любой подкласс View , например EditText , чтобы получить пользовательское представление — выберите наиболее близкое к желаемому результату. Затем вы можете использовать пользовательское представление, как и любой другой подкласс View в одном или нескольких макетах как XML-элемент с атрибутами.

Чтобы создать собственное пользовательское представление с нуля, расширьте класс View . Ваш код переопределяет методы View для определения внешнего вида и функциональности представления. Ключевым моментом в создании собственного пользовательского представления является то, что вы отвечаете за отрисовку всего элемента пользовательского интерфейса любого размера и формы на экране. Если вы создаёте подкласс существующего представления, например Button , этот класс будет управлять отрисовкой автоматически. (Подробнее о рисовании вы узнаете далее в этой практической работе.)

Чтобы создать пользовательское представление, выполните следующие общие шаги:

  • Создайте пользовательский класс представления, который расширяет View или расширяет подкласс View (например, Button или EditText ).
  • Если вы расширяете существующий подкласс View , переопределите только то поведение или аспекты внешнего вида, которые вы хотите изменить.
  • Если вы расширяете класс View , нарисуйте форму пользовательского представления и управляйте его внешним видом, переопределив методы View , такие как onDraw() и onMeasure() в новом классе.
  • Добавьте код для реагирования на взаимодействие с пользователем и, при необходимости, перерисуйте пользовательское представление.
  • Используйте класс пользовательского представления в качестве виджета пользовательского интерфейса в XML-макете вашей активности. Вы также можете определить пользовательские атрибуты для представления, чтобы настроить его в различных макетах.

В этом задании вам предстоит:

  • Создайте приложение с ImageView в качестве временного заполнителя для пользовательского представления.
  • Расширьте View , чтобы создать пользовательское представление.
  • Инициализируйте пользовательское представление с помощью значений чертежа и рисования.

Шаг 1: Создайте приложение с заполнителем ImageView

  1. Создайте приложение Kotlin с названием CustomFanController , используя шаблон Empty Activity. Убедитесь, что имя пакета — com.example.android.customfancontroller .
  2. Откройте activity_main.xml на вкладке Текст , чтобы отредактировать XML-код.
  3. Замените существующий TextView этим кодом. Этот текст будет использоваться в качестве метки в активности для пользовательского представления.
<TextView
       android:id="@+id/customViewLabel"
       android:textAppearance="@style/Base.TextAppearance.AppCompat.Display3"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:padding="16dp"
       android:textColor="@android:color/black"
       android:layout_marginStart="8dp"
       android:layout_marginEnd="8dp"
       android:layout_marginTop="24dp"
       android:text="Fan Control"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toTopOf="parent"/>
  1. Добавьте этот элемент ImageView в макет. Это заполнитель для пользовательского представления, которое вы создадите в этой практической работе.
<ImageView
       android:id="@+id/dialView"
       android:layout_width="200dp"
       android:layout_height="200dp"
       android:background="@android:color/darker_gray"
       app:layout_constraintTop_toBottomOf="@+id/customViewLabel"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       android:layout_marginLeft="8dp"
       android:layout_marginRight="8dp"
       android:layout_marginTop="8dp"/>
  1. Извлечь строковые и размерные ресурсы из обоих элементов пользовательского интерфейса.
  2. Перейдите на вкладку «Дизайн» . Макет должен выглядеть так:

Шаг 2. Создайте свой собственный класс представления

  1. Создайте новый класс Kotlin с именем DialView .
  2. Измените определение класса, чтобы расширить View . Импортируйте android.view.View при появлении соответствующего запроса.
  3. Нажмите View , а затем нажмите на красную лампочку. Выберите «Добавить конструкторы Android View с помощью @JvmOverloads» . Android Studio добавит конструктор из класса View . Аннотация @JvmOverloads указывает компилятору Kotlin сгенерировать перегрузки для этой функции, которые заменяют значения параметров по умолчанию.
class DialView @JvmOverloads constructor(
   context: Context,
   attrs: AttributeSet? = null,
   defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
  1. Над определением класса DialView , сразу под импортируемыми элементами, добавьте enum верхнего уровня для представления доступных скоростей вентилятора. Обратите внимание, что это enum имеет тип Int , поскольку значения представляют собой строковые ресурсы, а не настоящие строки. Android Studio выдаст ошибки из-за отсутствия строковых ресурсов в каждом из этих значений; вы исправите это позже.
private enum class FanSpeed(val label: Int) {
   OFF(R.string.fan_off),
   LOW(R.string.fan_low),
   MEDIUM(R.string.fan_medium),
   HIGH(R.string.fan_high);
}
  1. Добавьте эти константы под enum . Они понадобятся вам для отрисовки циферблатных индикаторов и меток.
private const val RADIUS_OFFSET_LABEL = 30      
private const val RADIUS_OFFSET_INDICATOR = -35
  1. Внутри класса DialView определите несколько переменных, необходимых для отрисовки пользовательского представления. При необходимости импортируйте android.graphics.PointF .
private var radius = 0.0f                   // Radius of the circle.
private var fanSpeed = FanSpeed.OFF         // The active selection.
// position variable which will be used to draw label and indicator circle position
private val pointPosition: PointF = PointF(0.0f, 0.0f)
  • Радиусradius текущий радиус окружности. Это значение устанавливается при отображении изображения на экране.
  • Параметр fanSpeed — это текущая скорость вращения вентилятора , которая является одним из значений перечисления FanSpeed . По умолчанию это значение равно OFF .
  • Наконец, postPosition — это точка X,Y , которая будет использоваться для отрисовки нескольких элементов представления на экране.

Эти значения создаются и инициализируются здесь, а не при фактической отрисовке вида, чтобы гарантировать максимально быстрое выполнение фактического этапа отрисовки.

  1. Также внутри определения класса DialView инициализируйте объект Paint с несколькими базовыми стилями. Импортируйте android.graphics.Paint и android.graphics.Typeface по запросу. Как и в случае с переменными, эти стили инициализируются здесь для ускорения процесса рисования.
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
   style = Paint.Style.FILL
   textAlign = Paint.Align.CENTER
   textSize = 55.0f
   typeface = Typeface.create( "", Typeface.BOLD)
}
  1. Откройте res/values/strings.xml и добавьте строковые ресурсы для скорости вращения вентилятора:
<string name="fan_off">off</string>
<string name="fan_low">1</string>
<string name="fan_medium">2</string>
<string name="fan_high">3</string>

После создания пользовательского представления необходимо иметь возможность его отрисовки. При расширении подкласса View , например EditText , этот подкласс определяет внешний вид и атрибуты представления и отображает себя на экране. Следовательно, вам не нужно писать код для отрисовки представления. Вместо этого вы можете переопределить методы родительского класса, чтобы настроить представление.

Если вы создаёте собственное представление с нуля (расширяя View ), вы отвечаете за отрисовку всего представления при каждом обновлении экрана и за переопределение методов View , отвечающих за отрисовку. Для корректной отрисовки пользовательского представления, расширяющего View , необходимо:

  • Рассчитайте размер представления при его первом появлении и каждый раз при изменении его размера, переопределив метод onSizeChanged() .
  • Переопределите метод onDraw() для рисования пользовательского представления, используя объект Canvas , стилизованный под объект Paint .
  • Вызовите метод invalidate() в ответ на щелчок пользователя, который изменяет способ отрисовки представления, чтобы сделать недействительным все представление, тем самым принудительно вызывая вызов onDraw() для перерисовки представления.

Метод onDraw() вызывается каждый раз при обновлении экрана, что может происходить много раз в секунду. Из соображений производительности и во избежание визуальных сбоев следует выполнять как можно меньше действий в onDraw() . В частности, не следует размещать выделение памяти в onDraw() , поскольку это может привести к сборке мусора, что может привести к визуальному подтормаживанию.

Классы Canvas и Paint предлагают ряд полезных сокращений для рисования:

  • Для рисования текста используйте drawText() . Для указания шрифта используйте функцию setTypeface() , а для указания цвета текста — setColor() .
  • Рисуйте примитивные фигуры с помощью drawRect() , drawOval() и drawArc() . Чтобы изменить заливку, контур или и то, и другое, вызовите setStyle() .
  • Рисуйте растровые изображения с помощью drawBitmap() .

Подробнее о Canvas и Paint вы узнаете в следующем практическом занятии. Подробнее о том, как Android отрисовывает представления, см. в статье «Как Android отрисовывает представления» .

В этом задании вы отобразите на экране пользовательское представление контроллера вентилятора — сам циферблат, индикатор текущего положения и метки индикатора — с помощью методов onSizeChanged() и onDraw() . Вы также создадите вспомогательный метод computeXYForSpeed(), для вычисления текущего положения метки индикатора на циферблате по осям X и Y.

Шаг 1. Рассчитать позиции и нарисовать вид

  1. В классе DialView , под инициализациями, переопределите метод onSizeChanged() из класса View math.min расчета размера циферблата пользовательского представления. Импортируйте kotlin по запросу.

    Метод onSizeChanged() вызывается каждый раз при изменении размера представления, включая первую отрисовку при увеличении макета. Переопределите onSizeChanged() для расчета положения, размеров и любых других значений, связанных с размером вашего пользовательского представления, вместо того, чтобы пересчитывать их при каждой отрисовке. В этом случае метод onSizeChanged() используется для расчета текущего радиуса круга циферблата.
override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
   radius = (min(width, height) / 2.0 * 0.8).toFloat()
}
  1. Ниже onSizeChanged() добавьте этот код, чтобы определить функцию расширения computeXYForSpeed() для PointF Класс. Импортируйте kotlin.math.cos и kotlin.math.sin по запросу. Эта функция расширения класса PointF вычисляет координаты X и Y на экране для текстовой метки и текущего индикатора (0, 1, 2 или 3) с учётом текущего положения FanSpeed и радиуса циферблата. Это будет использоваться в onDraw().
private fun PointF.computeXYForSpeed(pos: FanSpeed, radius: Float) {
   // Angles are in radians.
   val startAngle = Math.PI * (9 / 8.0)   
   val angle = startAngle + pos.ordinal * (Math.PI / 4)
   x = (radius * cos(angle)).toFloat() + width / 2
   y = (radius * sin(angle)).toFloat() + height / 2
}
  1. Переопределите метод onDraw() для отрисовки изображения на экране с помощью классов Canvas и Paint . Импортируйте android.graphics.Canvas по запросу. Вот переопределение скелета:
override fun onDraw(canvas: Canvas) {
   super.onDraw(canvas)
   
}
  1. В onDraw() добавьте эту строку, чтобы установить цвет краски: серый ( Color.GRAY ) или зелёный ( Color.GREEN ) в зависимости от того, OFF ли скорость вентилятора или установлена на другое значение. Импортируйте android.graphics.Color по запросу.
// Set dial background color to green if selection not off.
paint.color = if (fanSpeed == FanSpeed.OFF) Color.GRAY else Color.GREEN
  1. Добавьте этот код, чтобы нарисовать круг для циферблата, используя метод drawCircle() . Этот метод использует текущую ширину и высоту представления для определения центра круга, его радиуса и текущего цвета краски. Свойства width и height являются членами суперкласса View и указывают текущие размеры представления.
// Draw the dial.
canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), radius, paint)
  1. Добавьте следующий код, чтобы нарисовать меньший круг для метки индикатора скорости вращения вентилятора, также с помощью метода drawCircle() В этой части используется метод расширения PointF . computeXYforSpeed() для вычисления координат X, Y для центра индикатора на основе текущей скорости вращения вентилятора.
// Draw the indicator circle.
val markerRadius = radius + RADIUS_OFFSET_INDICATOR
pointPosition.computeXYForSpeed(fanSpeed, markerRadius)
paint.color = Color.BLACK
canvas.drawCircle(pointPosition.x, pointPosition.y, radius/12, paint)
  1. Наконец, нарисуйте метки скорости вентилятора (0, 1, 2, 3) в соответствующих местах вокруг циферблата. Эта часть метода снова вызывает PointF.computeXYForSpeed() для определения положения каждой метки и каждый раз повторно использует объект pointPosition , чтобы избежать выделения памяти. Для рисования меток используйте drawText() .
// Draw the text labels.
val labelRadius = radius + RADIUS_OFFSET_LABEL
for (i in FanSpeed.values()) {
   pointPosition.computeXYForSpeed(i, labelRadius)
   val label = resources.getString(i.label)
   canvas.drawText(label, pointPosition.x, pointPosition.y, paint)
}

Завершенный метод onDraw() выглядит так:

override fun onDraw(canvas: Canvas) {
   super.onDraw(canvas)
   // Set dial background color to green if selection not off.
   paint.color = if (fanSpeed == FanSpeed.OFF) Color.GRAY else Color.GREEN
   // Draw the dial.
   canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), radius, paint)
   // Draw the indicator circle.
   val markerRadius = radius + RADIUS_OFFSET_INDICATOR
   pointPosition.computeXYForSpeed(fanSpeed, markerRadius)
   paint.color = Color.BLACK
   canvas.drawCircle(pointPosition.x, pointPosition.y, radius/12, paint)
   // Draw the text labels.
   val labelRadius = radius + RADIUS_OFFSET_LABEL
   for (i in FanSpeed.values()) {
       pointPosition.computeXYForSpeed(i, labelRadius)
       val label = resources.getString(i.label)
       canvas.drawText(label, pointPosition.x, pointPosition.y, paint)
   }
}

Шаг 2. Добавьте представление в макет.

Чтобы добавить пользовательское представление в пользовательский интерфейс приложения, укажите его как элемент в XML-макете активности. Управляйте его внешним видом и поведением с помощью атрибутов XML-элемента, как и для любого другого элемента пользовательского интерфейса.

  1. В activity_main.xml измените тег ImageView для dialView на com.example.android.customfancontroller.DialView и удалите атрибут android:background . И DialView , и исходный ImageView наследуют стандартные атрибуты класса View , поэтому нет необходимости изменять какие-либо другие атрибуты. Новый элемент DialView выглядит следующим образом:
<com.example.android.customfancontroller.DialView
       android:id="@+id/dialView"
       android:layout_width="@dimen/fan_dimen"
       android:layout_height="@dimen/fan_dimen"
       app:layout_constraintTop_toBottomOf="@+id/customViewLabel"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       android:layout_marginLeft="@dimen/default_margin"
       android:layout_marginRight="@dimen/default_margin"
       android:layout_marginTop="@dimen/default_margin" />
  1. Запустите приложение. В окне активности появится окно управления вентилятором.

Последняя задача — настроить пользовательское представление для выполнения действия при касании пользователем. Каждое касание должно перемещать индикатор выбора в следующее положение: «выкл.» — «1-2-3» и обратно в положение «выкл.». Кроме того, если выбрано значение «1» или выше, фон должен измениться с серого на зелёный, указывая на то, что вентилятор включён.

Чтобы сделать ваше пользовательское представление кликабельным, вам необходимо:

  • Установите свойство isClickable представления в true . Это позволит вашему пользовательскому представлению реагировать на клики.
  • Реализуйте метод performClick () класса View для выполнения операций при щелчке по представлению.
  • Вызовите метод invalidate() . Это даст указание системе Android вызвать метод onDraw() для перерисовки представления.

Обычно в стандартном представлении Android вы реализуете OnClickListener() для выполнения действия при щелчке пользователя по этому представлению. Для пользовательского представления вы реализуете метод performClick () класса View и вызываете super . performClick(). Метод performClick() по умолчанию также вызывает onClickListener() , поэтому вы можете добавить свои действия в performClick() и оставить onClickListener() доступным для дальнейшей настройки вами или другими разработчиками, которые могут использовать ваше пользовательское представление.

  1. В DialView.kt , внутри перечисления FanSpeed , добавьте функцию расширения next() , которая изменяет текущую скорость вентилятора на следующую скорость в списке (с OFF на LOW , MEDIUM и HIGH , а затем обратно на OFF ). Полный список теперь выглядит следующим образом:
private enum class FanSpeed(val label: Int) {
   OFF(R.string.fan_off),
   LOW(R.string.fan_low),
   MEDIUM(R.string.fan_medium),
   HIGH(R.string.fan_high);

   fun next() = when (this) {
       OFF -> LOW
       LOW -> MEDIUM
       MEDIUM -> HIGH
       HIGH -> OFF
   }
}
  1. Внутри класса DialView , непосредственно перед методом onSizeChanged() , добавьте блок init() . Установка свойства isClickable представления в значение true позволяет этому представлению принимать пользовательский ввод.
init {
   isClickable = true
}
  1. Ниже init(), переопределите метод performClick() с помощью приведенного ниже кода.
override fun performClick(): Boolean {
   if (super.performClick()) return true

   fanSpeed = fanSpeed.next()
   contentDescription = resources.getString(fanSpeed.label)
  
   invalidate()
   return true
}

Сначала должен произойти вызов super . performClick() , который включает события доступности, а также вызывает onClickListener() .

Следующие две строки увеличивают скорость вентилятора с помощью метода next() и устанавливают описание содержимого представления для строкового ресурса, представляющего текущую скорость (выкл., 1, 2 или 3).

Наконец, метод invalidate() делает всё представление недействительным, вызывая вызов onDraw() для его перерисовки. Если что-то в вашем пользовательском представлении изменилось по какой-либо причине, включая взаимодействие с пользователем, и это изменение необходимо отобразить, вызовите invalidate().

  1. Запустите приложение. Коснитесь элемента DialView , чтобы переместить индикатор из положения «выкл.» в положение «1». Индикатор должен стать зелёным. С каждым нажатием индикатор должен переходить на следующую позицию. Когда индикатор возвращается в положение «выкл.», индикатор должен снова стать серым.

В этом примере показана базовая механика использования настраиваемых атрибутов в вашем пользовательском представлении. Вы определяете настраиваемые атрибуты для класса DialView , используя разные цвета для каждой позиции шкалы.

  1. Создайте и откройте res/values/attrs.xml .
  2. Внутри <resources> добавьте элемент ресурса <declare-styleable> .
  3. Внутри элемента ресурса <declare-styleable> добавьте три элемента attr , по одному для каждого атрибута, с name и format . format похож на тип, и в данном случае это color .
<?xml version="1.0" encoding="utf-8"?>
<resources>
       <declare-styleable name="DialView">
           <attr name="fanColor1" format="color" />
           <attr name="fanColor2" format="color" />
           <attr name="fanColor3" format="color" />
       </declare-styleable>
</resources>
  1. Откройте файл макета activity_main.xml .
  2. В DialView добавьте атрибуты fanColor1 , fanColor2 и fanColor3 и задайте для них значения цветов, показанных ниже. Используйте app: в качестве префикса для настраиваемого атрибута (например app:fanColor1 ) вместо android: поскольку ваши настраиваемые атрибуты принадлежат пространству имён schemas.android.com/apk/res/ your_app_package_name , а не пространству имён android .
app:fanColor1="#FFEB3B"
app:fanColor2="#CDDC39"
app:fanColor3="#009688"

Чтобы использовать атрибуты в классе DialView , их необходимо извлечь. Они хранятся в AttributeSet , который передается вашему классу при создании, если он существует. Атрибуты извлекаются в init , а их значения присваиваются локальным переменным для кэширования.

  1. Откройте файл класса DialView.kt .
  2. Внутри DialView объявите переменные для кэширования значений атрибутов.
private var fanSpeedLowColor = 0
private var fanSpeedMediumColor = 0
private var fanSeedMaxColor = 0
  1. В блоке init добавьте следующий код, используя функцию расширения withStyledAttributes . Вы указываете атрибуты и представление, а также задаёте локальные переменные. Импорт withStyledAttributes также импортирует нужную функцию getColor() .
context.withStyledAttributes(attrs, R.styleable.DialView) {
   fanSpeedLowColor = getColor(R.styleable.DialView_fanColor1, 0)
   fanSpeedMediumColor = getColor(R.styleable.DialView_fanColor2, 0)
   fanSeedMaxColor = getColor(R.styleable.DialView_fanColor3, 0)
}
  1. Используйте локальные переменные в onDraw() для установки цвета циферблата в зависимости от текущей скорости вращения вентилятора. Замените color . где задаётся цвет краски ( paint = if ( fanSpeed == FanSpeed. OFF ) Color. else Color. ), GRAY GREEN код.
paint.color = when (fanSpeed) {
   FanSpeed.OFF -> Color.GRAY
   FanSpeed.LOW -> fanSpeedLowColor
   FanSpeed.MEDIUM -> fanSpeedMediumColor
   FanSpeed.HIGH -> fanSeedMaxColor
} as Int
  1. Запустите приложение, нажмите на циферблат, и настройки цвета должны быть разными для каждого положения, как показано ниже.

Дополнительную информацию о пользовательских атрибутах представления см. в разделе Создание класса представления .

Доступность — это набор методов проектирования, реализации и тестирования, которые позволяют сделать ваше приложение доступным для всех, включая людей с ограниченными возможностями.

К распространённым нарушениям, которые могут повлиять на использование устройства Android, относятся слепота, слабое зрение, дальтонизм, глухота или потеря слуха, а также ограниченные двигательные навыки. Разрабатывая приложения с учётом доступности, вы улучшаете пользовательский опыт не только для пользователей с этими нарушениями, но и для всех остальных пользователей.

Android предоставляет несколько функций доступности по умолчанию в стандартных представлениях пользовательского интерфейса, таких как TextView и Button . Однако при создании пользовательского представления необходимо учитывать, как оно будет предоставлять доступные функции, такие как голосовые описания содержимого на экране.

В этом задании вы узнаете о TalkBack, программе чтения с экрана Android, и измените свое приложение, включив в него голосовые подсказки и описания для пользовательского представления DialView .

Шаг 1. Изучите TalkBack

TalkBack — это встроенная программа экранного доступа Android. С её помощью пользователь может взаимодействовать со своим устройством Android, не видя экрана, поскольку Android описывает его элементы вслух. Пользователи с нарушениями зрения могут использовать TalkBack для работы с вашим приложением.

В этом задании вы включите TalkBack, чтобы понять, как работают программы чтения с экрана и как осуществлять навигацию по приложениям.

  1. На устройстве Android или эмуляторе перейдите в раздел Настройки > Специальные возможности > TalkBack .
  2. Нажмите кнопку включения/выключения, чтобы включить TalkBack.
  3. Нажмите «ОК» , чтобы подтвердить разрешения.
  4. Подтвердите пароль устройства, если потребуется. Если вы запускаете TalkBack впервые, запустится обучающее руководство. (Обучение может быть недоступно на старых устройствах.)
  5. Возможно, будет полезно пройти обучение с закрытыми глазами. Чтобы снова открыть обучение, перейдите в раздел «Настройки» > «Универсальный доступ» > «TalkBack» > «Настройки» > «Запустить обучение TalkBack» .
  6. Скомпилируйте и запустите приложение CustomFanController или откройте его кнопкой «Обзор» или «Недавние» на устройстве. При включённом TalkBack обратите внимание, что озвучивается название приложения, а также текст метки TextView («Управление вентилятором»). Однако при нажатии на само представление DialView не озвучивается информация ни о его состоянии (текущих настройках циферблата), ни о действии, которое будет выполнено при нажатии на представление для его активации.

Шаг 2. Добавьте описания содержимого для меток циферблата.

Описания содержимого описывают значение и назначение представлений в вашем приложении. Эти метки позволяют программам чтения с экрана, таким как TalkBack в Android, точно объяснять функцию каждого элемента. Для статических представлений, таких как ImageView , вы можете добавить описание содержимого в представление в файле макета с помощью атрибута contentDescription . Текстовые представления ( TextView и EditText ) автоматически используют текст из представления в качестве описания содержимого.

Для пользовательского представления управления вентилятором вам необходимо динамически обновлять описание содержимого каждый раз при щелчке по представлению, чтобы указывать текущие настройки вентилятора.

  1. В нижней части класса DialView объявите функцию updateContentDescription() без аргументов и возвращаемого типа.
fun updateContentDescription() {
}
  1. В методе updateContentDescription() измените свойство contentDescription для пользовательского представления на строковый ресурс, связанный с текущей скоростью вентилятора (выкл., 1, 2 или 3). Это те же метки, которые используются в onDraw() при отображении циферблата на экране.
fun updateContentDescription() {
   contentDescription = resources.getString(fanSpeed.label)
}
  1. Прокрутите страницу до блока init() и в конце этого блока добавьте вызов updateContentDescription() . Это инициализирует описание контента при инициализации представления.
init {
   isClickable = true
   // ...

   updateContentDescription()
}
  1. Добавьте еще один вызов updateContentDescription() в метод performClick() , непосредственно перед invalidate() .
override fun performClick(): Boolean {
   if (super.performClick()) return true
   fanSpeed = fanSpeed.next()
   updateContentDescription()
   invalidate()
   return true
}
  1. Скомпилируйте и запустите приложение, убедившись, что TalkBack включён. Нажмите, чтобы изменить настройки отображения циферблата, и обратите внимание, что TalkBack теперь объявляет текущую метку (выкл., 1, 2, 3), а также фразу «Дважды коснитесь для активации».

Шаг 3. Добавьте дополнительную информацию о действии по клику

На этом можно остановиться, и ваше представление можно будет использовать в TalkBack. Но было бы полезно, если бы оно не только указывало на возможность активации («Двойное касание для активации»), но и объясняло, что произойдёт при активации («Двойное касание для изменения» или «Двойное касание для сброса»).

Для этого вы добавляете информацию о действии представления (в данном случае о щелчке или касании) в информационный объект узла доступности с помощью делегата доступности. Делегат доступности позволяет настраивать функции доступности вашего приложения посредством композиции (а не наследования).

Для этой задачи вы будете использовать классы доступности в библиотеках Android Jetpack ( androidx.* ) для обеспечения обратной совместимости.

  1. В DialView.kt , в блоке init , установите делегат доступности для представления как новый объект AccessibilityDelegateCompat . Импортируйте androidx.core.view.ViewCompat и androidx.core.view.AccessibilityDelegateCompat по запросу. Эта стратегия обеспечивает максимальную обратную совместимость в вашем приложении.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
   
})
  1. Внутри объекта AccessibilityDelegateCompat переопределите функцию onInitializeAccessibilityNodeInfo() объектом AccessibilityNodeInfoCompat и вызовите метод суперкласса. Импортируйте androidx.core.view.accessibility.AccessibilityNodeInfoCompat при появлении соответствующего запроса.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
   override fun onInitializeAccessibilityNodeInfo(host: View, 
                            info: AccessibilityNodeInfoCompat) {
      super.onInitializeAccessibilityNodeInfo(host, info)

   }  
})

Каждое представление имеет дерево узлов доступности, которые могут соответствовать или не соответствовать фактическим компонентам макета представления. Службы доступности Android перемещаются по этим узлам, чтобы получить информацию о представлении (например, описания озвучиваемого содержимого или возможные действия, которые можно выполнить с этим представлением). При создании пользовательского представления может также потребоваться переопределить информацию об узле, чтобы предоставить пользовательскую информацию для обеспечения доступности. В этом случае вы переопределяете информацию об узле, чтобы указать, что для действия представления имеется пользовательская информация.

  1. Внутри onInitializeAccessibilityNodeInfo() создайте новый объект AccessibilityNodeInfoCompat.AccessibilityActionCompat и присвойте его переменной customClick . Передайте в конструктор константу AccessibilityNodeInfo.ACTION_CLICK и строку-заполнитель. Импортируйте AccessibilityNodeInfo по запросу.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
   override fun onInitializeAccessibilityNodeInfo(host: View, 
                            info: AccessibilityNodeInfoCompat) {
      super.onInitializeAccessibilityNodeInfo(host, info)
      val customClick = AccessibilityNodeInfoCompat.AccessibilityActionCompat(
         AccessibilityNodeInfo.ACTION_CLICK,
        "placeholder"
      )
   }  
})

Класс AccessibilityActionCompat представляет действие над представлением для обеспечения доступности. Типичным действием является щелчок или касание, как в данном случае, но могут также использоваться другие действия, включая получение или потерю фокуса, операции с буфером обмена (вырезать/копировать/вставить) или прокрутку внутри представления. Конструктор этого класса требует константу действия (в данном случае AccessibilityNodeInfo.ACTION_CLICK ) и строку, которая используется TalkBack для указания действия.

  1. Замените строку "placeholder" вызовом context.getString() для получения строкового ресурса. Для конкретного ресурса проверьте текущую скорость вентилятора. Если текущая скорость равна FanSpeed.HIGH , строка будет "Reset" . Если скорость вентилятора другая, строка будет "Change." Эти строковые ресурсы будут созданы позже.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
   override fun onInitializeAccessibilityNodeInfo(host: View, 
                            info: AccessibilityNodeInfoCompat) {
      super.onInitializeAccessibilityNodeInfo(host, info)
      val customClick = AccessibilityNodeInfoCompat.AccessibilityActionCompat(
         AccessibilityNodeInfo.ACTION_CLICK,
        context.getString(if (fanSpeed !=  FanSpeed.HIGH) R.string.change else R.string.reset)
      )
   }  
})
  1. После закрывающих скобок для определения customClick используйте метод addAction() , чтобы добавить новое действие доступности к объекту информации об узле.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
   override fun onInitializeAccessibilityNodeInfo(host: View, 
                            info: AccessibilityNodeInfoCompat) {
       super.onInitializeAccessibilityNodeInfo(host, info)
       val customClick = AccessibilityNodeInfoCompat.AccessibilityActionCompat(
           AccessibilityNodeInfo.ACTION_CLICK,
           context.getString(if (fanSpeed !=  FanSpeed.HIGH) 
                                 R.string.change else R.string.reset)
       )
       info.addAction(customClick)
   }
})
  1. В res/values/strings.xml добавьте строковые ресурсы для «Изменение» и «Сброс».
<string name="change">Change</string>
<string name="reset">Reset</string>
  1. Скомпилируйте и запустите приложение, убедившись, что TalkBack включён. Обратите внимание, что фраза «Дважды коснитесь для активации» теперь отображается как «Дважды коснитесь для изменения» (если скорость вентилятора ниже высокой или 3) или «Дважды коснитесь для сброса» (если скорость вентилятора уже высокая или 3). Обратите внимание, что подсказка «Дважды коснитесь для...» предоставляется самой службой TalkBack.

Загрузите код для готовой лабораторной работы.

$  git clone https://github.com/googlecodelabs/android-kotlin-drawing-custom-views


Кроме того, вы можете загрузить репозиторий в виде ZIP-файла, распаковать его и открыть в Android Studio.

Загрузить ZIP-архив

  • Чтобы создать пользовательское представление, которое наследует внешний вид и поведение подкласса View , например EditText , добавьте новый класс, расширяющий этот подкласс, и внесите изменения, переопределив некоторые методы подкласса.
  • Чтобы создать пользовательское представление любого размера и формы, добавьте новый класс, расширяющий View .
  • Переопределите методы View , такие как onDraw() , чтобы определить форму и базовый внешний вид представления.
  • Используйте invalidate() для принудительной отрисовки или перерисовки представления.
  • Чтобы оптимизировать производительность, выделите переменные и назначьте все необходимые значения для рисования и заливки перед их использованием в onDraw() , например, при инициализации переменных-членов.
  • Переопределите performClick() вместо OnClickListener () в пользовательском представлении, чтобы обеспечить интерактивное поведение представления. Это позволит вам и другим разработчикам Android, которые могут использовать ваш класс пользовательского представления, использовать onClickListener() для реализации дополнительного поведения.
  • Добавьте пользовательское представление в XML-файл макета с атрибутами, определяющими его внешний вид, как это делается с другими элементами пользовательского интерфейса.
  • Создайте файл attrs.xml в папке values , чтобы определить настраиваемые атрибуты. Затем вы сможете использовать эти атрибуты для настраиваемого представления в XML-файле макета.

Курс Udacity:

Документация для разработчиков Android:

Видео:

В этом разделе перечислены возможные домашние задания для студентов, работающих над этой лабораторной работой в рамках курса, проводимого преподавателем. Преподаватель должен выполнить следующие действия:

  • При необходимости задавайте домашнее задание.
  • Объясните учащимся, как следует сдавать домашние задания.
  • Оцените домашние задания.

Преподаватели могут использовать эти предложения так часто или редко, как пожелают, и могут свободно задавать любые другие домашние задания, которые они сочтут подходящими.

Если вы работаете с этой лабораторной работой самостоятельно, можете использовать эти домашние задания для проверки своих знаний.

Вопрос 1

Какой метод следует переопределить, чтобы рассчитать позиции, размеры и любые другие значения, когда пользовательскому представлению впервые назначается размер?

onMeasure()

onSizeChanged()

invalidate()

onDraw()

Вопрос 2

Какой метод следует вызвать из потока пользовательского интерфейса, чтобы указать, что вы хотите перерисовать свое представление с помощью onDraw() , после изменения значения атрибута?

▢ onMeasure()

▢ onSizeChanged()

▢ invalidate()

▢ getVisibility()

Вопрос 3

Какой метод View следует переопределить, чтобы добавить интерактивности в ваше пользовательское представление?

▢ setOnClickListener()

▢ onSizeChanged()

▢ isClickable()

▢ performClick()

Ссылки на другие практические занятия по этому курсу см. на целевой странице практических занятий по курсу Advanced Android in Kotlin.