Эта практическая работа входит в курс «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
- Создайте приложение Kotlin с названием
CustomFanController
, используя шаблон Empty Activity. Убедитесь, что имя пакета —com.example.android.customfancontroller
. - Откройте
activity_main.xml
на вкладке Текст , чтобы отредактировать XML-код. - Замените существующий
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"/>
- Добавьте этот элемент
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"/>
- Извлечь строковые и размерные ресурсы из обоих элементов пользовательского интерфейса.
- Перейдите на вкладку «Дизайн» . Макет должен выглядеть так:
Шаг 2. Создайте свой собственный класс представления
- Создайте новый класс Kotlin с именем
DialView
. - Измените определение класса, чтобы расширить
View
. Импортируйтеandroid.view.View
при появлении соответствующего запроса. - Нажмите
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) {
- Над определением класса
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);
}
- Добавьте эти константы под
enum
. Они понадобятся вам для отрисовки циферблатных индикаторов и меток.
private const val RADIUS_OFFSET_LABEL = 30
private const val RADIUS_OFFSET_INDICATOR = -35
- Внутри класса
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 , которая будет использоваться для отрисовки нескольких элементов представления на экране.
Эти значения создаются и инициализируются здесь, а не при фактической отрисовке вида, чтобы гарантировать максимально быстрое выполнение фактического этапа отрисовки.
- Также внутри определения класса
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)
}
- Откройте
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. Рассчитать позиции и нарисовать вид
- В классе
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()
}
- Ниже
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
}
- Переопределите метод
onDraw()
для отрисовки изображения на экране с помощью классовCanvas
иPaint
. Импортируйтеandroid.graphics.Canvas
по запросу. Вот переопределение скелета:
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
}
- В
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
- Добавьте этот код, чтобы нарисовать круг для циферблата, используя метод
drawCircle()
. Этот метод использует текущую ширину и высоту представления для определения центра круга, его радиуса и текущего цвета краски. Свойстваwidth
иheight
являются членами суперклассаView
и указывают текущие размеры представления.
// Draw the dial.
canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), radius, paint)
- Добавьте следующий код, чтобы нарисовать меньший круг для метки индикатора скорости вращения вентилятора, также с помощью метода
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)
- Наконец, нарисуйте метки скорости вентилятора (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-элемента, как и для любого другого элемента пользовательского интерфейса.
- В
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-2-3» и обратно в положение «выкл.». Кроме того, если выбрано значение «1» или выше, фон должен измениться с серого на зелёный, указывая на то, что вентилятор включён.
Чтобы сделать ваше пользовательское представление кликабельным, вам необходимо:
- Установите свойство
isClickable
представления вtrue
. Это позволит вашему пользовательскому представлению реагировать на клики. - Реализуйте метод
performClick
()
классаView
для выполнения операций при щелчке по представлению. - Вызовите метод
invalidate()
. Это даст указание системе Android вызвать методonDraw()
для перерисовки представления.
Обычно в стандартном представлении Android вы реализуете OnClickListener()
для выполнения действия при щелчке пользователя по этому представлению. Для пользовательского представления вы реализуете метод performClick
()
класса View
и вызываете super
. performClick().
Метод performClick()
по умолчанию также вызывает onClickListener()
, поэтому вы можете добавить свои действия в performClick()
и оставить onClickListener()
доступным для дальнейшей настройки вами или другими разработчиками, которые могут использовать ваше пользовательское представление.
- В
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
}
}
- Внутри класса
DialView
, непосредственно перед методомonSizeChanged()
, добавьте блокinit()
. Установка свойстваisClickable
представления в значение true позволяет этому представлению принимать пользовательский ввод.
init {
isClickable = true
}
- Ниже
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().
- Запустите приложение. Коснитесь элемента
DialView
, чтобы переместить индикатор из положения «выкл.» в положение «1». Индикатор должен стать зелёным. С каждым нажатием индикатор должен переходить на следующую позицию. Когда индикатор возвращается в положение «выкл.», индикатор должен снова стать серым.
В этом примере показана базовая механика использования настраиваемых атрибутов в вашем пользовательском представлении. Вы определяете настраиваемые атрибуты для класса DialView
, используя разные цвета для каждой позиции шкалы.
- Создайте и откройте
res/values/attrs.xml
. - Внутри
<resources>
добавьте элемент ресурса<declare-styleable>
. - Внутри элемента ресурса
<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>
- Откройте файл макета
activity_main.xml
. - В
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
, а их значения присваиваются локальным переменным для кэширования.
- Откройте файл класса
DialView.kt
. - Внутри
DialView
объявите переменные для кэширования значений атрибутов.
private var fanSpeedLowColor = 0
private var fanSpeedMediumColor = 0
private var fanSeedMaxColor = 0
- В блоке
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)
}
- Используйте локальные переменные в
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
- Запустите приложение, нажмите на циферблат, и настройки цвета должны быть разными для каждого положения, как показано ниже.
Дополнительную информацию о пользовательских атрибутах представления см. в разделе Создание класса представления .
Доступность — это набор методов проектирования, реализации и тестирования, которые позволяют сделать ваше приложение доступным для всех, включая людей с ограниченными возможностями.
К распространённым нарушениям, которые могут повлиять на использование устройства Android, относятся слепота, слабое зрение, дальтонизм, глухота или потеря слуха, а также ограниченные двигательные навыки. Разрабатывая приложения с учётом доступности, вы улучшаете пользовательский опыт не только для пользователей с этими нарушениями, но и для всех остальных пользователей.
Android предоставляет несколько функций доступности по умолчанию в стандартных представлениях пользовательского интерфейса, таких как TextView
и Button
. Однако при создании пользовательского представления необходимо учитывать, как оно будет предоставлять доступные функции, такие как голосовые описания содержимого на экране.
В этом задании вы узнаете о TalkBack, программе чтения с экрана Android, и измените свое приложение, включив в него голосовые подсказки и описания для пользовательского представления DialView
.
Шаг 1. Изучите TalkBack
TalkBack — это встроенная программа экранного доступа Android. С её помощью пользователь может взаимодействовать со своим устройством Android, не видя экрана, поскольку Android описывает его элементы вслух. Пользователи с нарушениями зрения могут использовать TalkBack для работы с вашим приложением.
В этом задании вы включите TalkBack, чтобы понять, как работают программы чтения с экрана и как осуществлять навигацию по приложениям.
- На устройстве Android или эмуляторе перейдите в раздел Настройки > Специальные возможности > TalkBack .
- Нажмите кнопку включения/выключения, чтобы включить TalkBack.
- Нажмите «ОК» , чтобы подтвердить разрешения.
- Подтвердите пароль устройства, если потребуется. Если вы запускаете TalkBack впервые, запустится обучающее руководство. (Обучение может быть недоступно на старых устройствах.)
- Возможно, будет полезно пройти обучение с закрытыми глазами. Чтобы снова открыть обучение, перейдите в раздел «Настройки» > «Универсальный доступ» > «TalkBack» > «Настройки» > «Запустить обучение TalkBack» .
- Скомпилируйте и запустите приложение
CustomFanController
или откройте его кнопкой «Обзор» или «Недавние» на устройстве. При включённом TalkBack обратите внимание, что озвучивается название приложения, а также текст меткиTextView
(«Управление вентилятором»). Однако при нажатии на само представлениеDialView
не озвучивается информация ни о его состоянии (текущих настройках циферблата), ни о действии, которое будет выполнено при нажатии на представление для его активации.
Шаг 2. Добавьте описания содержимого для меток циферблата.
Описания содержимого описывают значение и назначение представлений в вашем приложении. Эти метки позволяют программам чтения с экрана, таким как TalkBack в Android, точно объяснять функцию каждого элемента. Для статических представлений, таких как ImageView
, вы можете добавить описание содержимого в представление в файле макета с помощью атрибута contentDescription
. Текстовые представления ( TextView
и EditText
) автоматически используют текст из представления в качестве описания содержимого.
Для пользовательского представления управления вентилятором вам необходимо динамически обновлять описание содержимого каждый раз при щелчке по представлению, чтобы указывать текущие настройки вентилятора.
- В нижней части класса
DialView
объявите функциюupdateContentDescription()
без аргументов и возвращаемого типа.
fun updateContentDescription() {
}
- В методе
updateContentDescription()
измените свойствоcontentDescription
для пользовательского представления на строковый ресурс, связанный с текущей скоростью вентилятора (выкл., 1, 2 или 3). Это те же метки, которые используются вonDraw()
при отображении циферблата на экране.
fun updateContentDescription() {
contentDescription = resources.getString(fanSpeed.label)
}
- Прокрутите страницу до блока
init()
и в конце этого блока добавьте вызовupdateContentDescription()
. Это инициализирует описание контента при инициализации представления.
init {
isClickable = true
// ...
updateContentDescription()
}
- Добавьте еще один вызов
updateContentDescription()
в методperformClick()
, непосредственно передinvalidate()
.
override fun performClick(): Boolean {
if (super.performClick()) return true
fanSpeed = fanSpeed.next()
updateContentDescription()
invalidate()
return true
}
- Скомпилируйте и запустите приложение, убедившись, что TalkBack включён. Нажмите, чтобы изменить настройки отображения циферблата, и обратите внимание, что TalkBack теперь объявляет текущую метку (выкл., 1, 2, 3), а также фразу «Дважды коснитесь для активации».
Шаг 3. Добавьте дополнительную информацию о действии по клику
На этом можно остановиться, и ваше представление можно будет использовать в TalkBack. Но было бы полезно, если бы оно не только указывало на возможность активации («Двойное касание для активации»), но и объясняло, что произойдёт при активации («Двойное касание для изменения» или «Двойное касание для сброса»).
Для этого вы добавляете информацию о действии представления (в данном случае о щелчке или касании) в информационный объект узла доступности с помощью делегата доступности. Делегат доступности позволяет настраивать функции доступности вашего приложения посредством композиции (а не наследования).
Для этой задачи вы будете использовать классы доступности в библиотеках Android Jetpack ( androidx.*
) для обеспечения обратной совместимости.
- В
DialView.kt
, в блокеinit
, установите делегат доступности для представления как новый объектAccessibilityDelegateCompat
. Импортируйтеandroidx.core.view.ViewCompat
иandroidx.core.view.AccessibilityDelegateCompat
по запросу. Эта стратегия обеспечивает максимальную обратную совместимость в вашем приложении.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
})
- Внутри объекта
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 перемещаются по этим узлам, чтобы получить информацию о представлении (например, описания озвучиваемого содержимого или возможные действия, которые можно выполнить с этим представлением). При создании пользовательского представления может также потребоваться переопределить информацию об узле, чтобы предоставить пользовательскую информацию для обеспечения доступности. В этом случае вы переопределяете информацию об узле, чтобы указать, что для действия представления имеется пользовательская информация.
- Внутри
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 для указания действия.
- Замените строку
"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)
)
}
})
- После закрывающих скобок для определения
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)
}
})
- В
res/values/strings.xml
добавьте строковые ресурсы для «Изменение» и «Сброс».
<string name="change">Change</string>
<string name="reset">Reset</string>
- Скомпилируйте и запустите приложение, убедившись, что TalkBack включён. Обратите внимание, что фраза «Дважды коснитесь для активации» теперь отображается как «Дважды коснитесь для изменения» (если скорость вентилятора ниже высокой или 3) или «Дважды коснитесь для сброса» (если скорость вентилятора уже высокая или 3). Обратите внимание, что подсказка «Дважды коснитесь для...» предоставляется самой службой TalkBack.
Загрузите код для готовой лабораторной работы.
$ git clone https://github.com/googlecodelabs/android-kotlin-drawing-custom-views
Кроме того, вы можете загрузить репозиторий в виде ZIP-файла, распаковать его и открыть в Android Studio.
- Чтобы создать пользовательское представление, которое наследует внешний вид и поведение подкласса
View
, напримерEditText
, добавьте новый класс, расширяющий этот подкласс, и внесите изменения, переопределив некоторые методы подкласса. - Чтобы создать пользовательское представление любого размера и формы, добавьте новый класс, расширяющий
View
. - Переопределите методы
View
, такие какonDraw()
, чтобы определить форму и базовый внешний вид представления. - Используйте
invalidate()
для принудительной отрисовки или перерисовки представления. - Чтобы оптимизировать производительность, выделите переменные и назначьте все необходимые значения для рисования и заливки перед их использованием в
onDraw()
, например, при инициализации переменных-членов. - Переопределите
performClick()
вместоOnClickListener
() в пользовательском представлении, чтобы обеспечить интерактивное поведение представления. Это позволит вам и другим разработчикам Android, которые могут использовать ваш класс пользовательского представления, использоватьonClickListener()
для реализации дополнительного поведения. - Добавьте пользовательское представление в XML-файл макета с атрибутами, определяющими его внешний вид, как это делается с другими элементами пользовательского интерфейса.
- Создайте файл
attrs.xml
в папкеvalues
, чтобы определить настраиваемые атрибуты. Затем вы сможете использовать эти атрибуты для настраиваемого представления в XML-файле макета.
Курс Udacity:
Документация для разработчиков Android:
- Создание пользовательских представлений
-
@JvmOverloads
- Пользовательские компоненты
- Как Android рисует представления
-
onMeasure()
-
onSizeChanged()
-
onDraw()
-
Canvas
-
Paint
-
drawText()
-
setTypeface()
-
setColor()
-
drawRect()
-
drawOval()
-
drawArc()
-
drawBitmap()
-
setStyle()
-
invalidate()
- Вид
- Входные события
- Краска
- Библиотека расширения Kotlin android-ktx
-
withStyledAttributes
- Документация Android KTX
- Оригинальный блог анонса Android KTX
- Сделайте пользовательские представления более доступными
-
AccessibilityDelegateCompat
-
AccessibilityNodeInfoCompat
-
AccessibilityNodeInfoCompat.AccessibilityActionCompat
Видео:
В этом разделе перечислены возможные домашние задания для студентов, работающих над этой лабораторной работой в рамках курса, проводимого преподавателем. Преподаватель должен выполнить следующие действия:
- При необходимости задавайте домашнее задание.
- Объясните учащимся, как следует сдавать домашние задания.
- Оцените домашние задания.
Преподаватели могут использовать эти предложения так часто или редко, как пожелают, и могут свободно задавать любые другие домашние задания, которые они сочтут подходящими.
Если вы работаете с этой лабораторной работой самостоятельно, можете использовать эти домашние задания для проверки своих знаний.
Вопрос 1
Какой метод следует переопределить, чтобы рассчитать позиции, размеры и любые другие значения, когда пользовательскому представлению впервые назначается размер?
▢ onMeasure()
▢ onSizeChanged()
▢ invalidate()
▢ onDraw()
Вопрос 2
Какой метод следует вызвать из потока пользовательского интерфейса, чтобы указать, что вы хотите перерисовать свое представление с помощью onDraw()
, после изменения значения атрибута?
▢ onMeasure()
▢ onSizeChanged()
▢ invalidate()
▢ getVisibility()
Вопрос 3
Какой метод View
следует переопределить, чтобы добавить интерактивности в ваше пользовательское представление?
▢ setOnClickListener()
▢ onSizeChanged()
▢ isClickable()
▢ performClick()
Ссылки на другие практические занятия по этому курсу см. на целевой странице практических занятий по курсу Advanced Android in Kotlin.